-
Notifications
You must be signed in to change notification settings - Fork 26.3k
Support directives and bindings on dynamically-created components #60137
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
Conversation
This PR also fixes this issue #47728 (even though this one was closed) 🎉 |
5735fb5
to
b9186c7
Compare
Also it brings us one step closer to #43120, I guess. |
b9186c7
to
28def5a
Compare
@@ -378,3 +378,80 @@ function isOutputSubscribable(value: unknown): value is SubscribableOutput<unkno | |||
value != null && typeof (value as Partial<SubscribableOutput<unknown>>).subscribe === 'function' | |||
); | |||
} | |||
|
|||
/** Listens to an output on a specific directive. */ | |||
export function listenToDirectiveOutput( |
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 wasn't totally sure where to place this one, but it's basically a targeted version of the listener instruction.
00cd6fd
to
dd4513c
Compare
Would this also help solve the #51878 or are they unrelated |
It's a step towards resolving #51878, but the main issue is that there's no way we can infer the type if an input is aliased. |
What about making content projection and keeping the hydration strategy turned on? 🥲 |
Will the new API allow (later probably) host directives input/output binding from hosting component without exposing them? |
Also what about: const name = signal('name');
modelBinding('name', name); To allow two way binding? |
Is it possible to bind to multiple directives and the component with one inputBinding? Or is this a scenario where ComponentRef.setInput should be used? |
@Harpush regarding host directives: allowing bindings directly to the host directives is something we're considering, but it will likely still be only to the exposed inputs/outputs. Also yes, we're considering adding @demike regarding targeting: we intentionally allow only targeted writes since the intent is much clearer compared to
|
I have also implemented a method for dynamic instruction loading |
Surely any change like this needs a RFC. Questions: Wouldn't createComponent and cresteDirective etc be a good replacement for new authoring as a way of removing decorators? If authoring goes functional is they actually going to be a real need for this? Is this just going to be a stop gap and if so doesn't that make end migration harder? |
So this approach assumes that all inputs are known. Scenario json based templating mechanism const template = {
componenId: "mycomponent", // dumb component --> id is used for lookup in a registry to get the class for createComponent
someComponentInput: true,
bind: "some.resource.identifier", // input for directive binding (like ngModel but for websocket data
children: [
// ...
]
// ...
} In the template engine the bindTo is well known --> create the directive for that and set the input Therefore I would pass all members of the template to inputBinding(inputName, inputValue, { ignoreMissingInput: true }) on the component |
I await this change with enthusiasm! Will it be possible to apply defer/hydrate on these components through createComponent? Currently, I'm struggling to find a way to do that. |
}, | ||
); | ||
|
||
describe('attaching directives to root component', () => { |
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.
nit: this test file becomes a bit long, I could see how thopse new tests are moved to a separate file (something like create_component_with_directives_spec.ts
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.
Hmm that was kinda my thinking with moving them into the file in the first commit.
Moves the tests for `createComponent` into their own file since the `component_spec.ts` was a bit too generic and was accumulating all sorts of tests.
…ives The check that verifies that there are no duplicates in the directives array was only running after host directive matching since that was the only case when it can happen. After the upcoming changes that won't be the case anymore so these changes move it always run after directive matching. I also did some additional cleanup by adding comments and by not lazily initializing the `allDirectiveDefs` array when matching host directives. The array is guaranteed to be defined since earlier in the function we verify that there's at least one def with host directives.
Reworks the `InputBinding` and `OutputBinding` functionality to be in object literals constructed in functions, rather than classes, because it seems like Terser was having a hard time tree shaking the classes when the functions weren't used.
462d54e
to
3311f2e
Compare
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.
reviewed-for: public-api
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.
LGTM
Reviewed-for: public-api
This PR was merged into the repository by commit a3cb7b9. The changes were merged into the following branches: main |
…ives (#60137) The check that verifies that there are no duplicates in the directives array was only running after host directive matching since that was the only case when it can happen. After the upcoming changes that won't be the case anymore so these changes move it always run after directive matching. I also did some additional cleanup by adding comments and by not lazily initializing the `allDirectiveDefs` array when matching host directives. The array is guaranteed to be defined since earlier in the function we verify that there's at least one def with host directives. PR Close #60137
Some upcoming functionality won't work if we can't retrieve a directive definition from a class. These changes add a `throwIfNotFound` to `getDirectiveDef`, similar to `getNgModuleDef`, to avoid duplication in such cases. PR Close #60137
Sets up the symbols used to power the upcoming `inputBinding` functionality. I also fixed that `setDirectiveInput` was incorrectly only allowing strings for the `value` parameter. PR Close #60137
Fixes that the `setDirectiveInput` function wasn't checkin if an input exists before writing to it which can lead to assertion errors. PR Close #60137
…ts (#60137) Adds the ability to bind to inputs on dynamically-created components, either by targeting the component itself or one of its directives. The new API looks as follows: ```ts const value = signal(123); createComponent(MyComp, { // Bind the value `'hello'` to `someInput` of `MyComp`. bindings: [inputBinding('someInput', () => 'hello')], directives: [{ type: MyDir, // Bind the `value` signal to the `otherInput` of `MyDir`. bindings: [inputBinding('otherInput', value)] }] }); ``` This behavior overlaps with `ComponentRef.setInput`, with a few key differences: 1. `setInput` sets the value on *all* inputs whereas `inputBinding` only targets the specified directive and its host directives. This makes it easier to know which directive you're targeting. 2. `inputBinding` is executed as if it's in a template, making it consistent with how bindings behave for selector-matched components, whereas `setInput` executes outside the lifecycle of the component. 3. It resolves a long-standing issue with `setInput` where it wasn't possible to set the initial value of an input before the first change detection run. Currently `inputBinding` is used only for `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create`, however it is going to be base for more APIs in the future. PR Close #60137
Calling `setInput` while the component already has an `inputBinding` active can lead to inconsistent state. These changes add an error that will be thrown if that's the case. PR Close #60137
…nents (#60137) Adds the new `outputBinding` function that allows users to listen to outputs on dynamically-created components in a similar way to templates. For example, here we create an instance of `MyCheckbox` and listen to its `onChange` event: ```ts interface CheckboxChange { value: string; } createComponent(MyCheckbox, { bindings: [ outputBinding<CheckboxChange>('onChange', event => console.log(event.value)) ], }); ``` Note that while it has always been possible to listen to events like this by getting a hold of of the instance and subscribing to it, there are a few key differences: 1. `outputBinding` behaves in the same way as if the event was bound in a template which comes with some behaviors like forwarding errors to the `ErrorHandler` and marking the view as dirty. 2. With `outputBinding` the listeners will be cleaned up automatically when the component is destroyed. 3. `outputBinding` accounts for host directive outputs by binding to them through the host. E.g. if the `onChange` event above was coming from a host directive, `outputBinding` would bind to it automatically. Currently `outputBinding` is available only in `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create`, but it will serve as a base for APIs in the future. PR Close #60137
Awesome, love it. |
…omponents Builds on the changes from angular#60137 to add support for two-way bindings on dynamically-created components. Example usage: ```typescript import {createComponent, signal, twoWayBinding} from '@angular/core'; const value = signal(''); createComponent(MyCheckbox, { bindings: [ twoWayBinding('value', value), ], }); ``` In the example above the value of `MyCheckbox` and the `value` signal will be kept in sync.
…omponents (#60342) Builds on the changes from #60137 to add support for two-way bindings on dynamically-created components. Example usage: ```typescript import {createComponent, signal, twoWayBinding} from '@angular/core'; const value = signal(''); createComponent(MyCheckbox, { bindings: [ twoWayBinding('value', value), ], }); ``` In the example above the value of `MyCheckbox` and the `value` signal will be kept in sync. PR Close #60342
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
These changes expand the APIs for creating components dynamically (
createComponent
,ViewContainerRef.createComponent
andComponentFactory.create
) to allow users to apply directives on the component, as well as to bind to inputs/outputs in a targeted manner either on the component or one of the directives, for example:Note that while it has been possible to do some these things through other APIs in the past (e.g.
setInput
), these new APIs have a few advantages:outputBinding
will clean up automatically when the component is destroyed.setInput
are defined as method that cannot be deleted.TestBed
andhostBindings
.