-
Notifications
You must be signed in to change notification settings - Fork 26.6k
feat(common): enable specification of bindings and directives when using ngComponentOutlet #63101
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
base: main
Are you sure you want to change the base?
Conversation
@@ -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'; |
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.
Needed to reference this type from common
. Should this be modified with the ɵ
syntax?
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.
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.', | ||
); | ||
} |
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.
Please advise if there is a better place to do this check.
Thank you for suggesting the feature, we discussed it a bit with the team and we think it's a right directive to go. |
/** | ||
* @deprecated This input is deprecated, use `ngComponentOutletBindings` instead. | ||
*/ |
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.
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.
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.
Got it. Instead, how about a note in the description that this input is incompatible with the ngComponentOutletBindings
input?
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.
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".
if (ngDevMode && this.ngComponentOutletInputs && this.ngComponentOutletBindings) { | ||
throw new RuntimeError( | ||
RuntimeErrorCode.INVALID_INPUT, | ||
'`ngComponentOutletInputs` and `ngComponentOutletBindings` are incompatible.', | ||
); | ||
} |
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.
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.
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.
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?
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.
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.
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.
@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.
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.
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.
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 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:
- 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 usengOnInit
and notngOnChanges
.) - 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.
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.
@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:
- the requested behaviour should occur in the
else
block of theif (this._needToReCreateComponentInstance(changes))
conditional. - similar behaviour is not expected of the
directives
input
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.
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
.
75306cf
to
b2a759e
Compare
@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? |
Indeed for some technical reasons, you can't use the initializer APIs (input, output, signal view queries) in the unit tests. |
…ing ngComponentOutlet Enable specification of bindings and directives when using ngComponentOutlet.
b2a759e
to
8564887
Compare
Got it, updated to use decorators. |
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 inputsbindings
anddirectives
which are simply passed through to the underlyingcreateComponent
implementation.Note: As
bindings
and the existinginputs
(which I think should be deprecated) are incompatible, in dev mode an error is thrown if bothbindings
andinputs
are specified.Does this PR introduce a breaking change?