mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(upgrade): support input signal bindings (#57020)
`@angular/upgrade` writes to inputs when downgrading an Angular 2+ component into an Angular.JS adapter. Previously, it wrote directly to the input property, which isn't compatible with input signals. It also handles `ngOnChanges` directly. The correct way to support input signals would be to refactor upgrade to use `ComponentRef.setInput`, which also handles `ngOnChanges` internally. However, this refactoring might be more breaking since it would change the timing of certain operations. Instead, this commit updates the code to recognize `InputSignal` and write it through the `InputSignalNode`. This avoids the above breaking changes for now, until a bigger refactoring can be tested. Fixes #56860. PR Close #57020
This commit is contained in:
parent
901c1e1a7f
commit
e40a4fa3c7
7 changed files with 70 additions and 9 deletions
|
|
@ -1006,7 +1006,7 @@ export interface InputSignalWithTransform<T, TransformT> extends Signal<T> {
|
|||
// (undocumented)
|
||||
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: TransformT;
|
||||
// (undocumented)
|
||||
[SIGNAL]: InputSignalNode<T, TransformT>;
|
||||
[SIGNAL]: ɵInputSignalNode<T, TransformT>;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
|
@ -1149,7 +1149,7 @@ export interface ModelOptions {
|
|||
// @public
|
||||
export interface ModelSignal<T> extends WritableSignal<T>, InputSignal<T>, OutputRef<T> {
|
||||
// (undocumented)
|
||||
[SIGNAL]: InputSignalNode<T, T>;
|
||||
[SIGNAL]: ɵInputSignalNode<T, T>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<any>,
|
||||
private wrapCallback: <T>(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<unknown, unknown>;
|
||||
node.applyValueToInputSignal(node, currValue);
|
||||
} else {
|
||||
componentRef.instance[prop] = currValue;
|
||||
}
|
||||
}
|
||||
|
||||
private groupProjectableNodes() {
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ withEachNg1Version(() => {
|
|||
$parse,
|
||||
componentFactory,
|
||||
wrapCallback,
|
||||
/* unsafelyOverwriteSignalInputs */ false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string>('');
|
||||
}
|
||||
|
||||
@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(`
|
||||
<div>
|
||||
<ng2 literal="Text" message="Hello {{name}}"></ng2>
|
||||
</div>`);
|
||||
|
||||
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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue