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

SFC & Template Import Primitives #454

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Conversation

@tomdale
Copy link
Member

@tomdale tomdale commented Feb 23, 2019

Summary

Expose low-level primitives for associating templates with component classes and customizing a template's ambient scope.

These primitives unlock experimentation, allowing addons to provide highly-requested features (such as single-file components and template imports) via stable, public API.

Rendered

@tomdale tomdale self-assigned this Feb 23, 2019
@lifeart
Copy link

@lifeart lifeart commented Feb 23, 2019

Regarding conversation about hot-module replacement. Is it possible to reassign component's template?
Kinda hey, component manager, use this layout for this component, instead of early assigned

@kellyselden
Copy link
Member

@kellyselden kellyselden commented Feb 23, 2019

One of the big selling points of Ember is being able to jump into a random project and know where everything is and how everything works because of the common conventions.

Would you consider this a drawback of the proposal, potentially encouraging drastically different component experiments?


The order of the array is unimportant, other than that the same order must be
preserved between the identifiers passed to `precompile` and the
corresponding values passed to `createTemplateFactory`.

This comment has been minimized.

@gossi

gossi Feb 23, 2019

This reads super risky to me. The order at two "independent" locations must be the same. Couldn't it be mapped by name? E.g.

const template = createTemplateFactory(json, {
  scope: () => {User, t}
});

PS. I came to the alternatives section later on ;) Would the createTemplateFactory() be able to create an array from the returned object, e.g. does it have access to the array for scope that is coming for the template from precompile()?

This comment has been minimized.

@pzuraq

pzuraq Feb 23, 2019
Contributor

I don't think it matters later on either way, once the scope is correctly associated, either by name or order, you should be able to pull the values off the object/array and use them in a minifiable way. The issue is with the createTemplateFactory() in the first place - creating that object and assigning the keys must be shipped to the client, and that part cannot be minified.

If there are a fair number of imports per file, say 5, this could add up in large apps. It's not a massive amount, yes, and if this were an API expected to be used by your average user I would totally agree, but the idea here is that this will be compiled output from a preprocessor, so it's much less likely that it'll get messed up IMO

@gossi
Copy link

@gossi gossi commented Feb 23, 2019

I like the experimentational idea of this RFC. Like "hey community, here is your toolbelt, now let's see what SFC you'll come up with and what might be the final spec we can compromise and align on".
However, I also do see that there will be plenty of SFC specs out there and ember can face a wild-wild-west situation here, which can end up problematic 😈. Each time creating a new component, you'll need to watch out for the correct addon to match your SFC-style.

I dunno if left out, but template, script and what about styles? Think about css modules or blocks. Do they need an API, too?

@ef4
Copy link
Contributor

@ef4 ef4 commented Feb 23, 2019

I would like us tp strongly consider keeping the template part as HBS within this new standardized format. Not precompiling it to the unstable wire format.

The benefit would be that addons can apply whatever SFC-to-standard transformations they want at their own publication time. That is not possible with the current proposal because the template wire format is tied to one exact ember version.

This is the pattern we are already working toward in embroider, in that I want addons to publish HBS to NPM, but with any custom preprocessors (including AST transforms) pre-applied. This greatly simplifies the final app build because all templates can share one standardized configuration, rather than setting up special preprocessors per addon.

@luxferresum
Copy link

@luxferresum luxferresum commented Feb 23, 2019

When doing precompile with a scope, how could I provide curly components? Would

precompile(`{{my-component}}`, {
  scope: ['my-component']  
});

work as expected? Shouldn't we specify components and helpers in separate arrays? And if I want a component to be available for curly and angle-bracket style, do I need to specify it twice?

@chriskrycho
Copy link
Contributor

@chriskrycho chriskrycho commented Feb 23, 2019

General read so far: 👍 on the overall direction, but I have serious (if as of yet somewhat inchoate) concerns about the precompile and scope API. The rationale in Alternatives makes sense, but don't overcome my hesitations. I'm going to mull over the rest of the weekend and early next week and try to make those concerns concrete and potentially outline some alternatives.

