Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<template> layering proposal #813

Closed
wants to merge 11 commits into from
Closed

<template> layering proposal #813

wants to merge 11 commits into from

Conversation

chancancode
Copy link
Member

@chancancode chancancode commented Apr 16, 2022

@chancancode chancancode marked this pull request as draft April 16, 2022 22:01
@chancancode chancancode changed the title [WIP] <template> layering proposal <template> layering proposal Apr 16, 2022
@chancancode
Copy link
Member Author

By the way, one of the things that I think was a missed opportunity is we didn't discuss whitespace-folding rules for <template> (which isn't too late to do still). We should take the opportunity to explore the possibility of auto-folding whitespaces or at least auto trimming the indentation from the tag? That has implications around how careful the de-suagring needs to be when it comes to matching the whitespace/indentation. That said, I think it is feasible to totally match match the whitespaces/indentations/lines exactly with this design in a transformation, if that turned out to be important.

@nightire
Copy link

We should take the opportunity to explore the possibility of auto-folding whitespaces or at least auto trimming the indentation from the tag

In that case, it should provide an option to preserve the whitespace for particular purposes like rendering code blocks, etc.

@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented Apr 17, 2022

Edit: I talked myself in and out of an approach in this comment, and do not think this is straightforward as my first sentence states 😅


it should provide an option to preserve the whitespace for particular purposes like rendering code blocks, etc.

idk about that -- I think doing something like stripIndent from https://www.npmjs.com/package/common-tags is straight-forward~ish, and does what everyone wants, while still allowing the use of invisible characters to sensibly make it in to the output.

but, if we want to bike shed, I think it'd be stellar to have an option to strip all non-single space invisible characters -- But!!! with how this proposal is laid out, I don't think we need to support that, because we can do:

import { stirpIndent } from 'common-tags';

const MyComponent = template(stripIndent`
  <p>
     hi
  </p>
`, {});

which, as long as we design the build-time transform to evaluate the first arg to template (somehow? I'm fuzzy on this -- anyone know how it'd work? could lead to security problems? (I can only think of eval)), stripIndent would make the template go from:

`
  <p>
    hi
  </p>
`  

to

`<p>
  hi
</p>
`  

@chancancode chancancode marked this pull request as ready for review April 18, 2022 00:23
@chancancode
Copy link
Member Author

chancancode commented Apr 18, 2022

I added a bunch more details, including the runtime opt-in, a restricted set of syntax that is guaranteed to be statically analyzable, and the question about whitespace handling. I marked it as ready for review/discussion now.

@NullVoxPopuli @nightire about the questions you asked (re: whitespace), maybe read the new section on that and we can continue from there. Short answer is no, I don't think we want to create an arbitrary macro ecosystem here and in any case, solutions like stripIndent doesn't work for <template>. I agree there are use cases where you care about whitespace, but the current/default rules doesn't really do anything for you (arguably actively making it worse) in those cases either. (It's important to discuss before we ship, but is probably anyway out-of-scope for this RFC.)

Copy link

@SolPier SolPier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of notes, nothing deep.
Happy to use this feature !
Syntax support in the editor is definitely needed.

Copy link
Contributor

@chriskrycho chriskrycho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a very strong 👍🏼 on the introduction of the target output, of updating the addon v2 spec to target it, and of the runtime template() import!

I am extremely confused (even after a few discussions with folks!) about what value the alternative tagged template literal template provides here compared to <template>, since in both cases it does nothing without a transformation. (I’m quite willing to be persuaded here, I just am not yet seeing the value there!)

text/0813-template-layering.md Outdated Show resolved Hide resolved
text/0813-template-layering.md Outdated Show resolved Hide resolved
text/0813-template-layering.md Outdated Show resolved Hide resolved
text/0813-template-layering.md Outdated Show resolved Hide resolved
text/0813-template-layering.md Show resolved Hide resolved
text/0813-template-layering.md Outdated Show resolved Hide resolved
Comment on lines +851 to +854
Because `template()` is a standard JavaScript API that has a "real" import
location, there is a natural place to document and describe its behavior in the
API docs. This should be the primary reference of the feature, and should also
where we document the idiomatic subset as well.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use that as a hook for describing the whole end-to-end bits in the API docs, including <template>, too, since as you note below it's the actual desugaring for that.

Comment on lines +938 to +945
However, even with that in mind, the default conclusion for the whitespace
handling in `<template>` tags is _still_ not useful, _especially_ when the
author is trying to preserve significant whitespace for something like code
samples. This is because `<template>` tags often appears in JavaScript
contexts that already has leading indentation (such as inside a class body), so
in practice, if we preserve the whitespace exactly as it appears inside the
`<template>` tag, it still would not do what the author was trying to
accomplish.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strongly agree, and in line with the discussions we've had offline, I think we should probably apply the HEREDOC/etc. rules: leading indentation up to the indentation of the line on which the opening <template> appears should be stripped.

