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
from

Conversation

@tomdale
Copy link
Member

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Member Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

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).

@amyrlam amyrlam referenced this pull request Feb 25, 2019

Merged

Ember Times No. 87 - March 1, 2019 #3837

12 of 15 tasks complete
@kovalchik
Copy link

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

This comment has been minimized.

Copy link

chriskrycho commented Feb 25, 2019

The updates made here address my concerns. 👍 👏

tomdale added some commits Feb 26, 2019

@MelSumner MelSumner referenced this pull request Feb 27, 2019

Open

Octane Tracking Issue #17234

78 of 128 tasks complete
</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?

@MelSumner

This comment has been minimized.

Copy link
Contributor

MelSumner commented Mar 3, 2019

@tomdale

but Ember's values mean we eventually need a recommended default.

What would prevent us from starting out with a recommended default?


In Ember components today, JavaScript code lives in a `.js` file and template
code lives in a separate `.hbs` file. Juggling between these two files adds
friction to the developer experience.

This comment has been minimized.

@MelSumner

MelSumner Mar 3, 2019

Contributor

Juggling between these two files adds friction to the developer experience.

Can we word this to be more relative? It is not true for everyone.

This comment has been minimized.

@sukima

sukima Mar 3, 2019

I usually split my editor (Vim) window to show the JS on the top and HBS on the bottom. Or a hot key (CTRL+^) that flips between the template to the JS. Because of this I don't experience this Juggling problem. Is it possible that the tools a developer uses contributes more to the problem then the file structure itself?

This comment has been minimized.

@sukima

sukima Mar 3, 2019

If both the JS and HBS were to be in one file I'd have to split the file anyway and it would make the hot swap more difficult as I'd have to mark two parts of a file and jump between them. Not only that but having two syntaxs and linting rules and compilers in one file is a massive undertaking in tooling and comprehension. I like that I can see a .js file and know it is JavaScript. I don't have to worry it is a Frankenstein experiment of non-JavaScript. I can ember-template-lint *.hbs and not have to also conciser it needs to be integrated into eslint. Doing any kind of find or git grep call-site -- *.hbs would compound the problem. I could go on but I hope my point is made. JSX is a fanatical nightmare in maintenance, usability, and comprehension in my experience. It may look nice for a really small module but I'd ask since when have all your components been that small? Real world apps are huge and complicated and we as devs need all the help we can get making sense of it all--computer tools too.

I would be curious if this same discussion happened with C files and Header files?

This comment has been minimized.

@chriskrycho

chriskrycho Mar 4, 2019

I think @MelSumner's note on this is probably the most apt: developer experiences around this are mixed. I have lots of friends who simply don't experience the kind of "nightmare in maintenance, usability, and comprehension" that @sukima does – and I absolutely believe both reports.

These kinds of things tend to be very specific to individual tastes (in much the same way that many people love each of Python and Ruby but very few people love both), and they are also profoundly impacted by the patterns used in specific apps and teams.

I also think it's worth remembering that the point here is to unlock different approaches, which may allow us to find something that works better than today's story or the alternatives currently used in other frameworks. Regardless, Ember will certainly continue to have a good default.

By way of analogy: we have first-tier supported experience with Ember Data—but don't in the least discourage people from using Apollo.

@chriskrycho

This comment has been minimized.

Copy link

chriskrycho commented Mar 4, 2019

@MelSumner I'd suggest we have a recommended default already: the current JS + HBS files situation. By the same token, we don't yet need a recommended default for SFC-style approaches, because the current default will continue to work well unless or until we find something generally recognized to be better.

@luxferresum

This comment has been minimized.

Copy link

luxferresum commented Mar 10, 2019

After exploring the component managers I've got curious why this API is not added to the component managers but completely separate? Wouldn't it be nicer to ask the component manager for the template? We currently ask the component manager for the this context, why not also ask it for the template (some kind of embeddable template) and the scope?

```

As of this RFC, scope values resolved via ambient scope are limited to
component classes and template helpers (i.e. subclasses of `Helper` or

This comment has been minimized.

@luxferresum

luxferresum Mar 10, 2019

did we miss modifiers here? 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.