Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Closed
wants to merge 10 commits into from

Conversation

crisbeto
Copy link
Member

These changes expand the APIs for creating components dynamically (createComponent, ViewContainerRef.createComponent and ComponentFactory.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:

import {createComponent, signal, inputBinding, outputBinding} from '@angular/core';

const canClose = signal(false);

// Create MyDialog
createComponent(MyDialog, {
  bindings: [
    // Bind a signal to the `canClose` input.
    inputBinding('canClose', canClose),

    // Listen for the `onClose` event specifically on the dialog.
    outputBinding<Result>('onClose', result => console.log(result)),
  ],
  directives: [
    // Apply the `FocusTrap` directive to `MyDialog` without any bindings.
    FocusTrap,

    // Apply the `HasColor` directive to `MyDialog` and bind the `red` value to its `color` input.
    // The callback to `inputBinding` is invoked on each change detection.
    {
      type: HasColor,
      bindings: [inputBinding('color', () => 'red')]
    }
  ]
});

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:

  1. They go through the same mechanisms as the template, meaning that they'll behave in the same way as input and output bindings in the template. For example, outputBinding will clean up automatically when the component is destroyed.
  2. They are able to target specific directives, whereas existing APIs apply to all directives at the same time which might not be desirable in some cases.
  3. The new APIs are tree-shakeable so their code won't make it into the bundle unless you're actually using it, whereas existing APIs like setInput are defined as method that cannot be deleted.
  4. It provides us with a unified way of defining bindings in TypeScript that can be rolled out in other places like TestBed and hostBindings.

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer target: minor This PR is targeted for the next minor release labels Feb 27, 2025
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: core Issues related to the framework runtime labels Feb 27, 2025
@ngbot ngbot bot added this to the Backlog milestone Feb 27, 2025
@eneajaho
Copy link
Contributor

This PR also fixes this issue #47728 (even though this one was closed) 🎉

@crisbeto crisbeto force-pushed the create-component-dir branch from 5735fb5 to b9186c7 Compare February 27, 2025 09:42
@eneajaho
Copy link
Contributor

Also it brings us one step closer to #43120, I guess.

@crisbeto crisbeto force-pushed the create-component-dir branch from b9186c7 to 28def5a Compare February 27, 2025 10:06
@@ -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(
Copy link
Member Author

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.

@crisbeto crisbeto marked this pull request as ready for review February 27, 2025 10:22
@crisbeto crisbeto force-pushed the create-component-dir branch from 00cd6fd to dd4513c Compare February 27, 2025 12:39
@sp90
Copy link
Contributor

sp90 commented Feb 27, 2025

Would this also help solve the #51878 or are they unrelated

@crisbeto
Copy link
Member Author

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.

@osnoser1
Copy link

What about making content projection and keeping the hydration strategy turned on? 🥲

@Harpush
Copy link

Harpush commented Feb 27, 2025

Will the new API allow (later probably) host directives input/output binding from hosting component without exposing them?

@Harpush
Copy link

Harpush commented Feb 27, 2025

Also what about:

const name = signal('name');
modelBinding('name', name);

To allow two way binding?

@demike
Copy link
Contributor

demike commented Feb 27, 2025

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?

@crisbeto
Copy link
Member Author

@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 twoWayBinding and potentially content projection as bindings as well.

@demike regarding targeting: we intentionally allow only targeted writes since the intent is much clearer compared to setInput. That being said, the inputBinding doesn't have any special compiler magic so you can pass it around if you want to reuse it, e.g.

const disabledBinding = inputBinding('disabled', () => true);

createComponent(Comp, {
  directives: [{type: One, bindings: [disabled, ...otherBindings]}, {type: Two, bindings: [disabled]}]
});

@wszgrcy
Copy link
Contributor

wszgrcy commented Feb 28, 2025

I have also implemented a method for dynamic instruction loading
Because I haven't found a job, I haven't made it public either
But currently, someone has proposed PR. I have also made it public for everyone's reference
https://github.com/wszgrcy/dynamic-component-define

@Jordan-Hall
Copy link
Contributor

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?

@demike
Copy link
Contributor

demike commented Feb 28, 2025

@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 twoWayBinding and potentially content projection as bindings as well.

@demike regarding targeting: we intentionally allow only targeted writes since the intent is much clearer compared to setInput. That being said, the inputBinding doesn't have any special compiler magic so you can pass it around if you want to reuse it, e.g.

const disabledBinding = inputBinding('disabled', () => true);

createComponent(Comp, {
  directives: [{type: One, bindings: [disabled, ...otherBindings]}, {type: Two, bindings: [disabled]}]
});

So this approach assumes that all inputs are known.
Is it possible to ignore missing bindings like inputBinding('disable', () => true, { ignoreMissingInput: true }

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
but the component that gets instantiated might also be intereseted in the binding address (bind) . i.e.: if it is a complex component and wants to bind to sub resources.

Therefore I would pass all members of the template to inputBinding(inputName, inputValue, { ignoreMissingInput: true }) on the component
and template.bind as input binding to the well known binding directive

@GuillaumeREIGNAT
Copy link

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', () => {

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

Copy link
Member Author

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.

@pullapprove pullapprove bot requested a review from alxhub March 3, 2025 09:45
crisbeto added 2 commits March 3, 2025 11:04
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.
@crisbeto crisbeto force-pushed the create-component-dir branch from 462d54e to 3311f2e Compare March 3, 2025 13:15
Copy link
Contributor

@thePunderWoman thePunderWoman left a 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

@pullapprove pullapprove bot requested a review from kirjs March 3, 2025 17:23
Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a 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

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 3, 2025
@crisbeto crisbeto removed request for kirjs, alxhub and mmalerba March 3, 2025 17:29
@mmalerba
Copy link
Contributor

mmalerba commented Mar 3, 2025

This PR was merged into the repository by commit a3cb7b9.

The changes were merged into the following branches: main

@mmalerba mmalerba closed this in 9330463 Mar 3, 2025
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
…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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
…d components (#60137)

Updates `createComponent`, `ViewContainerRef.createComponent` and `ComponentFactory.create` to allow the user to specify directives that should be applied when creating the component.

PR Close #60137
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
…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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
…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
mmalerba pushed a commit that referenced this pull request Mar 3, 2025
#60137)

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.

PR Close #60137
@kukjevov
Copy link
Contributor

kukjevov commented Mar 7, 2025

This PR also fixes this issue #47728 (even though this one was closed) 🎉

Awesome, love it.

crisbeto added a commit to crisbeto/angular that referenced this pull request Mar 12, 2025
…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.
pkozlowski-opensource pushed a commit that referenced this pull request Mar 17, 2025
…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
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Apr 7, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.