This combination of things is what makes me 😬:

  • the bag-o'-names definition for scope (which I don't see any good way to write types for – I'm sure we could come up with something, but it won't be pretty)
  • the corresponding need for a bunch of runtime checks (with errors thrown for them)
  • the drawbacks noted a future where we get Glimmer AOT compilation, which is closely related to the runtime semantics of the previous two points

All of those point to the need for a more statically- than dynamically-checked API here. (IMO they point very strongly to that, but I'm definitely biased toward static/build-time checks over runtime checks.)

More to come—hopefully with some API ideas—after I've had some further time to think on this.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Feb 23, 2019

@kellyselden totally agree that the conventional file layout is a huge benefit to us (once you learn it)! I think we definitely should keep the resolution rules for things like routes, controllers, and services long term. These are the backbone of an app's structure IMO.

However, for components and helper functions I feel like we've been struggling against this system for quite some time. Some components are meant to be reused everywhere, and some are meant for specific use cases, but all components get put in the same place, and the lack of granularity and the ability to organize and sort these different types of components can get overwhelming quickly!

This is why "local lookup" became a thing in MU, it was to allow some more flexibility. But the rules for local lookup meant:

  1. Things are no longer simple, you can't just look at the components folder, you need to look at several different folders to find the thing you're looking for.
  2. You now have a large amount of implicit state about the app to reason about what a particular thing resolves to, especially for private collections and name collisions.

I think explicit imports solves these problems a bit more nicely, it gives each app more flexibility for organization, and it means you can easily figure out where a given value is coming from very quickly. The downside is app file structures could become less conventional, as you pointed out, but I think a better way to approach that would be with linting rules, which allow us to guide the ecosystem toward a convention, without forcing every app to follow it if it doesn't meet their needs.

@tomdale
Copy link
Member Author

@tomdale tomdale commented Feb 23, 2019

@lifeart:

Regarding conversation about hot-module replacement. Is it possible to reassign component's template?
Kinda hey, component manager, use this layout for this component, instead of early assigned

No, this is not supported at this time. Making templates static allows for the template compiler to emit faster bytecode that does not need to insert guards around every component invocation that checks if the template has changed. It also means component invocations can be compiled to bytecode that jumps directly to a fixed offset in the heap, rather than having to first look up the offset in the heap.

To support hot module reloading, a follow up RFC could specify that dynamic template replacement is supported in development mode only, by putting the VM in a less optimized mode.

@kellyselden:

One of the big selling points of Ember is being able to jump into a random project and know where everything is and how everything works because of the common conventions.

Would you consider this a drawback of the proposal, potentially encouraging drastically different component experiments?

Yes, I tried to capture this in the Drawbacks section.

In reality, the framework core team has been discussing template imports and SFC formats for some time, and we are interested in exploring those designs via an "official" addon. The benefit of stabilizing the primitives first is that, like with Sparkles/Glimmer components, we can iterate quickly on an "official" addon outside of the core framework, and that addon will be compatible with a wide range of Ember versions. The primitives also make it possible for the entire community to explore the solution space in parallel and contribute back good ideas.

But, yes, as with component manager and Glimmer components, having the ability to experiment and extend is important, but Ember's values mean we eventually need a recommended default.

@gossi

I like the experimentational idea of this RFC. Like "hey community, here is your toolbelt, now let's see what SFC you'll come up with and what might be the final spec we can compromise and align on".
However, I also do see that there will be plenty of SFC specs out there and ember can face a wild-wild-west situation here, which can end up problematic 😈. Each time creating a new component, you'll need to watch out for the correct addon to match your SFC-style.

Agreed, see above reply.

I dunno if left out, but template, script and what about styles? Think about css modules or blocks. Do they need an API, too?

We've thought about this a bit, but considered it out of scope for this RFC. The key insight here is that it's important for the template to inherit its lexical scope from JavaScript, so the API has been designed around making that case possible. It's not obvious to me whether other kinds of files, like CSS, benefit from being in the same scope as JavaScript, and if that is even a coherent thing to express given the differences between the two languages. We are definitely interested in exploring ways to make CSS more productive in Ember in the future.

@ef4:

I would like us tp strongly consider keeping the template part as HBS within this new standardized format. Not precompiling it to the unstable wire format.

+1, will update the RFC to support this constraint.

@luxferresum:

When doing precompile with a scope, how could I provide curly components? … Shouldn't we specify components and helpers in separate arrays? And if I want a component to be available for curly and angle-bracket style, do I need to specify it twice?

Names in the template's ambient scope must be valid JavaScript identifiers, so names with dashes are not supported. That said, explicit imports means that the previous naming constraints are no longer in place, so you can invoke components with curlies like this:

import Component from '@glimmer/component';
import OtherComponent from './OtherComponent';

export default class extends Component {
  <template>
    {{OtherComponent foo=bar}}
  </template>
}

That said, it is recommended that invocations of components be changed from curly to angle bracket syntax, simply for the visual disambiguation between components and helpers that it provides.

Components and helpers don't need to be provided separately because we can determine their type at runtime from the value provided. So long as it is given a valid JavaScript identifier, an imported component can be invoked via {{OtherComponent}} and <OtherComponent /> in the same template just fine and does not need to be specified twice.

@luxferresum
Copy link

@luxferresum luxferresum commented Feb 23, 2019

so names with dashes are not supported.
so no helpers with dashes? What is the reason for this limitation?


And shouldn't be precompile gets exported in ember-source so we can import it during build from node?

@jenweber
Copy link
Contributor

@jenweber jenweber commented Feb 23, 2019

Is this content important enough that some subset of the explanations belong in the "advanced" section of the Ember CLI guides? There's a long way to go to fill in more basic info for addon authors within those guides, but perhaps a callout to the APIs would be appropriate. What do you think?

@paxer
Copy link

@paxer paxer commented Feb 24, 2019

What I really like in Ember is a clear separation of templates (HTML, hbs) and JavaScript, this concept allows our designers to work with the templates utilisiIg their HTML and css knowledge without involvement JavaScript engineers most of the time. Our engineering team do not write HTML/css at all, it is all up to designers. So I am not a big fan of the concept which React use, it is a real issue for us because designers don’t want to mess with JSX. VueJS single file component looks a lot better in terms of clear separation between templates and JS, however I think Ember conventions solve the problem where to find a template and with the new UI file structure I think it will be even easier since both as I remember will be in the same folder. So I hope if Ember still go single file component way - it will be more Vue-like and not React.

@sukima
Copy link

@sukima sukima commented Feb 24, 2019

What I really like in Ember is a clear separation of templates (HTML, hbs) and JavaScript, this concept allows our designers to work with the templates utilisiIg their HTML and css knowledge without involvement JavaScript engineers most of the time.

As much as I personally agreed with this. The devil’s advocate is that the separation is the core reason Ember gets so much apathy in the larger JS communities. The common theme I see when asking React developers and React shops is the illusion that cowboy coding and unstructured spaghetti code is supirior in most cases. Many devs seem to prefer JSX or single component files in Vue.

Again as much as I agree with Ember’s conventions I worry about how competitive it is in a market which glorifies low level code. Could exposing more flexibility in lower level primitives offer more interest to the larger JS communities?

@davewasmer
Copy link
Contributor

@davewasmer davewasmer commented Feb 25, 2019

@sukima @paxer to your concerns: I think it's important to keep in mind that this RFC is merely proposing that we unlock the possibility to experiment. Nothing here says that single-file components are necessarily the way of the future for Ember. All this does is give us a chance to iterate and try to discover potential solutions (including ones that might continue to allow designers to jump into the templates in some fashion).

Copy link

@kovalchik kovalchik left a comment

I seem to have written a lot, sorry about the length. I like the idea of opening this up to addon experimentation; there have been a lot of proposed solutions to this problem and community involvement has always been a good tactic for these difficult problems.

}
}
</script>
```

This comment has been minimized.

@kovalchik

kovalchik Feb 25, 2019

Is it worth also calling out how Angular utilizes template literals for inline templates?

    import { Component } from '@angular/core';
     
    @Component({
      selector: 'app-root',
      template: `
        <h1>Hello {{name}}</h1>
      `
    })
    export class AppComponent {
      name = 'World';
    }

This comment has been minimized.

@MelSumner

MelSumner Mar 10, 2019
Contributor

only if we're using it as an example of something we'd never do...In all seriousness though, It may be better to leave it out.

@service session;
}
</script>
```

