fix(core): do not allow setInput to be used with inputBinding (#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
This commit is contained in:
Kristiyan Kostadinov
2025-02-27 08:17:29 +01:00
committed by Miles Malerba
parent fe57332fc5
commit 3459faadbf
4 changed files with 42 additions and 2 deletions
+2
View File
@@ -79,6 +79,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
INVALID_MULTI_PROVIDER = -209,
// (undocumented)
INVALID_SET_INPUT_CALL = 317,
// (undocumented)
INVALID_SKIP_HYDRATION_HOST = -504,
// (undocumented)
LOOP_TRACK_DUPLICATE_KEYS = -955,
+1
View File
@@ -62,6 +62,7 @@ export const enum RuntimeErrorCode {
UNINITIALIZED_LET_ACCESS = 314,
NO_BINDING_TARGET = 315,
INVALID_BINDING_TARGET = 316,
INVALID_SET_INPUT_CALL = 317,
// Bootstrap Errors
MULTIPLE_PLATFORMS = 400,
+17 -2
View File
@@ -260,6 +260,9 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
rootViewInjector,
)
: createHostElement(cmpDef, hostRenderer);
const hasInputBindings =
componentBindings?.some(isInputBinding) ||
directives?.some((d) => typeof d !== 'function' && d.bindings.some(isInputBinding));
const rootLView = createLView<T>(
null,
@@ -355,7 +358,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
leaveView();
}
return new ComponentRef(this.componentType, rootLView);
return new ComponentRef(this.componentType, rootLView, !!hasInputBindings);
} finally {
setActiveConsumer(prevConsumer);
}
@@ -454,6 +457,10 @@ function getRootTViewTemplate(
};
}
function isInputBinding(binding: Binding): boolean {
return binding[BINDING].kind === 'input';
}
/**
* Represents an instance of a Component created via a {@link ComponentFactory}.
*
@@ -473,7 +480,8 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
constructor(
componentType: Type<T>,
private _rootLView: LView,
private readonly _rootLView: LView,
private readonly _hasInputBindings: boolean,
) {
super();
this._tNode = getTNode(_rootLView[TVIEW], HEADER_OFFSET) as TElementNode;
@@ -487,6 +495,13 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
}
override setInput(name: string, value: unknown): void {
if (this._hasInputBindings && ngDevMode) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_SET_INPUT_CALL,
'Cannot call `setInput` on a component that is using the `inputBinding` function.',
);
}
const tNode = this._tNode;
this.previousInputValues ??= new Map();
// Do not set the input if it is the same as the last value
@@ -1084,6 +1084,28 @@ describe('createComponent', () => {
ref.changeDetectorRef.detectChanges();
}).toThrowError(/RootDir does not have an input with a public name of "someInput"/);
});
it('should throw when using setInput on a component already using inputBindings', () => {
@Component({template: ''})
class RootComp {
@Input() someInput = '';
}
const hostElement = document.createElement('div');
const environmentInjector = TestBed.inject(EnvironmentInjector);
const ref = createComponent(RootComp, {
hostElement,
environmentInjector,
bindings: [inputBinding('someInput', () => 'hello')],
});
ref.changeDetectorRef.detectChanges();
expect(() => {
ref.setInput('someInput', 'changed');
}).toThrowError(
/Cannot call `setInput` on a component that is using the `inputBinding` function/,
);
});
});
describe('error checking', () => {