text/0813-template-layering.md Show resolved Hide resolved
text/0813-template-layering.md Show resolved Hide resolved
@chancancode
Copy link
Member Author

chancancode commented Apr 29, 2022

About the tagged template string literal version, I think it's a useful feature to have that can stand on its own, but also fits into our bigger plan around the content-tags-in-JavaScript format.

But before that, I think we are in broad agreement that <template> is going to be the primary format. I think #779 (and the blog post) made good arguments about why that is necessary, and I believe this RFC made it pretty clear it's not about re-opening that conclusion. It remains the case that we will recommend <template> and use it throughout our teaching materials, etc, and when everything is implemented, it is what is going to give users the best experience, both because that's what we are going to focus on building tools for, but also because of the intrinsic difficulties of adding functionalities into existing JS language support.

However, even with a degraded experience, it is still useful to have a version of the feature using only legal JavaScript syntax. Broadly, I can think of two kind of use cases:

  1. Where you are able to run a full/normal ember build but have little ability to customize the editing experience
  2. Where you are uninterested in actually running the code but it is important that the code parses/highlights correctly

codesandbox.io is probably a good example of the first case. It allows running arbitrary build code in a Node/remote container environment, but the editor UI is implemented in the browser and offers little in the way of an editor customization/plug-in system. In this scenario, if you were to use the <template>/.gjs format, you will get no syntax highlight, and any other editor features that are normally available to JS code. On the other hand, using the "template backtick" version in this proposal, you will still not get syntax highlighting for the templates, and you may have to disable or ignore some unused variable warnings, but otherwise you will still be able to use the normal JS features.

We may or may not be able to work with the codesandbox maintainers to get this particular case resolved quickly, but the general point is that "tooling support" is a long tail, and I am confident that we will get the major ones ticked off pretty quickly and provide a superb experience in those environments, there will be a bunch of places where the support trails behind or where we just cannot support due to restrictions outside of our control.

As the <template> programming model become mainstream, it will become the main way Ember users learn about, think about and write components, and everything else will slowly fade into unfamiliar/"obsolete" territories over time, in the same way that there are probably plenty of Ember users that never wrote a "classic Ember class" anymore. It is important to that we offer a comparable programming model for these environments so users who operate in them are not forced to drop down to a completely different programming model, whether that means "the old way of doing things" or "a much lower-level way of doing things".

In my opinion, having this available as an option will also help with adoption. As editor support is still being worked on, some of these benefits apply today for editors that are still unsupported, and if some of your teammates aren't using the "mainstream" environments that could be a blocker to adoption as well. The "template backtick" version can help fill the gaps in the meantime, provides the same programming model (in terms of thinking about writing components) and is 100% codemod-able into the <template> syntax when desired.

For the second type of use cases, the RFC text alluded to a few possibilities, but mostly it amounts to syntax highlighting in blog posts, gists, that kind of stuff (so codepen actually isn't a good example of that).

Again I think these reasons are good and important enough that the feature can stand on its own, but also this layering approach fits into the broader "plan" (sketch) around content-tags-in-JS.

The general idea is that we write a generalized specification that enables things like <template> tags to work (in theory, it can be proposed as a language feature for JavaScript itself). In the generalized version of the feature is that:

  • <foo>...</foo> desugars into foo`...`
  • <foo bar="baz">...</foo> desugars into foo({ bar: "baz" })`...`
  • When inside a class body, it desugars into a static initializer block:
    class MyClass {
      <foo bar="baz">...</foo> // => static { foo({ bar: "baz" })`...` }
    }
    

...and that's it. It doesn't synthesize a default export, doesn't automatically add imports, etc.

The point of this is to specify a general-purpose syntactic transformation/desugaring for the feature into standard JavaScript, while not specifying what the syntax actually does. This allows us to write a shared tool for this "first-stage" transformation that can be used by anyone interested in the feature (say, adding supporting <style> tags in component classes), but then could use standard tools (Babel, etc) to implement the tag-specific logic/semantics (e.g. pulling out the styles into an external stylesheet file).

In terms of implementing the <template> feature, the ideal layering would be:

  1. Transform them into "template backtick"
  2. In Babel, transform "template backtick" into idiomatic template() with the scope closure
  3. In Babel, transform idiomatic template() calls into the low-level primitives

We don't necessarily have to immediately go out of our way to rewrite code that already works, but that conceptual layering/desugaring should work.

mainstream editors, this will likely remain the case for a subset of less
used editors that we did not or could not prioritize supporting.

2. There may be editors and environments that are impossible for us to support,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not unique to this RFC but applies to Ember in general. As such, I am not sure it's relevant to include as a supporting reason.

@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented May 2, 2022

to ensure we never did anything like what this RFC is proposing.

how do you mean?

this RFC is essential for a lot of reasons.
And is totally orthogonal to first-class-component-templates

in a modern ember world, dynamic template stuff is super annoying / nearly impossible with deep intimate knowledge of how glimmer is zipping up the already existing primitives.
(which this RFC does not propose deprecating)

@chriskrycho
Copy link
Contributor

@NullVoxPopuli your response here is part of why I had actually suggested breaking this into two separate RFCs: dynamic/client-side template compilation is indeed finicky, but it does not require the template tagged template literal; it only needs the template() import to which that compiles (and indeed the tagged template literal form is just as inert as <template> is without a Babel transform operating on it!). What @MelSumner is alluding is that it was a lot of work and a very narrow path we walked to consensus around <template>, which very carefully excluded tagged template literals as an authoring format.

This remains my concern as well: for all that the RFC indicates caution around messaging, I think it’s important that it go further—it’s totally fine by me if the tagged template literal template is a legal intermediate “desugaring” of the <template> tag (in view of generalizing the capability and making it viable for non-Ember users to adopt the same semantics!) which is then further compiled into our template() invocation. However, I think very strongly that we should not teach it as an alternative in any way: only as the desugaring.

I do not (yet) find the motivation offered here for the tagged template literal as even a secondary authoring format (which it is explicitly offered as here) to come close to warrant reopening that very, very difficult consensus thing.

Mind: this really comes down to “How Do We Teach This?” from my POV because, again, I’m on board with it as a well-rationalized desugaring as part of a multi-stage build pass. I think it makes an enormous difference in terms of the relationship to the existing consensus we worked hard to achieve whether or not we treat the tagged template literal form as something we should encourage users to author under any circumstances or not. I think we can probably have consensus on it quite easily as an intermediate compilation formation, and I am not sure if or whether we could get to consensus for it at all as even a secondary authoring format.

In other words, this—

In my opinion, having this available as an option will also help with adoption. As editor support is still being worked on, some of these benefits apply today for editors that are still unsupported, and if some of your teammates aren't using the "mainstream" environments that could be a blocker to adoption as well. The "template backtick" version can help fill the gaps in the meantime, provides the same programming model (in terms of thinking about writing components) and is 100% codemod-able into the <template> syntax when desired.

—is the crux of the issue. @chancancode's RFC proposes adding a secondary, but real, authoring format for first-class component templates, not simply an update to the compilation pipeline, and that is the part which is controversial.

@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented May 3, 2022

which very carefully excluded tagged template literals as an authoring format.
it’s totally fine by me if the tagged template literal template is a legal intermediate “desugaring”

Ye! To me, and what I'm most excited for, is that
this wrapper API (roughly, squinting) can be maintained by the framework
(it currently lives here: https://github.com/NullVoxPopuli/ember-repl/blob/main/addon/hbs.ts )

example implementation of `template`
import { precompileJSON } from '@glimmer/compiler';
import { getTemplateLocals } from '@glimmer/syntax';
import { setComponentTemplate } from '@ember/component';
import templateOnlyComponent from '@ember/component/template-only';
import { createTemplateFactory } from '@ember/template-factory';

export function template(template, scope, klass = templateOnlyComponent()) {
  return setComponentTemplate(compileTemplate(template, scope), klass);
}

function createTemplate(source: string, scope) {
  let locals = getTemplateLocals(source);
  let moduleName = '(dynamically compiled template)';
  let options = {
    strictMode: true,
    moduleName,
    locals,
    isProduction: false,
    meta: { moduleName },
  };

  // Copied from @glimmer/compiler/lib/compiler#precompile
  let [block, usedLocals] = precompileJSON(source, options);

  // some assertion here about locals missing from `scope`
  assert('...', ...);

  let blockJSON = JSON.stringify(block);
  let templateJSONObject = {
    id: moduleName,
    block: blockJSON,
    moduleName,
    scope,
    isStrictMode: true,
  };

  let factory = createTemplateFactory(templateJSONObject);

  return factory;
}

I think very strongly that we should not teach it as an alternative in any way

100% on board with this. template is solely a power-user tool (dynamic or compileless environment, etc)

but real, authoring format for first-class component templates, not simply an update to the compilation pipeline, and that is the part which is controversial.

I'm not 100% sure what's controversial, even after reading this -- but maybe I can try re-phrasing and we can see how close I am? (I like lists)

✔️ template() as dynamic tool
✔️ template() as an option to be an intermediary compile format
🚫 template() as a component authoring tool apps addons etc (static, exists on disk)
💡 template() is not a feature for the average user, and is mostly a power-user and tooling.. tool

is this close?

Copy link
Contributor

@snewcomer snewcomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

- It requires manually supplying the lexical scope variable bindings (the
"scope function").

- It uses a [static initializer block][static-block] to associate the template.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on reading the Class code snippets, the proposal is to use the static initializer block to side effect the association of the template? Or is there actual static initialization on the class itself? Perhaps inconsequential, but was hoping to enhance my understanding of this proposal.

However, that is not an ideal outcome. `<template>` is a user-facing feature,
and is poised to (or at least well-positioned to) become the main way Ember
users read, write and reason about components in the next edition of Ember when
the feature is fully rolled out.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<template> vs template(). What prevents the latter from wide adoption and use in the community? Do we just expect users to prefer the format in rfc 779?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The answer is: as currently proposed in the text of this RFC, nothing about it prevents that in any strong way. It would just be a matter of what is taught in the guides and what tooling does or does not support. We would expect those to be relatively strong nudges, to be sure—for example, I strongly suspect that everybody is going to want the goodies that Glint provides, and we won’t be supporting the tagged form. But I think it’s very likely that we’ll have a steady stream of requests for support for it if we do actively teach this somewhere.

@chriskrycho
Copy link
Contributor

@NullVoxPopuli

I'm not 100% sure what's controversial, even after reading this -- but maybe I can try re-phrasing and we can see how close I am? (I like lists)

✔️ template() as dynamic tool
✔️ template() as an option to be an intermediary compile format
🚫 template() as a component authoring tool apps addons etc (static, exists on disk)
💡 template() is not a feature for the average user, and is mostly a power-user and tooling.. tool

You actually captured fairly well the issue here, if only by accident! The trick is that there are two template() functions proposed by this RFC:

  • a template tagged template function, which is basically identical to the hbs tagged template function as implemented in the alternative proposal in ember-template-imports; this one does not take in scope and requires a further Babel transform: otherwise it does literally nothing (but it parses as valid JS)

  • a template() function which is the final compile target: it accepts a scope argument, wires up the relationship between the template and the host component (whether that's a class or a template-only component), etc.

For now, I’m going to refer to the pieces in play here as <template> (RFC 779), tagged (the template equivalent to hbs), and compileTarget (the template() which is the final compile target) to avoid everyone’s heads exploding from trying to parse too many different meanings of the word “template”.

Then the list actually looks like this, in terms of what I think we have consensus on:

✔️ <template> as a component authoring tool for apps and addons (static, exists on disk)
✔️ compileTarget() as component authoring tool for dynamic components, i.e. for client-side runtime compilation of templates
tagged as a component authoring tool for apps and addons (static, exists on disk)
🚫 compileTarget() as a component authoring tool for apps and addons (static, exists on disk)
✔️ tagged as an option to be an intermediary compile format
✔️ compileTarget() as an option to be an output compile format

As I understand it, @MelSumner's objection is to having the tagged form—marked in this list with ❓—included as an option for authoring at all, as the RFC explicitly calls for it to be (albeit as a secondary option for environments which don’t yet have support for <template>).

@NullVoxPopuli
Copy link
Contributor

ah, I get it now!

I would very much appreciate this RFC to focus on the compileTarget() + dynamic usage, and 100% agree with avoiding the tagged form (not just because tagged doesn't help me and my efforts in any way, but it seems like a whole can of worms, and focusing on just unifying

a template() function which is the final compile target: it accepts a scope argument, wires up the relationship between the template and the host component (whether that's a class or a template-only component), etc.

seems like an overall good thing.


Thanks for the clarifications! (and emoji list!)

@chancancode
Copy link
Member Author

chancancode commented May 6, 2022

I would put it a bit differently. I don't think "authoring format" and "primitives" are as real/concrete as a distinction than we try to make it sound.


By the standard we are using here, I think you could say the class {} syntax in JavaScript is the "authoring format" and the function-as-a-constructor-plus-prototype-manipulation paradigm is the "primitive". Likewise the getter/setter syntax can be called the "authoring format" and Object.defineProperty would be the "primitive".

You could technically say that there is nothing "preventing" the primitives from gaining wide adoption – it's not like you get a warning when you use the primitives and they didn't make you type Object.dangerouslyDefineProperty(). Technically it would also be correct to say that TC39 just "expected" users will prefer the nicer "authoring format". But I think most would agree that's a strange angle to analyze the situation.

In those examples, I think it would be wrong (at least overly strong) to say the primitives should never be recommended under any circumstances. It is true that you would generally prefer to use the high-level syntax, it feels better to use, and tools like TypeScript gives you better support. However, occasionally you find yourself in strange situations where the high-level features just didn't work for you. Maybe you are dynamically modifying a class, writing a decorator-esq thing, etc.

Perhaps you should reconsider whether you really should be approaching the problem that particular way, and sometimes that leads you to better alternative designs. But there are also less common but totally valid reasons for needing to do that. It's true that when you reach for the primitives, it tends to get a bit awkward, is likely to raise some eyebrows on code reviews, and you probably need to write a comment explaining yourself. But other than that, the primitives are totally "real" and have exactly the same semantics. If you have the need for them, you can reach for them, you can write the code, you can find the docs on MDN, you can choose to save it on disk, or not. All of the "can"s here doesn't mean "recommended", but it also doesn't mean "never ever recommended" or "we didn't mean to tell anyone about these".


We can also look at some examples from the Ember ecosystem.

In the Octane time frame, we shipped two features that changes the "authoring experience" for components – making template-only components have outer-HTML semantics (#278) and component template co-location (#481).

The intended design is for components to be a fairly lightweight concept in Octane. At the most basic form, all they are is a way to make reusable markup snippets. You can easily create one buy making a .hbs file in the components folder, copy some markup into it and you can now reuse it anywhere. Need to do something sightly different on each invocation? There are arguments and blocks for that purpose. Need to do more than that? Add a class when you need one. In this world, "template-only components" aren't special at all, they are just what a basic bare bones component that didn't happen to need a class (yet). It's a subtle but important paradigm shift and you can see this in action in the Octane tutorial and I think most would agree that felt great.

When we first proposed/shipped #278, template-only components are specified in terms of the user-facing feature – it is a bare .hbs template file on disk without a corresponding .js file at the expected filesystem location. The feature can be enabled by an addon (which became @ember/optional-features). The rest are all private implementation details.

This made sense, as that's how most users would interact with this feature on a day-to-day basis. However, over time, we realized that there are a small but real use cases or environments where these specifications didn't make sense or is not reliable/acceptable. For example, addon authors cannot generally count on the flag being enabled in consuming apps, the addon opt-in and filesystem specifications didn't necessarily make sense for Ember Twiddle and other exotic/non-Ember-CLI environments.

Therefore, when we did #481, which was primarily about the user-facing component template co-location feature, we also introduced the primitives at the same time, like setComponentTemplate() and templateOnlyComponent(). While the RFC was pretty clear that we didn't intend for users/app authors to use these APIs directly, the feature was specified in terms of these APIs, so that if you did find yourself in a situation where you need to replicate the same behavior, these primitive APIs would give you the same end result.

I think that divide held up reasonably well overtime. If you were to survey 100 random Ember users, most of them probably have never heard of or used these APIs. And yet, there just are use cases where they are useful and necessary (the <template> feature being one of them!), some of which we didn't even anticipate at the time.

Another thing that happened, TypeScript (Glint) users ended up writing templateOnlyComponent() on the regular, which I am personally not thrilled about. (This is not a dig at the amazing work by the TS/Glint team did to enable template type checking!) The API was really meant as a low-level tool and isn't really a sensible concept for app authors to have to learn about. The whole point of this aspect of the Octane design is that a "template-only" component isn't a distinct concept, it's just a regular component but without a class.

But at the end of the day, the confusion we were trying to prevent was pretty bounded and manageable. We would have preferred to present the thoughtful design to all of our users, but it's not like the programming model is a fragile house of cards that falls apart immediately if you look at it under the wrong light. Perhaps it would have been better if we worked with the TypeScript/Glint sooner to develop an alternative that preserved the intended design (which we are doing with <template> in Polaris), but it was also a far cry from table-flipping incoherence. Until then, it's on balance a good thing the workaround exists, even if it's sub-optimal, and that the TS/Glint could do what they needed to do without being blocked.

With all that said, I definitely do not regret including templateOnlyComponent() in the RFC. I am happy we didn't ship only the user-facing feature and leaving other use cases outside of the intended happy path in the dust. I am happy we didn't design the primitives so narrowly that they were only usable in the blessed use cases. I am happy we didn't make them artificially cumbersome to use just because they were intended as compilation targets and we can get away with making them extra verbose and awkward. (If we had done that, we would probably have just made TypeScript users type the very cumbersome thing on the regular and have changed nothing else!) I am happy that the TS team didn't have to propose another adjacent primitive that only worked for them, just to have @NullVoxPopuli come back to propose something else for their REPL thing. I am glad we documented these APIs and didn't intentionally make them hard to find. We chose not to teach them in the guides, because there were no need or them at the time (which is more or less by design), but I am happy that we didn't legislate into the RFC that they shall never be mentioned in the guides. 🙃

Another example from the Octane time frame was that we introduced the @tracked decorator. That worked great for the most part but there were situations where decorators just didn't work. Maybe you are in an environment that doesn't support them. Maybe you are not writing a class. Maybe you have an unbounded set of dynamic keys. And at the time, the status quo was that you either use @tracked or you can use the classic object model (Ember.get, Ember.set, computed properties, etc), which really sucked when you happen to not be on the happy path, which points to a "missing middle" layer of API.

So we did #669 to introduce the primitives, which we intentionally chose to do rather than simply solving the most common symptom of the problem by introducing tracked built-ins (which is a great set of APIs are on the way of being standardized into Polaris, but they couldn't solve all the problems/scenarios). With the benefit of hindsight, IMO it would have been better if they were rolled out at the same time with @tracked, but better late than never. There were technically nothing preventing the primitives from gaining wide adoption instead of the decorators in that case either, but we don't generally think that's something we need to worry about. In fact, the RFC went out of its way to show how the @tracked decorator could be implemented with the primitives – it even specifically recommended that we include that in the in-depth guides!


So back to this RFC. The intention here is to fill out the "missing middle" APIs for the <template> feature, and I am happy to categorize them as primitives. Some of those niches/needs came up here and there in the original RFC comment thread, some of them were left as work for the future (which I am picking up here), some were too niche/lacked enough interest to solve at the time.

I think the context were just pretty different. In the work leading up to #779, the focus was designing and standardizing the primary end-user facing feature. That made sense, and that's what matters the most at the end of the day. I am happy with the choices we made there, and it makes sense that we prioritize the most important day-to-day use cases and focus on how we could make that the best possible experience. Like @tracked, it's okay that it doesn't solve all the possible use cases, and it wouldn't make sense to accommodate niche use cases at the expense of a compromised experience for the day-to-day usages. But from the past experiences, these can be solved by composing the high-level features in well-designed layers of primitives, which is what I am trying to accomplish here.

In an edition, each individual feature is just a single puzzle piece that forms a bigger picture.

With Octane, there were a bunch of individual changes the the component programming model – outer-HTML template-only components, defaulting to template-only components in the generators, co-location, angle bracket invocations, splattributes, @namedArguments, @model, Glimmer Components, modifiers... Each of them have their own benefits and can stand on their own, but the most important thing is the overall paradigm shift – components are a lightweight abstraction for reusing markup, like functions but for HTML, with the ability to parametrize them with arguments and blocks, and the ability to abstract state and behavior using classes, etc.

In Polaris, <template> is certainly a very important (critical, even) feature, but there is a more important bigger picture paradigm shift we are trying to accomplish here – we want to move away from name-based resolutions, we want imports, we want to break down the barriers between JavaScript and templates (as in – "ease of refactoring between JS and templates", not "blur the line and make JS code and templates the same thing"). <template> is how most users would get those benefits (but it's not the only thing that matters, other features like default managers are part of that picture too), and it's important that we make that experience as polished as possible and get as many users onboard as possible.

But at the end of the day, what is even more important is to get everyone on board with the new components programming model, and we cannot do that with <template> being the only way to access that world. Just like @tracked, we must acknowledge that it has its limitations and there are situations where it is not going to work. And just like with @tracked, we can solve that by layering the APIs so that there will be something else to fallback to, whether they are used directly or for someone else to build something on top of those layers that is fitting for the unique situation we didn't account for.

And to be clear, I am not talking about presenting these as "alternative" as in "beef, chicken or vegetables". It's more like "you can get the binaries for this tool, or install it with cargo, or build it from source yourself". Or perhaps something closer to home: use native <button> or <select> tags if you could, because they do everything right for you, and if you think you couldn't, think again harder because there is probably some way to approach the problem differently, but otherwise, what they do for you can be described and broken down into the lower-level aria roles, aria attributes and other behavioral specifications so you can compose those to do the right thing in your use case.

With <template>, yes, it's already built on primitives, but those primitives represent the legacy worldview that doesn't really match the new Polaris paradigm (such as working with "raw templates" as a distinct concept from components), so it's worth replacing them with something that matches the <template> programming model.

Now, is it possible that someone would take the primitives and use it in ways "we don't like"? Sure? Does that mean it's a bad idea to expose the primitives? I don't think so. First of all, we should probably have less opinion on what experiments others choose to do in their free time and how people want to write their code 😛 But more importantly, if people are motivated enough to experiment/deviate from the happy path, they are going to do it anyway. By providing more strategically placed branching-off points/off-ramps, we actually get to share more and keeping everyone closer together and headed in the same overall direction. If the only available branching-off point is all the way down at the bottom and they have to rebuild a lot of the same things and remake a bunch of the decisions we already made, that's when you end up getting people veering way off course.

Case in point, whether you personally prefer it or not, projects like Emblem exists and some people like to use them. We don't have to go out of our way to support them, but where possible we should work towards layering the work so that it is possible for someone who is motivated enough to make it work can leverage a lot of the work we put into making <template> work (hand wave: something something transforming into the hbs before the rest of the pipeline "sees" the templates). Otherwise, they may end up deciding it's not worth the effort and decide to stick with standalone template files plus some kind of {{import}} helpers. And I think that would be a worse situation overall. (Not claiming this RFC solves the Emblem problem, though along with the content-tag proposal I think it gets us much closer.)


So, specifically about the tagged template literal part of the layering. I am happy to call it an intermediate format.

I think there are use cases for it where that makes sense, and honestly I think I made a pretty good case for it with the codesandbox scenario. There just are places where running a build is not an issue but the editor experience is the bottleneck. We can agree that this is not a super common situation and that this is not a massively popular/important use case, and that's a good thing! Otherwise, we will be in even deeper trouble! But that's not really the point and not the bar we need to clear for these middle/primitives APIs. We landed templateOnlyComponent() originally just because we needed something for addon authors, and the other use cases were mostly discovered organically and that's totally fine.

I also stipulated that some users (because of their editor preferences, or because they want to be an early adopter) may want to adopt the programming model but is constrained by the editing experience. I think that's just a special case of the same situation that is perhaps more concrete and easily to relate to, but now the concern is "woah, now that's too useful and uncanny, it solves that problem almost too well and people may get comfortable with it and not want to switch", so that kind of feels like a catch 22 situation in terms of justifying whether this feature is actually useful or not. 🙃

As far as why isn't just having template() good enough, I think it's the same reason why just having precompileTemplate() isn't good enough. The factor here being the scope closure. It is cumbersome and hard to get right. In situations where you can't avoid it, it's better than nothing, but when possible, we should prefer to steer people (even "power users") away from having to write it, either by hand or by re-implementing the logic to generate one. While the high-level of "capture any referenced lexical variables" is easy to say, we still have unresolved semantics questions around the finer details of how exactly this works (which we should resolve asap!), and it would be unfortunate if those details need to be widely understood.

Even if we ignore the writing-by-hand cases, it's still good for us to layer our tooling that way so that a third-party babel transform (say, something to do with Emblem or some other experimentation that requires splicing in components) can be inserted before ours and emit the backtick code and let our plugin handle generating the correct scope closure, rather than having to target the template() directly and have to re-implement the logic.

I have more thoughts but this is getting long and it's getting late. The bottomline is, I agree this is probably mostly a "How Do We Teach This?" issue, but I don't think it's as simple as what words we do or do not say in the docs. I think "authoring format" vs "primitives" is kind of a red herring, in the real world they are not going to be so black and white. When we introduce APIs, of course we expect some people to actually use them. We can be clear about our intentions, provide good guidelines, clear documentation for their purpose, carrots, etc, but at the end of the day we need to trust those who are writing the code to pick the right tools for their situations given our guidance. We can't be everyone's code parents.

So, I think it makes more sense to focus on what future we are intending to build and make that clear – <template> is the user-facing high-level feature that we recommend to everyone and focus our resources on, while "template backtick" and template() are the de-sugaring/primitives that it is built on; it's not an either-or-depending-on-your-preference choice, it's not --pods (or maybe saying it is like pods is the better deference, take your pick! 😄); we are not going to have a toggle on the guides to switch between the two; we are probably not going to have a generator flag that you can save to .ember-cli. I think we can focus on the making <template> a good experience for most and the core Ember community has a pretty good track record of sticking with the happy path that we recommend (sometimes maybe too much! see: the "packages in the blueprint" issue), and I don't think we need to (or is appropriate for us to) police what other alternatives/experimental things people want to do.

Happy to have the conversations at the meeting tomorrow and I am sure that would change things too, so I'll leave the rest for later.

@habdelra
Copy link

habdelra commented May 6, 2022

I'm really excited about this RFC. I'm working on a project that where one of our steps is to compile JS using babel at runtime. Our js includes glimmer templates. One of the challenges that we are running into is that the various babel plugins for class property and decorator support can alter a simple class into something that is almost unrecognizable. For instance if you have a class that looks like:

export class Person extends Card {
  @field firstName = contains(StringCard);
  @field lastName = contains(StringCard);
  static embedded = class Embedded extends Component<typeof this> {
    <template>
      <@fields.firstName/> <@fields.lastName/>
    </template>
  }
}

it will result in this after a babel compile with the standard presets (where we use the 'ember-template-imports/lib/preprocess-embedded-templates' to preprocess the <template> tags):

.
.
export let Person = (_class = (_class2 = class Person extends Card {
  constructor(...args) {
    super(...args);

    _initializerDefineProperty(this, "firstName", _descriptor, this);

    _initializerDefineProperty(this, "lastName", _descriptor2, this);
  }

}, _defineProperty(_class2, "embedded", (_GLIMMER_TEMPLATE = __GLIMMER_TEMPLATE(`
      <@fields.firstName/> <@fields.lastName/>
    `, {
  strictMode: true
}), class Embedded extends Component {
  constructor(...args) {
    super(...args);

    _defineProperty(this, _GLIMMER_TEMPLATE, void 0);
  }

})),
.
.

sadly that moves the __GLIMMER_TEMPLATE tags into syntax that is not supported by ember-template-imports/src/babel-plugin, The ember-template-imports plugin can only (understandably) support a few different syntaxes:

/**
 * Supports the following syntaxes:
 *
 * const Foo = [GLIMMER_TEMPLATE('hello')];
 *
 * export const Foo = [GLIMMER_TEMPLATE('hello')];
 *
 * export default [GLIMMER_TEMPLATE('hello')];
 *
 * class Foo {
 *   [GLIMMER_TEMPLATE('hello')];
 * }
 */

I think providing a template() function with a runtime meaning would be really helpful to these sorts of scenarios where we need to interact with compilers, like babel, where it may perform these complex but valid transformations on the underlying javascript.

I could see ember-template-imports/lib/preprocess-embedded-templates emitting the new template() function instead of the __GLIMMER_TEMPLATE tags which would allow for the consumers of that output to have more flexibility with how those templates are handled (specifically if they go thru a babel transform like in our case).

@wagenet
Copy link
Member

wagenet commented Jul 25, 2022

I'd like to try to catch up on this, but maybe someone who has been following along can summarize where this stands.

@chriskrycho chriskrycho added T-templates T-framework RFCs that impact the ember.js library T-Tooling RFCs that impact tooling, like Ember Inspector T-TypeScript labels Jul 26, 2022
@wagenet wagenet added the S-Proposed In the Proposed Stage label Dec 2, 2022
@wagenet wagenet added S-Exploring In the Exploring RFC Stage and removed S-Proposed In the Proposed Stage labels Jan 27, 2023
@ef4
Copy link
Contributor

ef4 commented Jan 27, 2023

My understanding here is that when @gitKrystan and @wycats did the low-level integration work for ESLint et al their finding was that having a JS representation like this was less useful than standardizing an AST-based representation. If that's correct, we may want to replace this RFC with one that standardizes the AST representation of <template>.

@gitKrystan
Copy link
Contributor

gitKrystan commented Jan 30, 2023

My understanding here is that when @gitKrystan and @wycats did the low-level integration work for ESLint et al their finding was that having a JS representation like this was less useful than standardizing an AST-based representation. If that's correct, we may want to replace this RFC with one that standardizes the AST representation of <template>.

Here are some issues we've been having that might be alleviated with a standardized AST representation:

ember-tooling/prettier-plugin-ember-template-tag#43
ember-cli/eslint-plugin-ember#1659
ember-cli/eslint-plugin-ember#1750
ember-tooling/prettier-plugin-ember-template-tag#1

@ef4
Copy link
Contributor

ef4 commented Feb 3, 2023

Discussion from core framework review meeting:

  • the more explicit form here is still desirable as the standard "vanilla" form that, for example, addons should publish to NPM. It's better than setComponentTemplate because setComponentTemplate leaks the concept of "template" separately from "component", which is not a thing we want to keep. It's also shorter and requires less imports.
  • whether or not this form is actually used in the implementation of tooling is not really the important part. Instead for tooling we probably want a library that emits an AST-based representation of the template tag.

@ef4
Copy link
Contributor

ef4 commented Jun 23, 2023

This was superseded by #931 which is advancing.

@ef4 ef4 added the FCP to close The core-team that owns this RFC has moved that this RFC be closed. label Jun 23, 2023
@ef4 ef4 closed this Jun 30, 2023
chancancode added a commit that referenced this pull request Oct 24, 2023
This brings back some details about runtime template compilation
that got lost from #813
chancancode added a commit that referenced this pull request Oct 24, 2023
This brings back some details about runtime template compilation
that got lost from #813
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
E-Polaris Work for the Polaris Edition FCP to close The core-team that owns this RFC has moved that this RFC be closed. S-Exploring In the Exploring RFC Stage T-framework RFCs that impact the ember.js library T-templates T-Tooling RFCs that impact tooling, like Ember Inspector T-TypeScript
Projects
None yet
Development

Successfully merging this pull request may close these issues.