This comment has been minimized.

@kovalchik

kovalchik Feb 25, 2019

I think it might be worth adding an example which shows how a syntax could facilitate both inline and separate files. Although this RFC is only intended to facilitate experimentation, separation of concerns has long been a tenant of Ember's and is an important use case for many users. An example showing how such an API could unlock easy inline templates, while not excluding dedicated template files, would probably go a long way to making people feel more comfortable about exploring this direction.

For example (this is completely contrived, but hopefully it gets the point across):

my-component/component.js

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import titleize from './helpers/titleize';
import BlogPost from './components/blog-post';

export default class MyComponent extends Component {
  @service session;

  template() {
    return `
      {{#let this.session.currentUser as |user|}}
        <BlogPost @title={{titleize @model.title}} @body={{@model.body}} @author={{user}} />
      {{/let}}
    `;
  };
}

Could also be written as:

my-component/template.hbs

{{#let this.session.currentUser as |user|}}
  <BlogPost @title={{titleize @model.title}} @body={{@model.body}} @author={{user}} />
{{/let}}

my-component/component.js

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import titleize from './helpers/titleize';
import BlogPost from './components/blog-post';
import Template from './template.hbs';

export default class MyComponent extends Component {
  @service session;
  template() { return Template };
}

I'm aware there's some problems with the second example, such as BlogPost and titleize not being utilized within the component directly, problems which this RFC aims to help address by opening up experimentation. However, I think it would be helpful to demonstrate that such experimentation doesn't necessarily lead us to a place where we can't support both inline and separate file templates.

This comment has been minimized.

@pzuraq

pzuraq Feb 25, 2019
Contributor

FWIW I believe that these would be the corresponding examples for the variations. I do agree having them be available in this RFC would be nice, since this is also about how to move forward with importing in non-SFC components in general

Frontmatter

my-component/template.hbs

---
import titleize from './helpers/titleize';
import BlogPost from './components/blog-post';
---

{{#let this.session.currentUser as |user|}}
  <BlogPost @title={{titleize @model.title}} @body={{@model.body}} @author={{user}} />
{{/let}}

my-component/component.js

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service session;
}

Inline

my-component/template.js

import titleize from './helpers/titleize';
import BlogPost from './components/blog-post';

<template>
  {{#let this.session.currentUser as |user|}}
    <BlogPost @title={{titleize @model.title}} @body={{@model.body}} @author={{user}} />
  {{/let}}
</template>

my-component/component.js

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service session;
}

Vue Style

my-component/template.hbs

<script type="module">
  import titleize from './helpers/titleize';
  import BlogPost from './components/blog-post';
</script>
<template>
  {{#let this.session.currentUser as |user|}}
    <BlogPost @title={{titleize @model.title}} @body={{@model.body}} @author={{user}} />
  {{/let}}
</template>

my-component/component.js

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class MyComponent extends Component {
  @service session;
}

This comment has been minimized.

@kovalchik

kovalchik Feb 25, 2019

Thanks for the better examples @pzuraq!

@chriskrycho
Copy link
Contributor

@chriskrycho chriskrycho commented Feb 25, 2019

The updates made here address my concerns. 👍 👏

@MelSumner MelSumner mentioned this pull request Feb 27, 2019
97 of 142 tasks
</script>
```

Neither of these approaches is exactly right for Ember. Ideally, we'd find a

This comment has been minimized.

@joefraley

joefraley Feb 28, 2019

Why are neither of these exactly right for Ember? Do you mean that neither of them is exactly aligned with your ideal solution in a general sense? Or do you mean that there is something specifically different about Ember that renders the Vue and React strategies undesirable?

dynamically through Ember's container. This lookup process is somewhat
complex and not always as fast as we'd like.

Because the rules for how components and helpers are looked up are implicit

This comment has been minimized.

@joefraley

joefraley Feb 28, 2019

I find this ember's least attractive property. I'm glad it's called out explicitly here

const template$1 = templateFactory({
"id": "ANJ73B7b",
"block": "{\"statements\":[\"...\"]}",
"scope": () => [User, t]

This comment has been minimized.

@joefraley

joefraley Feb 28, 2019

are there memory implications of maintaining explicit references to these values? I don't know, I only mean this is explicitly different from ember's current architecture. consider a page with hundreds or thousands of the same component (like a table cell, for example). will that page have noticeably different memory constraints on this RFC than it does in ember today?

@joefraley
Copy link

@joefraley joefraley commented Jun 26, 2019

Neither of these approaches is exactly right for Ember. Ideally, we'd find a way to "bend the curve" of tradeoffs: maintaining the performance benefits of templates, while gaining productivity and learnability by having those templates seamlessly participate in JavaScript's scoping rules.

i would trade any degree of performance for any degree of productivity/transparency. i would pay 1000% performance slow down for a 1% improvement in legibility/ease of use. i have almost never been in a situation in my career where 2x or 5x performance was make-or-break, but almost every codebase i have ever worked in/organization i have ever been part of has been brutalized by onboarding/general productivity issues

@joefraley
Copy link

@joefraley joefraley commented Jun 26, 2019

Again as much as I agree with Ember’s conventions I worry about how competitive it is in a market which glorifies low level code. Could exposing more flexibility in lower level primitives offer more interest to the larger JS communities?

why does this matter? is it important for ember to be competitive in adoption with other approaches? i would naïvely think that the only important thing about a tool is whether it helps people accomplish their aims. if more people have other tools or other aims than there are people that find ember useful for their aims, does it matter? i appreciate that more popular tools have intrinsically broader and more active communities, which one can expect to furnish more and better solutions to a wider variety of problems (because numbers).

at the same time, i rarely see people talk this way about the design of elm-lang or cycle.js. the attitude of those communities seems largely to be "we'll make tools that solve our problems and we won't worry about the rest". is the community growth stuff the shortcoming with that attitude, or is it also something different? why should market adoption bear on technical guidance?

for example, ember could be great for a small number of very specific people. those people might not be people attracted to using react. should converting-react-users be part of technical design of ember?

fwiw i would not choose to use ember for some of the reasons alluded to here and others, but having explicit control over template scope would be helpful to me in making good use of ember where i must.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Jun 26, 2019

i would trade any degree of performance for any degree of productivity/transparency. i would pay 1000% performance slow down for a 1% improvement in legibility/ease of use

That may be true for some applications, but regressing in performance too much has actually impacted applications in the community (Discourse, for instance, couldn't upgrade to Ember 2 for quite some time because of the 1.13 performance regressions). Performance does become a concern for many applications as they grow, and templates are one area we can leverage a performance advantage as it stands, so it makes sense to continue to try to keep that advantage.

@joefraley
Copy link

@joefraley joefraley commented Jun 26, 2019

i would trade any degree of performance for any degree of productivity/transparency. i would pay 1000% performance slow down for a 1% improvement in legibility/ease of use

That may be true for some applications, but regressing in performance too much has actually impacted applications in the community (Discourse, for instance, couldn't upgrade to Ember 2 for quite some time because of the 1.13 performance regressions). Performance does become a concern for many applications as they grow, and templates are one area we can leverage a performance advantage as it stands, so it makes sense to continue to try to keep that advantage.

I think that is totally fair and valid. I don't mean to say that performance is never important or valuable to anyone. I only meant that I have never valued even large performance gains against even marginal productivity gains.

my fundamental point is: "it makes sense to continue to try to keep that advantage." is reasonable, and i gladly accept performance gains all things being equal. but i do not want to buy it if the prices are labeled in productivity. in the same way, i would not trade security or privacy for faster web experiences. i would rather wait 20s for the thing to load than 200ms at the expense of vulnerabilities.

i'm saying that i think balancing performance against productivity is a zero-sum losing game.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Jun 26, 2019

Totally get your point. I think the main thing @tomdale is pointing out here is that we can bend the curve here, because the most valuable benefits from a DX perspective, template imports and being able to use normal JS scoping rules, do not have a major impact on performance. We can do this and maintain all of our benefits from template syntax, without needing a custom re-export process like in Vue or other frameworks. This is the low hanging fruit.

Where it gets harder is if we wanted to go further down the path of JSX, and have a much more dynamic templating syntax that can interleave with JavaScript seamlessly. There are advantages to it, but there are disadvantages too, and it would be a much larger lift to get there even if we decided those advantages were worth it.

@sdhull
Copy link

@sdhull sdhull commented Sep 3, 2019

Is this the sort of thing (SFC) that could be enabled by an addon so we can experiment with it? Google surfaced this, but it's 2 years old 🤔

@MelSumner
Copy link
Contributor

@MelSumner MelSumner commented Sep 3, 2019

i would trade any degree of performance for any degree of productivity/transparency.

I don't think we should do things that put developers before end users.

@lifeart
Copy link

@lifeart lifeart commented Sep 3, 2019

This is how we have jsx support glimmerjs/glimmer-experimental#3

@ming-codes
Copy link

@ming-codes ming-codes commented Mar 5, 2020

JavaScript as template compilation target also paves way for typed template. My concern in this is that how are we going to embed type signatures when the template is compiled into byte code. Maybe a different wire format for non-production builds?

@Devoter
Copy link

@Devoter Devoter commented Mar 26, 2020

What do you think about this type of single-file components?

import Component from '@glimmer/component';
import { hbs } from 'ember-cli-htmlbars';

const layout = hbs`
<div>
  <span>Hello, Ember's world!</span>
</div>
`;

export default class MyComponent extends Component {
  constructor(layout = layout) {
    super(...arguments);
  }
}

Sure, layout property should be supported by @glimmer/component and the components manager.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Mar 26, 2020

@Devoter there are two issues with that exact design:

  1. The constructor parameters for Glimmer components are already defined and public API, so we can't change them as is

  2. It would be fairly misleading to users, since templates cannot be dynamic. They must be defined statically, ahead of time. Passing it via a constructor argument implies that it can change per-instance of the component.

@gabrielcsapo
Copy link

@gabrielcsapo gabrielcsapo commented Apr 7, 2020

Any movement on FCP? I think this is going to add a lot of value for developers and the community at large.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Apr 7, 2020

I believe this RFC is superceded by #496, which is under active development. The primitives provided by #496, along with the existing setComponentTemplate API, would provide the same functionality as this RFC proposes.

@Gaurav0
Copy link
Contributor

@Gaurav0 Gaurav0 commented Apr 8, 2020

Where it gets harder is if we wanted to go further down the path of JSX, and have a much more dynamic templating syntax that can interleave with JavaScript seamlessly. There are advantages to it, but there are disadvantages too, and it would be a much larger lift to get there even if we decided those advantages were worth it.

I would prefer we did not do this. It would further introduce the ability to do arbitrary non performant things with arbitrary side effects during render.

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Apr 8, 2020

It would not be possible to have the same level of dynamism as JSX with the Glimmer VM. It would require a massive rewrite of the rendering engine, and I don't think it's anywhere near in scope for this feature.

@sukima
Copy link

@sukima sukima commented Mar 16, 2021

I'm curious what the drive to merge languages are. Templates are a LISP language, why not lean on LISP for this and import as a procedure

{{import Foo from ""}}

I think one of the bigist reasons I really don't like JSX is that it mixes contexts. My JS mingles with my HTML in a weird-have-to-understand-the-implementation way.

Is it possible to have template imports inside the <template>-context-switching addition to the JavaScript spec?

@NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented Mar 16, 2021

{{import Foo from "…"}}

I'd like to see something like that implemented -- even if just for comparison. Implementation wise, I have a hunch it's going to be a similar type of transform as gjs 🤔

@pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Mar 16, 2021

Templates are a LISP language, why not lean on LISP for this and import as a procedure

This actually goes back a bit in the decision making process, all the way to Module Unification. We actually were planning on doing that, it just looked very different:

<some-addon-name::some-component>
</some-addon-name::some-component>

A few things changed after this syntax was accepted:

  1. We decided to make <AngleBracket/> invocation capital case. This was not fatal on its own, but it would mean there was an awkward mix between snake-case package names and CapitalCase component names, or that we would have to translate addon names to capital case as well. Then...
  2. NPM added named scopes to package names, making them much longer, and conceptually colliding with @ in args:

The result would have been something like

<@my-scope/some-addon-name::SomeComponent>
</@my-scope/some-addon-name::SomeComponent>

This was clearly not going to work, it was way too verbose overall. So, we went back to the drawing board.

The first draft actually was something similar to what you proposed with {{import Foo from "…"}}. However, there was immediately an issue: how do handle named imports?

{{import DefaultValue, ( Foo as bar, baz ) from ""}}

Every combination we came up with felt wrong in some way. Then, another point was made: The whole reason we were having to make this decision in the first place was because the wider ecosystem - in this case NPM - decided to make a fundamental change to the way that imports worked. It's is 100% possible that other changes could happen in the future that could also collide with this new custom import syntax. Changes such as additional import features, changes to build tools that would make it impossible for us to use a custom import syntax for bundling, etc.

This is why in the end we decided that we would use JavaScript import syntax for imports. In the end, developing a custom, parallel syntax would require a lot of work, and it would always have the potential to conflict with future changes in the wider ecosystem. This had already happened to us once, and we decided that we didn't want it to happen again.

I think one of the biggest reasons I really don't like JSX is that it mixes contexts. My JS mingles with my HTML in a weird-have-to-understand-the-implementation way.

FWIW, I empathize with this concern, as I actually feel this pain today with the current split between template files and JavaScript. For me, having to context switch constantly between two different files is really hard on my development flow, and I find it really hard to reason about what's going on.

I think everyone is a bit different here, and while I've definitely heard from plenty of folks who prefer the separation, I have also heard from just as many, if not more, people who would prefer to have the component and template in the same file. So I think whatever the standard/default setting is, we may not end up satisfying everyone.

What I keep coming back to is tests. Today, we write templates in tests, and I believe we want to keep doing that as a framework:

test('MyComponent works', function() {
  await render(hbs`
    <MyComponent/>
  `);
});

test('MyComponent works with args', function () {
  await render(hbs`
    <MyComponent @arg={{this.foo}}/>
  `);
});

In order for this work with template imports, we either need to repeat the imports in every single template:

test('MyComponent works', function() {
  await render(hbs`
    ---
    import MyComponent from './my-component';
    ---
  
    <MyComponent/>
  `);
});

test('MyComponent works with args', function () {
  await render(hbs`
    ---
    import MyComponent from './my-component';
    ---
   
    <MyComponent @arg={{this.foo}}/>
  `);
});

Or we need to say that the template has access to the outer JS scope:

import MyComponent from './my-component';

test('MyComponent works', function() {
  await render(hbs`
    <MyComponent/>
  `);
});

test('MyComponent works with args', function () {
  await render(hbs`
    <MyComponent @arg={{this.foo}}/>
  `);
});

Out of these two options, the later seems much better. It also means we can finally solve the problem of testing contexts with local tracked state, so you no longer need to refer to this in tests (which is a bit awkward), and you can add local helpers as well:

test('MyComponent works with args', () => {
  let state = new TrackedObject({ a: 1, b: 2 });

  let add = helper(([a, b]) => a + b);

  await render(hbs`
    <MyComponent @arg={{add state.a state.b}}/>
  `);

  state.a = 4;

  await settled();
});

These features would definitely take some getting used to, but to me this feels like it would really help with the expressiveness of tests and the ability to write great tests. We are actually the only framework besides React I think that writes templates in tests like this, so I think it's good to maximize our capabilities and leverage here 😄

So if we went that direction in tests, the question then becomes: why have two ways of doing the same thing? The learning curve of two things is always higher than one, as is the infrastructure to be built, etc.

So yeah, this is where I am currently. This is not the state of the core team as a whole, there is no consensus yet, but personally I find these capabilities really motivating. Having one unifying story for writing templates in tests and app-code seems like it would really reduce tooling cost and learning cost, and help us to accelerate our development and progress as a community. Definitely open to more discussion here, and proposals for alternative syntaxes and solutions, etc (feel free to PR them to ember-template-imports!).

@knownasilya
Copy link
Contributor

@knownasilya knownasilya commented Mar 16, 2021

Have written an app that used {{import ...}} syntax exclusively, I can whole heartedly agree on having one way to do imports, and the confusion of {{import ...}} in some of the situations mentioned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment