Skip to content

Conversation

MaximSagan
Copy link

@MaximSagan MaximSagan commented Aug 12, 2025

What is the current behavior?

Cannot specify bindings/host directives when using ngComponentOutlet.

Issue Number: #63099

What is the new behavior?

NgComponentOutlet has received new inputs bindings and directives which are simply passed through to the underlying createComponent implementation.

Note: As bindings and the existing inputs (which I think should be deprecated) are incompatible, in dev mode an error is thrown if both bindings and inputs are specified.

Does this PR introduce a breaking change?

  • Yes
  • No

@pullapprove pullapprove bot requested a review from mmalerba August 12, 2025 03:03
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: common Issues related to APIs in the @angular/common package labels Aug 12, 2025
@ngbot ngbot bot added this to the Backlog milestone Aug 12, 2025
@@ -114,7 +114,7 @@ export {
afterNextRender,
ɵFirstAvailable,
} from './render3/after_render/hooks';
export {Binding, inputBinding, outputBinding, twoWayBinding} from './render3/dynamic_bindings';
export {Binding, DirectiveWithBindings, inputBinding, outputBinding, twoWayBinding} from './render3/dynamic_bindings';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to reference this type from common. Should this be modified with the ɵ syntax?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this API is already exposed in createComponent , I think it's fine to expose it on the public API. (Binding being also already public).

Could you please add /** @publicApi **/ to both Binding and DirectiveWithBindings.

RuntimeErrorCode.INVALID_INPUT,
'`ngComponentOutletInputs` and `ngComponentOutletBindings` are incompatible.',
);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please advise if there is a better place to do this check.

@JeanMeche
Copy link
Member

Thank you for suggesting the feature, we discussed it a bit with the team and we think it's a right directive to go.
We'll help you get the PR in a proper state for review.

@JeanMeche JeanMeche removed the request for review from mmalerba August 13, 2025 14:47
Comment on lines 112 to 114
/**
* @deprecated This input is deprecated, use `ngComponentOutletBindings` instead.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we didn't deprecate setInput when we introduced bindings, we'll probably want to keep that API for now.
Depending on usages & user feedback, we can still deprecate it later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Instead, how about a note in the description that this input is incompatible with the ngComponentOutletBindings input?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll definitely need such information in the JSDoc of both properties.
Something like "input and bindings are mutually exclusive, you can either use on or the other but not both together".

Comment on lines +175 to +179
if (ngDevMode && this.ngComponentOutletInputs && this.ngComponentOutletBindings) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_INPUT,
'`ngComponentOutletInputs` and `ngComponentOutletBindings` are incompatible.',
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably the right location for that (we'll see if we maybe want to extract that in a separate function.

Also, the general behavior we like to have is following:

  • Throw if you provide both like you did
  • Throw if the bindings change & the component outlet didn't change.

Bindings can only be set once, so we believe that they could be changed only we change the component that we show.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the general behavior we like to have is following:

  • Throw if you provide both like you did
  • Throw if the bindings change & the component outlet didn't change.

Bindings can only be set once, so we believe that they could be changed only we change the component that we show.

Isn't this covered by adding ngComponentOutletBindings to the check in _needToReCreateComponentInstance(changes)? Or is this implicit behavior undesired?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would want to explictly throw in such case.
Without this behavior, changing a binding (or more precisly changing the reference to the binding), would recreate the component from scratch which would be surprising.

Copy link
Author

@MaximSagan MaximSagan Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JeanMeche I don't mean to push back too much on this, but I think it might be worth considering (if you haven't already) that throwing an error here could make certain usages difficult/impossible.

Consider I have a component:

@Component({
  ...,
  template: `<ng-template *ngComponentOutlet="config().component; bindings = config().bindings" />`
})
class Switcher {
  readonly config = input.required<{ component: Type<>; bindings: Binding[] }>();
}

And I have multiple possible "configs" to pass to this Switcher component, including:

  configA = { component: ComponentX, bindings: [inputBinding("x", myValue)] };
  configB = { component: ComponentY, bindings: [inputBinding("y", myValue)] };
  configC = { component: ComponentX, bindings: [modelBinding("x", myWritableValue)] };

We would be able to switch from configA to configB or from configB to configC with no issue, but with the proposed error, switching directly from configA to configC would throw (because the ngComponentOutlet (component) property didn't change - it was ComponentX in both cases). In this case, it seems quite likely that the user might in fact want a completely different component instance (because their usage of the component is different in each case - one with an input binding and one with a model binding), but it would be impossible for them to achieve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this real-world use case you have ? Because while I see the argument, you could save that Config A & C are basically the same.
In terms of API design it is easier to go from something more restrictive to something less than the oppositive.
This is also why we prefer to be conservative first and maybe loosen the restrictions later if we consider the change to be worth it.

Copy link
Author

@MaximSagan MaximSagan Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my example wasn't great. Maybe a better case would be something like this:

productAConfig = {
  component: ProductTypeXComponent,
  bindings: [inputBinding("productId", () => "a"), twoWayBinding("quantity", productAQuantity)]
};
productBConfig = {
  component: ProductTypeYComponent
  bindings: [inputBinding("productId", () => "b"), twoWayBinding("quantity", productBQuantity)]
};
productCConfig = {
  component: ProductTypeXComponent,
  bindings: [inputBinding("productId", () => "c"), twoWayBinding("quantity", productCQuantity)]
};

Here, if we compare A and C, despite the fact that both products would be rendered using the same component type (X), I think we can see two reasons we would want to recreate the component with new bindings:

  1. The productId binding is potentially non-reactive, e.g. it has some kind of logic that "initializes" the component using that product ID, and does not respond to further changes. (I try to avoid this kind of component, but I think in reality there are many such components in large codebases, particularly legacy components that use ngOnInit and not ngOnChanges.)
  2. The quantity two-way binding needs to be switched to a writable signal that represents the current state of the quantity of that product, hence re-binding (and hence recreation of the component) is necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JeanMeche If my above example is not persuasive, I'll implement the requested behaviour (throw error when bindings change but component doesn't), can I just verify the following:

  1. the requested behaviour should occur in the else block of the if (this._needToReCreateComponentInstance(changes)) conditional.
  2. similar behaviour is not expected of the directives input

Copy link
Member

@JeanMeche JeanMeche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beside my other comments,
We'll like this feature change to be covered by the corresponding unit tests in packages/common/test/directives/ng_component_outlet_spec.ts.

You'll be able to run the unit tests with pnpm bazel test //packages/common/test:test.

@JeanMeche JeanMeche added the action: review The PR is still awaiting reviews from at least one requested reviewer label Aug 13, 2025
@MaximSagan MaximSagan force-pushed the ng-component-outlet-bindings branch 2 times, most recently from 75306cf to b2a759e Compare August 21, 2025 15:26
@MaximSagan
Copy link
Author

MaximSagan commented Aug 21, 2025

Beside my other comments, We'll like this feature change to be covered by the corresponding unit tests in packages/common/test/directives/ng_component_outlet_spec.ts.

You'll be able to run the unit tests with pnpm bazel test //packages/common/test:test.

@JeanMeche I used signal inputs/models, new outputs, and signal queries in the tests. I could see that they hadn't been used in the existing tests, but these were quite old, and particularly, use of model inputs for testing two-way binding seemed appropriate. However, but based on the test results, it seems that these aren't supported in @angular/common tests (perhaps only in core). Is this right?

@JeanMeche
Copy link
Member

Indeed for some technical reasons, you can't use the initializer APIs (input, output, signal view queries) in the unit tests.
We're fine with decorators here.

…ing ngComponentOutlet

Enable specification of bindings and directives when using ngComponentOutlet.
@MaximSagan MaximSagan force-pushed the ng-component-outlet-bindings branch from b2a759e to 8564887 Compare August 22, 2025 04:00
@MaximSagan
Copy link
Author

Indeed for some technical reasons, you can't use the initializer APIs (input, output, signal view queries) in the unit tests. We're fine with decorators here.

Got it, updated to use decorators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: review The PR is still awaiting reviews from at least one requested reviewer area: common Issues related to APIs in the @angular/common package detected: feature PR contains a feature commit
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants