diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index a53793cf74c..83600998d47 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -1006,7 +1006,7 @@ export interface InputSignalWithTransform extends Signal { // (undocumented) [ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: TransformT; // (undocumented) - [SIGNAL]: InputSignalNode; + [SIGNAL]: ɵInputSignalNode; } // @public @@ -1149,7 +1149,7 @@ export interface ModelOptions { // @public export interface ModelSignal extends WritableSignal, InputSignal, OutputRef { // (undocumented) - [SIGNAL]: InputSignalNode; + [SIGNAL]: ɵInputSignalNode; } // @public @deprecated diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 657e02103a7..495f51dec44 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -137,3 +137,5 @@ export {isPromise as ɵisPromise, isSubscribable as ɵisSubscribable} from './ut export {performanceMarkFeature as ɵperformanceMarkFeature} from './util/performance'; export {stringify as ɵstringify, truncateMiddle as ɵtruncateMiddle} from './util/stringify'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider_flags'; + +export {type InputSignalNode as ɵInputSignalNode} from './authoring/input/input_signal_node'; diff --git a/packages/core/src/core_reactivity_export_internal.ts b/packages/core/src/core_reactivity_export_internal.ts index 68ccc917c11..5a9906bafc5 100644 --- a/packages/core/src/core_reactivity_export_internal.ts +++ b/packages/core/src/core_reactivity_export_internal.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ +export {SIGNAL as ɵSIGNAL} from '@angular/core/primitives/signals'; + export {isSignal, Signal, ValueEqualityFn} from './render3/reactivity/api'; export {computed, CreateComputedOptions} from './render3/reactivity/computed'; export { diff --git a/packages/upgrade/src/common/src/downgrade_component.ts b/packages/upgrade/src/common/src/downgrade_component.ts index 056f2d0dc6f..852cc0595b0 100644 --- a/packages/upgrade/src/common/src/downgrade_component.ts +++ b/packages/upgrade/src/common/src/downgrade_component.ts @@ -105,6 +105,8 @@ export function downgradeComponent(info: { $injector: IInjectorService, $parse: IParseService, ): IDirective { + const unsafelyOverwriteSignalInputs = + (info as {unsafelyOverwriteSignalInputs?: boolean}).unsafelyOverwriteSignalInputs ?? false; // When using `downgradeModule()`, we need to handle certain things specially. For example: // - We always need to attach the component view to the `ApplicationRef` for it to be // dirty-checked. @@ -216,6 +218,7 @@ export function downgradeComponent(info: { $parse, componentFactory, wrapCallback, + unsafelyOverwriteSignalInputs, ); const projectableNodes = facade.compileContents(); diff --git a/packages/upgrade/src/common/src/downgrade_component_adapter.ts b/packages/upgrade/src/common/src/downgrade_component_adapter.ts index b5bf4c08a8a..639f3936652 100644 --- a/packages/upgrade/src/common/src/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/src/downgrade_component_adapter.ts @@ -19,6 +19,8 @@ import { StaticProvider, Testability, TestabilityRegistry, + type ɵInputSignalNode as InputSignalNode, + ɵSIGNAL as SIGNAL, } from '@angular/core'; import { @@ -53,6 +55,7 @@ export class DowngradeComponentAdapter { private $parse: IParseService, private componentFactory: ComponentFactory, private wrapCallback: (cb: () => T) => () => T, + private readonly unsafelyOverwriteSignalInputs: boolean, ) { this.componentScope = scope.$new(); } @@ -131,7 +134,7 @@ export class DowngradeComponentAdapter { let expr: string | null = null; if (attrs.hasOwnProperty(inputBinding.attr)) { - const observeFn = ((prop) => { + const observeFn = ((prop, isSignal) => { let prevValue = INITIAL_VALUE; return (currValue: any) => { // Initially, both `$observe()` and `$watch()` will call this function. @@ -140,11 +143,11 @@ export class DowngradeComponentAdapter { prevValue = currValue; } - this.updateInput(componentRef, prop, prevValue, currValue); + this.updateInput(componentRef, prop, prevValue, currValue, isSignal); prevValue = currValue; } }; - })(inputBinding.prop); + })(inputBinding.prop, input.isSignal); attrs.$observe(inputBinding.attr, observeFn); // Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time @@ -166,9 +169,9 @@ export class DowngradeComponentAdapter { } if (expr != null) { const watchFn = ( - (prop) => (currValue: unknown, prevValue: unknown) => - this.updateInput(componentRef, prop, prevValue, currValue) - )(inputBinding.prop); + (prop, isSignal) => (currValue: unknown, prevValue: unknown) => + this.updateInput(componentRef, prop, prevValue, currValue, isSignal) + )(inputBinding.prop, input.isSignal); this.componentScope.$watch(expr, watchFn); } } @@ -314,13 +317,19 @@ export class DowngradeComponentAdapter { prop: string, prevValue: any, currValue: any, + isSignal: boolean, ) { if (this.implementsOnChanges) { this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue); } this.inputChangeCount++; - componentRef.instance[prop] = currValue; + if (isSignal && !this.unsafelyOverwriteSignalInputs) { + const node = componentRef.instance[prop][SIGNAL] as InputSignalNode; + node.applyValueToInputSignal(node, currValue); + } else { + componentRef.instance[prop] = currValue; + } } private groupProjectableNodes() { diff --git a/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts b/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts index c7412d292db..f13a3115d55 100644 --- a/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts +++ b/packages/upgrade/src/common/test/downgrade_component_adapter_spec.ts @@ -181,6 +181,7 @@ withEachNg1Version(() => { $parse, componentFactory, wrapCallback, + /* unsafelyOverwriteSignalInputs */ false, ); } diff --git a/packages/upgrade/static/test/integration/downgrade_component_spec.ts b/packages/upgrade/static/test/integration/downgrade_component_spec.ts index 93e0f6a3e7d..7557e04438d 100644 --- a/packages/upgrade/static/test/integration/downgrade_component_spec.ts +++ b/packages/upgrade/static/test/integration/downgrade_component_spec.ts @@ -15,6 +15,7 @@ import { ElementRef, EventEmitter, Injector, + input, Input, NgModule, NgModuleRef, @@ -171,6 +172,49 @@ withEachNg1Version(() => { }); })); + it('should bind properties to signal inputs', waitForAsync(() => { + const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['name'] = 'world'; + }); + + @Component({ + selector: 'ng2', + inputs: ['message'], + template: 'Message: {{message()}}', + }) + class Ng2Component { + message = input(''); + } + + @NgModule({declarations: [Ng2Component], imports: [BrowserModule, UpgradeModule]}) + class Ng2Module { + ngDoBootstrap() {} + } + + // Hack to wire up the `input()` signal correctly, since our JIT tests don't run with the + // transform which supports `input()`. + (Ng2Component as any).ɵcmp.inputs.message = ['message', /* InputFlags.SignalBased */ 1]; + + ng1Module.directive( + 'ng2', + downgradeComponent({ + component: Ng2Component, + }), + ); + + const element = html(` +
+ +
`); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(document.body.textContent)).toEqual('Message: Hello world'); + + $apply(upgrade, 'name = "everyone"'); + expect(multiTrim(document.body.textContent)).toEqual('Message: Hello everyone'); + }); + })); + it('should bind properties to onpush components', waitForAsync(() => { const ng1Module = angular.module_('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['dataB'] = 'B';