-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Default Helper Manager #756
Conversation
Co-authored-by: Robert Jackson <me@rwjblue.com>
Co-authored-by: Robert Jackson <me@rwjblue.com>
Co-authored-by: Robert Jackson <me@rwjblue.com>
We discussed this at today's core team meeting and are moving it into final comment period. 🎉 🎉 |
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.
Looks amazing, thanks for putting this together @NullVoxPopuli, really excited about this RFC 😄
Thank you for doing the exploratory work 🎉 |
Sorry for the late comment, but hey, this is why we have FCP! Unfortunately, I’m here to raise a fairly significant issue from the Typed Ember team: I believe it is actually impossible (at least with the current feature set of TypeScript or any anticipated features) to provide types which work correctly with a named-args-as-last-argument and (a) can be robustly type checked or (b) provide useful autocomplete. I’m working on writing up a longer deep dive explanation which will show why this is so we can dig through it in detail, and we’ll double check that our current view of the situation is correct as part of writing that up. The TL;DR is that even with TS's fancy tuple types, we can't actually keep enough information around for tools like Glint to have the info they need—it's basically the same kind of problem as JS's lack of support for In the interim, while I’m working on that, a few notes:
Note: while I’m working on getting this written up, I would appreciate it if we skipped any proposals for workarounds/etc. It’ll be much more productive if we can avoid going around about what does/n’t work until we have a clear explanation of the constraints in place! |
thanks for the explanation and thoughts! that said, I don't think dropping support for named arguments today prevents the addition of them in the future, like, if TC39&co do decide to add support for |
All right, having spent a few hours doing ✨ DARK TS MAGIC ✨ I have actually found a way to make this work, so consider the concern withdrawn.1 That said, there may be some costs to this: repeatedly doing the kind of overloaded type resolution (using conditional types, no less) for helpers and modifiers in the context of something like Glint will have overhead for type-checking, slowing it down both in build and in-editor feedback. I will note that getting this to work is quite finicky, and it wasn't possible when we last evaluated it during the Glint private alpha phase. The reason it's finicky is that there's a mismatch between the Handlebars semantics (which look more like Ruby's or Python's keyword argument semantics) and JavaScript semantics. Since JS doesn't have keyword arguments, the Handlebars/Glimmer distinction between named and positional parameters will remain a bad fit with JS/TS semantics for the foreseeable future.2 While I’m happy to see this specific issue unblocked, I hope we revisit the general design of helpers and modifiers and indeed “invokables” during the Ember 4.x/post-Polaris era, because I think there are potentially much better designs here which have much closer alignment with JS semantics.3 Footnotes
|
@chriskrycho I strongly agree with you that we should revisit the design after Polaris. A quick sketch of one direction: instead of helper manager, we could standardize on the "just a JS function" design in this RFC and require that people who want to do more elaborate mapping create higher-order functions that produce the kind of function Ember expects. As long as you type your higher-order function properly, Glint could target the natural mapping from Handlebars function call to JS function call, and everything should just work. A note on JS idioms: I personally believe that people should not create signatures that combine:
I think sometehing like |
Thanks for exploring this @chriskrycho @wycats! The team discussed this today, and (given that @chriskrycho confirmed that this wasn't a fundamental blocker) are going to go ahead and land this RFC. Thanks again for all of the conversation here y'all!! |
BREAKING CHANGE: copy real implementation this changes the behavior of the options arg to enable optional positional params when options are not used. See the RFC update for more details. emberjs/rfcs#756
# [2.0.0](v1.0.15...v2.0.0) (2022-01-30) ### chore * sync with RFC ([343a698](343a698)) ### BREAKING CHANGES * copy real implementation this changes the behavior of the options arg to enable optional positional params when options are not used. See the RFC update for more details. emberjs/rfcs#756
What do y'all think about binding the Please let me know, if I should open this as a separate issue instead or start work on an RFC right away. /** Creates a new ember-intl `t` helper function that is scoped to a prefix. */
export function scopedT(prefix: string) {
return function (this: Owner, key: string, options: Record<string, unknown>): string {
return this.lookup('service:intl').t(`${prefix}.${key}`, options);
}
}
export const tLabel = scopedT('ui.labels');
export const tError = scopedT('ui.error'); <label>{{tLabel "foo"}}</label>
<strong>{{tError "oh-no"}}</strong>
{{#let (scopedT "on-the-fly.bindings") as |t|}}
<p>{{t "are-super-useful"}}</p>
{{/let}} |
@buschtoens we very much want things to be normally bound when invoked… but probably not to the |
@chriskrycho Thank you for the super-swift reply! ⚡ I am 💯 on-board with what you're saying about retaining normal JS binding rules for method invocations, i.e. (And I'm very interested in helping! In general, I would like to familiarize myself more with the Glimmer internals. So far I only ever really ventured in there, when fixing bugs.) Coming back to the owner as As a matter of fact "bind" might be the wrong term to use. The function / method itself should not be bound to the owner as IMO this can co-exist with {{foo.bar "baz"}} → bound to `foo`
{{#let foo.bar as |bar|}}
{{bar "baz"}} → bound to owner
{{/let}} I've implemented a quick PoC for the polyfill. Of course it doesn't yet have any special handling for key paths. Your mention of key paths got me thinking though: Maybe we shouldn't use the owner for I can imagine scenarios, where convenient access to the template context without explicitly requiring the user to pass |
First—super excited to hear you're interested in the other bit; I'll chat with other Framework folks and see if we can get someone engaged to help that get moving forward with you! Second—on reading your original post again I follow what you’re wanting this for. Unfortunately, in that case, there's a fundamental problem (and it goes for any other general Also, note that for all template-only components, there is a template context, but not necessarily a backing component class instance: All that being said, I think there's an interesting design question around making it possible for functions to interact more directly with the DI container, even if I don't think we can do it in a direct way via function binding. One possibility in the space is something shaped sort of like React's import Helper from '@ember/component/helper';
import { service } from '@ember/service';
import IntlService from 'wherever/it/comes/from';
interface ScopedTSig {
Args: {
Named: Record<string, unknown>;
Positional: [prefix: string];
};
}
class ScopedT extends Helper<ScopedTSig> {
@service declare intl: IntlService;
compute([prefix]: [string], options: Record<string, unknown>) {
return this.intl.t(`${prefix}.${key}`, options);
}
}
<template>
{{ScopedT 'some-key' makeItAwesome=true}}
</template> This is definitely a bit longer, but it gets the job done nicely. I’ve also written a tiny class-based helper in the past to just pass along a service: import Helper from '@ember/component/helper';
import { getOwner } from '@ember/application';
export default class GetService extends Helper {
compute([name]) {
return getOwner(this).lookup(`service:${name}`);
}
} import GetService from 'my-app/helpers/get-service';
import someHelperWhichNeedsFoo from 'my-app/helpers/some-helper';
<template>
{{someHelperWhichNeedsFoo foo=(getService 'foo')}}
<template> Given recent TS work, that could even in principle become well-typed to return a known service with some tweaks (happy to chat about that on Discord). None of that changes the fact that something shaped like |
Advance RFC #756 to Stage Recommended
This feature landed in Ember 4.5+, but this polyfill would allow it to work on 3.25+ References RFC: emberjs/rfcs#756 Update: emberjs/rfcs#788 Guides: ember-learn/guides-source#1924
* Enable "plain function as helpers" polyfill This feature landed in Ember 4.5+, but this polyfill would allow it to work on 3.25+ References RFC: emberjs/rfcs#756 Update: emberjs/rfcs#788 Guides: ember-learn/guides-source#1924 * Convert truth-helpers to use plain functions Mainly to test that the polyfill is working, but it's a good refactor anyway.
Rendered