-
-
Notifications
You must be signed in to change notification settings - Fork 406
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
Glimmer Components #416
Glimmer Components #416
Conversation
This is fantastic. Nice write-up! I think that mentioning event handlers is warranted. While
I'm guessing that element modifiers are the way forward and I would be perfectly happy with that. Could you add something to the RFC clarifying this? Maybe just a section in the "How we teach this". Edit: I forgot to mention the existing |
Great points! I think we should definitely add a section mentioning the reasons why we are not including event handler hooks on the components, and we can also discuss the possibilities for solving those problems. Addressing them individually:
I do think outlining specific APIs is out of scope for this RFC, but we should definitely mention current alternatives for users who are used to classic components in "How we teach this", specifically use either |
@service time; | ||
|
||
// Use the values of args in an initializer | ||
fullName = `${this.args.firstName} ${this.args.lastName}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really neat. Just to clarify, this would not be tracked, right? So, if the args change, fullName
wouldn't update, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, for two reasons:
-
The property is not marked as tracked, so it will not trigger a rerender even if it is changed (unless you use
set
) -
The property is not a getter, so it doesn't know how to recalculate even if it knows it's dependent on the values of the args. In that sense, this would just be a convenience for initial setup.
API of a component, but this also has the potential for misuse and enables | ||
antipatterns. | ||
|
||
Glimmer components assign their arguments to the `args` property on their |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update this section to add the "new way" to author the classic syntax just above?
not make major changes without first getting community input, and will be | ||
considered part of the public API of Ember. | ||
|
||
## Prior Art |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps list react here as well?
Is there any concept of positional params in the angled/glimmer component world? <form onSubmit={{action foo}}>
<CustomInput @firstName> First Name </CustomInput>
<CustomInput @lastName > Last Name </CustomInput>
</form>
|
In the Angle Bracket Invocation RFC positional parameters were decided against for angle bracket syntax. I believe the reason this came to be was the difficulty parsing positional parameters vs. attributes, for instance: <option selected></option> It's difficult to tell if However, positional parameters are still legal in curly syntax and will continue to be, which was part of the reason for having both invocation styles. Glimmer components do not support positional parameters in the current proposal, and I believe the use cases are rare enough that a custom component manager (that does things like enforce that they are defined, and asserts against users using angle bracket invocation for them) would be a good alternative. This should definitely be mentioned in the RFC either way, thanks for bringing it up! |
Just to reiterate @pzuraq's point, we need to be very careful to not infer that the invocation style is related to the base class used (mostly because that was true in past revisions of these features)... |
What's the reason for having a new class for Glimmer components? Why not add these features incrementally to the existing components API?:
It seems like this would make it easier for existing users to move to this new approach to components, and easier for new users to not have two component classes to consider. (And Ember 4.0 can drop the deprecated the features, and what's left is Glimmer components.) |
@michaelrkn that's a great point! This has been considered over time but I think has been seen as undesirable and problematic for a number of reasons. We should definitely add a section addressing this in Alternatives, but to answer directly: It's still a single massive shiftThe biggest one to me is that we have such a large shift, that no matter what we effectively end up with two "classes". As you pointed out, outer HTML semantics, which are perhaps the biggest shift for Glimmer Components, would still require users to opt-in via a feature flag or per-component flag. Being able to transition one component at a time is important, so a per-component flag would definitely be necessary. This flag would essentially disable a whole bunch of hooks and properties (all of the event hooks, There is no middle state for most of these features. You can't start using outer HTML and then get rid of Typings and performanceTypescript is starting to become more commonly used in Ember apps, and even users who don't use it are able to benefit from typings. It's a very nice DX to be able to import the component class and immediately see what properties are part of its public API. This is possible today with Ember components, but there's a lot of noise (over 50 props and methods) so this makes it less helpful. Combine this with the outer-html feature flag, where you are effectively changing the public API based on a flag, and it becomes even less helpful. Additionally, Glimmer components having a much smaller API will be immediately better for performance, as will them not inheriting from Confusing conventionsPart of the issue with attempting to add these APIs incrementally is it may make it much less clear what the Ember Way is, for any given combination of APIs. For instance, what are best practices before you enable outer-html, but after you switch to using Doing this incrementally, feature by feature, necessarily increases the possible permutations of in-between states, and makes it harder to define conventions for each one of them. New users may get lost in the variety of features (and permutations of them) they need to learn, and existing users may be frustrated by the lack of clarity. InteroperabilityOne of the stated goals of Glimmer components in the RFC is to interoperate with Glimmer.js, and allow users to begin using components with both frameworks. On its own, this is definitely not a compelling enough reason to make the decision to have a new default component class - Ember should do what's best for Ember. However, with the other reasons pushing us toward a new base class anyways, it is a nice win that will enable more experimentation, and increase Ember's appeal to users who care about byte-size. Without a new base class, we definitely would not be able to do this for some time, since it would require removing all Ember specific parts of the component class (including extending from |
|
||
```ts | ||
interface GlimmerComponent<T = object> { | ||
args: T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always wanted to have something like React's PropTypes in Ember. More than that, I really wanted to be able to specify the attributes a component should receive with Typescript. By specifying the shape of the args
using Typescript, could we get typed arguments in Glimmer components?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the answer is partially. You definitely will be able to use TS to specify the shape of args when defining classes, and internally within the class these types will work. However, we don't currently have a way to integrate templates with the Typescript compiler, so there's no way for us to validate that the types are correct when a component is used in a template. Long term, I think this is the ideal way to have argument typings.
In the meantime, runtime validations like React's PropTypes are possible, but extracting them from TS would be tricky. There's been some discussion of this on Discord, but nothing concrete as of yet. Libaries like @ember-decorators/argument and ember-prop-types can continue to fill the gap, but I definitely think we can do more here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if understanding you correctly, this RFCs is does some the work to support typed args but it's not all the work that needs to be done to validate that a component is being passed the right arguments. In other words, this RFCs lets us specify the types a component should receive but we can't actually validate we're sending those types because the templates aren't integrated into the typescript compiler. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but this is a nice incremental step forward. It also allows the usage of this.args
inside the component to be properly typed which is a nice win too...
Yeah - you could even just copy your comment over!
Can't ES6 classes,
Why would all of the event hooks need to be disabled in order to use outer HTML? With
What is the performance impact? And isn't a major version release a better place to focus on removing APIs? |
Yes, the absolutely can. What I was referring to here was the set of features that are tied together by Outer HTML, so:
There may be some more I'm forgetting at the moment, but the point is that as soon as you toggle that switch, you have to rewrite all of the above to the new world. That's a fairly big switch, and really points to two separate class APIs.
I was specifically referring to the
It's hard to say without actual benchmarking, and it's hard to benchmark without rewriting a real world application 😄 We do have a minimal component API in the form of However, the fact is that we are doing much more work than is necessary right now, between the usage of
Absolutely! This is not a deprecation RFC, and classic components will remain part of the Ember API for the forseeable future. They still have functionality which is not available in Glimmer components (such as positional parameters), and they are very widespread in usage so it will take a long time to transition away from them. This was part of the reason for creating component managers in the first place, and the painstaking design that was done to ensure that all forms of component defined using them would interoperate at all levels - to ensure that transitions could be done incrementally, without removing or deprecating component APIs that are still in common use. |
@pzuraq Thanks for all the explanation. I'd still suggest that |
@michaelrkn that'd be a good thing to do in a deprecation RFC - for helping out with the transition and such |
|
||
Glimmer components align their argument access with named args by making | ||
arguments available exclusively on a (shallow) frozen object, `this.args`. | ||
Attempting to modify `this.args` will hard error, meaning that like templates, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for this question may be too small. What's
Attempting to modify
this.args
will hard error, ....
I guess you are trying to say something like 'will throw error' right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly!
Does this RFC for Glimmer Component prevent introducing single file components in the future? React pioneered this and since then, it's been picked up by Svelte and Vue.js (among others). Personally, I only use single file components with Ember and it's glorious: import hbs from "htmlbars-inline-precompile"
export default Ember.Component.extend({
// ....
layout: hbs`
<h1>Template here</h1>
`
}) For me, it makes the app way less confusing to navigate as I'm no longer juggling 2 files for every one component. My feelings aside, the adoption of single file components in both React, Vue.js, and Svetle.js suggests other developers really love the simplicity this provides. With the removal of |
@scottmessinger I believe it doesn't prevent single-file components. It could just be implemented with babel transform that takes out the As I read it what this DOES prevent is something like reusing a single template file for several components, is that so? Maybe the same thing could be achieved creating a symlink? |
No, this RFC definitely does not foreclose on the concept of single file components (which is definitely something that we would like to push forward in the near term).
It does however make the specific syntax that you highlighted not work (because there would no longer be a |
No, this shouldn't be an issue as far as I can tell. In the event that you wanted to use the same template in multiple places, you would author it in the "primary" location and use a reexport in the various other locations... |
We reviewed this RFC at the core team meeting today, and believe that this is ready for final comment period. |
I love all the detail here, especially in the teaching section. What's the rationale on needing both |
@courajs this is actually based on Ember's runloop and meta system. When you destroy an Ember object, you are also tearing down a companion meta object. If this isn't done today, it's possible that your app can have memory leaks due to references that the meta objects keep (though this will probably become less and less of an issue over time as we move away from certain abstractions). All object destructions in a single runloop are scheduled to occur together, at the end of the runloop, since the teardown work is very similar and more efficient if done together. So, today, when you call Glimmer Components won't extend from EmberObject, but they will use the same meta system for computed properties, tracked properties, and other things. Keeping the destruction flow the same will make it easier for code that needs to interoperate with both components, and there isn't really a good motivating reason to change it, so we opted to keep the same flow. Hope that answers your question! edit: Also, FWIW, something we've discussed is exposing these functions globally so that you can use them with POJOs and plain classes that also use these meta objects. So for instance, something like: import { destroyMeta, isDestroying, isDestroyed } from '@ember/meta';
obj = {};
destroyMeta(obj);
isDestroying(obj); // true
isDestroyed(obj); // false
setTimeout(() => {
isDestroyed(obj); //true
}); I think this is worth its own RFC if we were to do it. IMO, ideally, you shouldn't have to worry about destroying metas, and we would just allow them to be garbage collected naturally when an object is no longer in use. |
Could we add a bit to "how we teach this" that addresses One snag I've already seen experienced devs hit as they adjust to the "octane mental model" is the use of The following (template-only) {{#if showSomething}}
[Here's your something]
<button onclick={{action (mut showSomething) false}}>Hide</button>
{{else}}
<button onclick={{action (mut showSomething) true}}>Show</button>
{{/if}} This will not work the same way with glimmer components, since with no backing object, related: emberjs/ember.js#17202 |
@mike-north Glimmer Component's do have a backing object, and patterns with We should definitely add a section thoroughly explaining template-only components if the optional feature is turned on by default in Octane (I think it will be, but would have to confirm), but I'm not sure if it belongs here since this RFC is about a new backing class for components, and any components which use that class will necessarily not be template-only. |
@pzuraq I'd argue that "Glimmer Components" vs "Glimmer VM Components" is an implementation detail. In the GC world, deleting my .js or .ts file would result in one of two things, depending on whether the flag that's currently enabled in the blueprint, remains so. A ( B ( Both of these involve the component's nature changing more than developers may be used to (compare to the classic mental model of "you can safely delete empty JS modules" for components, routes, controllers, adapters, serializers). This is something that needs special teaching attention to set devs up for success. |
True, and I understand your concern - this change in behavior needs to be documented. However, this was discussed and decided upon in a previous RFC. If it were not for #278, we could for instance have decided to continue creating a backing component for template-only-components, and just have changed it to be an instance of That said, Octane (and more generally, a new Edition) is about rolling up the various changes since the last edition and making sure they are all fully covered in the guides. This definitely includes TO-components, along with Angle Bracket invocation, named args, and Glimmer Components. I think ideally we would have done a better job discussing the learning impact when TO-components were first proposed, but they will most certainly be covered by the effort to update the docs in this new edition. |
This RFC is looking great! Handling of component argument defaults is the one thing I'm still a little uncertain about. My thoughts are:
Has any consideration been given to some kind of interface Args {
enabled: boolean
};
export default class extends GlimmerComponent<Args> {
static defaultArgs(): Partial<Args> {
return { enabled: true };
}
}; Any arguments that aren't provided to the component, or have the value I realise the above may cut against the intended role of |
@richard-viney that's a great point, and while I do believe it has been discussed somewhere I can't seem to find it in this PR so I think it'll be good add the reasoning behind why we didn't add From a language design perspective, it comes down to being able to look at the template and know, with confidence, what things are. Currently, with all of the new features, we can know that:
We can know these things about these values without even looking at the rest of the template, which is pretty cool when you consider how dynamic Handlebars templates used to be! It used to be that you had no idea whether or not something was a property, an argument, a local variable, a global, etc. without reading not only the entire template, but also the With The fallback is that we use an alias with a default, so something like One final note is that one frequent use case for default arg values is for subclassing different types of components and overriding default values: <!-- button.hbs -->
<button class="{{this.buttonClass}}">{{yield}}</button> // button.js
export default Component.extend({
buttonClass: 'default',
}); // success-button.js
export default Button.extend({
buttonClass: 'success',
}); We believe this is actually better done (and probably more performant) "functionally", via templates: <!-- button.hbs -->
<button class="{{or @buttonClass 'default'}}">{{yield}}</button> <!-- success-button.hbs -->
<Button @buttonClass="success">{{yield}}</Button> This can quickly become a maintenance nightmare however, as soon as you add a new arg to That covers the language design part of this, but the other part is that the |
@richard-viney - Thanks for chiming in! Unfortunately, I think you hit the nail on the head here:
😺 Specifically, a named argument in the template tells you explicitly that that value is unaffected by the backing JS/TS file for the template. If we made it possible to modify/mutate the value of the named argument in the JS/TS file that pretty squarely violates the original goal. The motivation section of the Named Arguments RFC has more explanation and details here.
In lots of cases where you have a simple default, this is a perfectly good solution when you don't also need the defaulted value in the JS/TS side. RE: the caveats:
Yes! I think this is absolutely the right thing! Seeing In a future world (when Property Lookup Fallback Deprecation and "template imports" are done) there will be three different ways of referencing values in a template:
IMHO, this massively clarifies templates for future readers, and reduces a number of super common pitfalls in the current system... |
@pzuraq haha, our replies crossed in the mail! I think you probably said it better though 😃 |
Thanks for the responses! Yes I've read the Named Args RFC carefully, have been using them for some time in production, and generally agree with the rationales put forward there.
I agree that for arguments which only need a default applied once in the template it's reasonable to apply the default in the template directly. Is using Wrapping a template in
By 'burying' what I meant was: how does a developer coming to a component know where to find the default value for an argument if it has one? i.e. they're interested in the component's interface, rather than diving into its internals. The most immediately useful thing to them is the As an aside I've found that documentation of a component's public interface (including defaults) is complicated a little by template-only components. The team needs a different convention for documenting its available args (and their defaults). For backed components this is handled fairly naturally in the backing TS/JS file. Are there other documentation conventions for components that I'm not aware of?
My experience is that it's common for a component to have some args that are only consumed in the template, some args that are only consumed in the JS/TS, and some args that are consumed in both places. Splitting responsibility for defaults such that args only referenced in the template are defaulted there, and args that are referenced in the JS/TS are defaulted there in some other manner seems sub-optimal, ideally it would all be handled in a unified way. Also args may switch where they're being consumed (e.g. when refactoring) which complicates matters if defaults aren't handled via a single system. Some other possible discussion points:
I can't speak directly to the implementation/architectural complexity here - though I recognise it is an important consideration. Would it still be a significant challenge if the 'defaults' object was immutable and was provided once on registration of a component type? The main point here is that there would be no need to access the component instance when falling back to a default argument value. I wasn't intending to suggest that individual component instances be able to customise the defaults in any way, they would apply globally to all instances. Sorry for the exposition! I've been following the project for a while and felt compelled enough by this to chime in. |
I think you make some great points here. A few thoughts here:
FWIW, I think that this behavior is actually desired in many cases, specifically when a value is null or undefined. I do think that this should be handled by a
Exposing the expected interface of the component in a unified way definitely is important, agree 100% here. I do think that this is a somewhat separate problem from the question of defaults though, specifically because this problem is mostly related to TO-components (since components with a class do have a way to define this interface, as you pointed out), and one of the major benefits of TO-components is that they do not instantiate a class, making them more performant than components with a backing class. I think whatever mechanism we use to define this interface, it shouldn't encourage/require creation of a class just to define it.
I'm not sure I agree here. I think splitting the interface definition is definitely sub-optimal, but the defaulting logic could actually be better being split because it means that the default values are shown where they are relevant, and don't require context switching to find. A few final thoughts here:
|
One point to note is that having a way to specify defaults in the JS/TS file doesn't have to mandate a backing component class/context, along with its potential performance drawbacks. Building on what you suggested, we could envision a backing file such as export interface Args {
enabled: boolean
}
export const defaultArgs: Partial<Args> = {
enabled: true
} Here, the JS/TS file doesn't export a component class at all. This would allow defaults to be specified in a unified way across both normal and TO components, and has other benefits such as a unified place to define/document the component interface that works for TO components too, which I think is highly valuable. It also allows for proper type checking in TO components in future when type-safety in templates is a thing, which wouldn't be possible in current TO components as there'd be no Another observation is that the issues raised re. avoiding context switching between the template file and the backing file when reading the code are already going to reduced: first by MU, and then entirely by single-file components (when available). If we take both of these as broadly desirable changes then the concern re. context switching (which is mentioned frequently) would go away. At this point it would be hard to champion any defaulting constructs that were either specific to the use of arguments in templates (which could cause confusion due to divergence from the values held in An additional nicety here is that adding a backing class to a previously TO component becomes less potentially error-prone, as there'd be no figuring out of what arg defaults may be being applied in the template that now need to be moved to the backing class to be used in the JS/TS. Instead you just export the new backing class and continue with I considered suggesting a
If we follow JS convention for functions with default arguments then an argument with a value of
Agreed, most (all?) of what's being suggested around handling defaults is an addition rather than a fundamental change to the overall Glimmer components RFC, and could be handled in a follow-up RFC based on broader experience and feedback on pain points. We can certainly make do in the meantime with |
Thank you @richard-viney and @pzuraq for hammering through the ideas around defaulting and concluding that it is severable into a follow up RFC! |
This RFC was moved into Final Comment Period a little over a week and a half ago, all of the concerns that were raised have been addressed (or will be layered on top of this one in a new RFC): its time has come. 😸 Thank you to everyone who participated in the process, we absolutely would not have been able to do it without y'all! |
Will |
rendered
Huge thanks to @tomdale, @rwjblue, @krisselden, @mike-north, @stefanpenner, @chadhietala, @MelSumner, and everyone else who helped with feedback and design in this RFC! This has been a long time coming, and represents a massive community effort that I have been very glad to be a part of!