diff --git a/goldens/public-api/core/errors.api.md b/goldens/public-api/core/errors.api.md index 4f575ac92e8..9832a074115 100644 --- a/goldens/public-api/core/errors.api.md +++ b/goldens/public-api/core/errors.api.md @@ -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, diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index ff71b1d956a..afeda548b34 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -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, diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index a01d501b9e3..aab486edd64 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -260,6 +260,9 @@ export class ComponentFactory extends AbstractComponentFactory { rootViewInjector, ) : createHostElement(cmpDef, hostRenderer); + const hasInputBindings = + componentBindings?.some(isInputBinding) || + directives?.some((d) => typeof d !== 'function' && d.bindings.some(isInputBinding)); const rootLView = createLView( null, @@ -355,7 +358,7 @@ export class ComponentFactory extends AbstractComponentFactory { 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 extends AbstractComponentRef { constructor( componentType: Type, - 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 extends AbstractComponentRef { } 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 diff --git a/packages/core/test/acceptance/create_component_spec.ts b/packages/core/test/acceptance/create_component_spec.ts index 14d05ca1099..66d4c48d4da 100644 --- a/packages/core/test/acceptance/create_component_spec.ts +++ b/packages/core/test/acceptance/create_component_spec.ts @@ -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', () => {