Public API for render-tree-based scope exploration
Propose adding a new Public API for exploring render-tree-based scope.
Rendered
Summary
This pull request is proposing a new RFC.
To succeed, it will need to pass into the Exploring Stage, followed by the Accepted Stage.
A Proposed or Exploring RFC may also move to the Closed Stage if it is withdrawn by the author or if it is rejected by the Ember team. This requires an "FCP to Close" period.
An FCP is required before merging this PR to advance to Accepted.
Upon merging this PR, automation will open a draft PR for this RFC to move to the Ready for Released Stage.
Exploring Stage Description
This stage is entered when the Ember team believes the concept described in the RFC should be pursued, but the RFC may still need some more work, discussion, answers to open questions, and/or a champion before it can move to the next stage.
An RFC is moved into Exploring with consensus of the relevant teams. The relevant team expects to spend time helping to refine the proposal. The RFC remains a PR and will have an Exploring label applied.
An Exploring RFC that is successfully completed can move to Accepted with an FCP is required as in the existing process. It may also be moved to Closed with an FCP.
Accepted Stage Description
To move into the "accepted stage" the RFC must have complete prose and have successfully passed through an "FCP to Accept" period in which the community has weighed in and consensus has been achieved on the direction. The relevant teams believe that the proposal is well-specified and ready for implementation. The RFC has a champion within one of the relevant teams.
If there are unanswered questions, we have outlined them and expect that they will be answered before Ready for Release.
When the RFC is accepted, the PR will be merged, and automation will open a new PR to move the RFC to the Ready for Release stage. That PR should be used to track implementation progress and gain consensus to move to the next stage.
Checklist to move to Exploring
- [ ] The team believes the concepts described in the RFC should be pursued.
- [ ] The label
S-Proposedis removed from the PR and the labelS-Exploringis added. - [ ] The Ember team is willing to work on the proposal to get it to Accepted
Checklist to move to Accepted
- [ ] This PR has had the
Final Comment Periodlabel has been added to start the FCP - [ ] The RFC is announced in #news-and-announcements in the Ember Discord.
- [ ] The RFC has complete prose, is well-specified and ready for implementation.
- [ ] All sections of the RFC are filled out.
- [ ] Any unanswered questions are outlined and expected to be answered before Ready for Release.
- [ ] "How we teach this?" is sufficiently filled out.
- [ ] The RFC has a champion within one of the relevant teams.
- [ ] The RFC has consensus after the FCP period.
I'm a fan of accessing the DI container through functions. I looks a bit clunky though, when you always get the scope first, then the owner from it.
A similar sounding concept, with also similar idea (to access the DI container) is done in ember-polaris-service. Could you also take reference as prior art? I think there is a little effect onto this RFC based on that.
See: https://github.com/chancancode/ember-polaris-service
@gossi done, thanks!
@gossi from my understanding scope is not the DI container but a localized scope for the current rendering context.
DI container implies (usually) a singleton for the entire application.
It is even lower level than an idea of something like context which only has a lookup and sends the closest entry (some implementations literally throw away reference to things that provide the same key)
The way context is shown here, you have access to full iterator stack so if you really wanted to you could implement something like getAncestors:
function getAncestors(component: typeof Component) {
const scope = getScope()
return scope.entries.filter(a => a instanceof component).toArray();
}
class TreeNode extends Component {
get depthCount() {
return getAncestors(TreeNode);
}
<template>
<Provide @key={{TreeNode}} @value{{this}}>
... IDK cool stuff probably
</Provide>
</template>
}
Something else to explore -- can we use the module hierarchy to ensure that everything can be known statically?
another option for ensuring type safety:
class Foo {
bar = 2;
}
// libraries could export `foo`, or each thing separately if they want
const foo = makeContext(Foo)
<template>
<foo.Provide>
{{#let (foo.consume) as |fooState|}}
{{fooState.bar}} == 2
{{/let}}
</foo.Provide>
{{ (foo.consume) }} <-- throws an exception
</template>
Clarify:
- what is reactive? (not the value passed to makeContext)
- reactivity must be within the context
- reduce harm
- throw on no provider in the hierarchy
Bringing in ideas from createContext in react as well as AutoFac from C# (which allows localized scope for factories/injections)
function makeContext<T>(defaultValueFactory: () => T) : {
Provide: ProvideComponent<T>,
consume: () => T
}
This allows the greatest flexibility while also ensuring better safety.
For people who want to stick to @NullVoxPopuli's a zero argument constructor as the factory this could easily be built as a higher order component:
function makeClassContext<T>(klass: new () => T) {
return makeContext(() => new klass);
}
[!NOTE] This
makeClassContextcould be an overload and this could have benefits of registering classes that may have lifecycle
But we also have a default factory function inspired by AutoFac which allows hooks for possible scenarios:
- Some people might be ok with value being null so can return null
- Some people may want their context to error when not found (this can be done and reduce null checks in user land if desired)
- Some teams may want to return a value for production stability, but want to have a dev assertion
Example of a "Permission" context where you may want to add user impersonation (which must be able to pass through element boundaries)
But the providers let you sparsely provide args
<template>
<Permission @user={{session.user}}>
<Permission @role={{adminRole}}>
<UserCard />
</Permission>
{{!-- We want to impersonate and override outer scope--}}
<Permission @user={{tempUser}}>
<UserCard />
</Permission>
</Permission>
</template>
const PermissionContext = makeContext(() => {
user: null,
role: null,
})
class Permission extends Component {
get permissionInfo () {
const upperTheme = PermissionContext.consume();
return {
user: this.args.user ?? upperTheme.user,
role: this.args.role ?? upperTheme.role,
}
}
<template>
<PermissionContext.Provide @value={{this.permissionInfo}}>
{{yield}}
</PermissionContext.Provide>
</template>
}
Some TS exploration and overloads: https://www.typescriptlang.org/play/?#code/MYGwhgzhAECyCeAVAFgSwHYHNoG8C+AUAQKYAeADgPYBOALtAGYCu6wtql60qEAcsQHcwAIxDEAFKQBc0MOngBKGaW4x0g6OIB0OsNUwQZLANbpKA9AG0AugugBeAHzRenXkxDhRxADyVhAFbEbM44BNDQAPSR0Ijw5MQAysDUqOT0yJDQ1MRgnvDQwmAAJtDk1JS0lfHE0BBM5FR00AC0LdC0yMQFxZwA5PQ5eSDw4VExQuj0DJQgxjBMEBjYnTx1NPSUDHW0TAzbbYVM9Ki0fTCZ6MUFM9RlxNTbcqXDHZm0WmPRXzHEECAYWgtYo8ERiFrqUhAgHqaAAAVoNQgKTSQL+MNokTMLRYEDADGILQAtsQicIHi0wMBgH8IGMcrtqFxSFpypVqgkAPxaYCcCC0ahMNg0Bz2ezQUgAbgIhAIzFY7E4sigD1o4iJEEwMn5qSwABoOn9aEZ0KZzOglMqIKqYLQjbgCABIVDbcQAQjt-LsnQqAmgGsw0tlgIeDCptQAChUAG6oYrEADClCJVHUUx8AEFQtx0NHKMYwcQZAKmLVg1NQ+HoEmK1CfIhQk6o5RY-GZM3W4nk6niOmG9LHbz0PUSTJxHYnLEg0RIgAqWfhWfZYiMrjRvKl6CZGDW2gAeQsD1kVytlGAqDAdoAIkaKvBCwm0CBSrvoErTpoXdwzjA5G-AsEtAKIukQkBQGyMCwbAcFwRJgMYXa1rQ9aOOI8Zhh4tAAGobsQABiVJVNQ8BjhOziIJaNZ2nW-ZgU00xQYqsHwYh1HIQ2aHEBhIDYbhBHCsRMjqH644OORlGcGxKHSmQ9GQQqMH+ixVFkOxqHoWAmE4SApYyBRMgqTRjgyeBzTytBSpwQhhlqdouj6IY0ARnoYAkna1AQD4iIJFs0AAPpWfhNB4YxMEoc4AA+TkuW5Dyed5xC+QFLF4TQ-BCN4kXRdQrkrnFXk1ElgWpdQ2mlo4dhhI6kDWnQ4gAAYAKqmmYFiyKkiIyAAJDgegGFoYhYJ0eBaEp1mSapb7oCM0CyYBMCcLU5AxXl1D1QafUQANvaYJ0origAjAo0pOmI9AaZhhYOO1BiWAADNYJ3Oq6PDpYWnHcbQhYKJVTqOgyTBMv5xVpYI70XTx30DrKz2aAlvkQ193j7dAfTmUxfS-Y6-0roDXDJQhJUhQpnAfZpkPeMdTowwDQME0FpW4WTl2UydspyqFSr00TnPoCh4gMOgpFibEElIShDqOtEsieGUqTrnasjkGsXQ5KdK7QAhBTioFzWoAAjqWNmJPAZKzOOT201wVWOh2cZFmNxB2-G1DiFrBqCwoep-UOI4O4FNZ+67Wte9TMocyT+Mg9Qb3ePzgtCRoomTvp1YTUZVVnZr3TXbr6AG0b6e0CbZsgBbRA46uku2zG9syIFzsPG73Qexa3vY77TCjo7gdd03Ift4QhDo4p3M0GVvgceuOkO6nNkS2EEQRFnWu5yxeuG6xqkl8I5tU06VvV439csY3wct9A0+lqHHd8n3J-jcOffn4og-hwQAdF+ICAoMsVOf0hcQydnAACZ-7KS-sAzQ+AfrSgAWxIBZFoDCTgEgNAWBxxUyAA