fix(upgrade): two-way binding and listening for event (#22772)
Changes would not propagate to a value in downgraded component in case you had two-way binding and listening to a value-change, e.g. [(value)]="value" (value-change)="fetch()" Closes #22734 PR Close #22772
This commit is contained in:
parent
aca4735c8b
commit
5391f96406
@ -168,42 +168,40 @@ export class DowngradeComponentAdapter {
|
|||||||
const outputs = this.componentFactory.outputs || [];
|
const outputs = this.componentFactory.outputs || [];
|
||||||
for (let j = 0; j < outputs.length; j++) {
|
for (let j = 0; j < outputs.length; j++) {
|
||||||
const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName);
|
const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName);
|
||||||
let expr: string|null = null;
|
|
||||||
let assignExpr = false;
|
|
||||||
|
|
||||||
const bindonAttr = output.bindonAttr.substring(0, output.bindonAttr.length - 6);
|
const bindonAttr = output.bindonAttr.substring(0, output.bindonAttr.length - 6);
|
||||||
const bracketParenAttr =
|
const bracketParenAttr =
|
||||||
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]`;
|
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]`;
|
||||||
|
// order below is important - first update bindings then evaluate expressions
|
||||||
|
if (attrs.hasOwnProperty(bindonAttr)) {
|
||||||
|
this.subscribeToOutput(output, attrs[bindonAttr], true);
|
||||||
|
}
|
||||||
|
if (attrs.hasOwnProperty(bracketParenAttr)) {
|
||||||
|
this.subscribeToOutput(output, attrs[bracketParenAttr], true);
|
||||||
|
}
|
||||||
if (attrs.hasOwnProperty(output.onAttr)) {
|
if (attrs.hasOwnProperty(output.onAttr)) {
|
||||||
expr = attrs[output.onAttr];
|
this.subscribeToOutput(output, attrs[output.onAttr]);
|
||||||
} else if (attrs.hasOwnProperty(output.parenAttr)) {
|
|
||||||
expr = attrs[output.parenAttr];
|
|
||||||
} else if (attrs.hasOwnProperty(bindonAttr)) {
|
|
||||||
expr = attrs[bindonAttr];
|
|
||||||
assignExpr = true;
|
|
||||||
} else if (attrs.hasOwnProperty(bracketParenAttr)) {
|
|
||||||
expr = attrs[bracketParenAttr];
|
|
||||||
assignExpr = true;
|
|
||||||
}
|
}
|
||||||
|
if (attrs.hasOwnProperty(output.parenAttr)) {
|
||||||
|
this.subscribeToOutput(output, attrs[output.parenAttr]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (expr != null && assignExpr != null) {
|
private subscribeToOutput(output: PropertyBinding, expr: string, isAssignment: boolean = false) {
|
||||||
const getter = this.$parse(expr);
|
const getter = this.$parse(expr);
|
||||||
const setter = getter.assign;
|
const setter = getter.assign;
|
||||||
if (assignExpr && !setter) {
|
if (isAssignment && !setter) {
|
||||||
throw new Error(`Expression '${expr}' is not assignable!`);
|
throw new Error(`Expression '${expr}' is not assignable!`);
|
||||||
}
|
}
|
||||||
const emitter = this.component[output.prop] as EventEmitter<any>;
|
const emitter = this.component[output.prop] as EventEmitter<any>;
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
emitter.subscribe({
|
emitter.subscribe({
|
||||||
next: assignExpr ? (v: any) => setter !(this.scope, v) :
|
next: isAssignment ? (v: any) => setter !(this.scope, v) :
|
||||||
(v: any) => getter(this.scope, {'$event': v})
|
(v: any) => getter(this.scope, {'$event': v})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing emitter '${output.prop}' on component '${getComponentName(this.componentFactory.componentType)}'!`);
|
`Missing emitter '${output.prop}' on component '${getComponentName(this.componentFactory.componentType)}'!`);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, OnDestroy, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
|
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, OnDestroy, Output, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
|
||||||
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
@ -434,6 +434,55 @@ withEachNg1Version(() => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should support two-way binding and event listener', async(() => {
|
||||||
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
const listenerSpy = jasmine.createSpy('$rootScope.listener');
|
||||||
|
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
|
||||||
|
$rootScope['value'] = 'world';
|
||||||
|
$rootScope['listener'] = listenerSpy;
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: `model: {{model}};`})
|
||||||
|
class Ng2Component implements OnChanges {
|
||||||
|
ngOnChangesCount = 0;
|
||||||
|
@Input() model = '?';
|
||||||
|
@Output() modelChange = new EventEmitter();
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
switch (this.ngOnChangesCount++) {
|
||||||
|
case 0:
|
||||||
|
expect(changes.model.currentValue).toBe('world');
|
||||||
|
this.modelChange.emit('newC');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(changes.model.currentValue).toBe('newC');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Called too many times! ' + JSON.stringify(changes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
|
||||||
|
|
||||||
|
@NgModule({declarations: [Ng2Component], imports: [BrowserModule]})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`
|
||||||
|
<div>
|
||||||
|
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
|
||||||
|
| value: {{value}}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
adapter.bootstrap(element, ['ng1']).ready((ref) => {
|
||||||
|
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
|
||||||
|
expect(listenerSpy).toHaveBeenCalledWith('newC');
|
||||||
|
ref.dispose();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should initialize inputs in time for `ngOnChanges`', async(() => {
|
it('should initialize inputs in time for `ngOnChanges`', async(() => {
|
||||||
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, ElementRef, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core';
|
import {ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, ElementRef, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, Output, SimpleChanges, destroyPlatform} from '@angular/core';
|
||||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
@ -148,6 +148,58 @@ withEachNg1Version(() => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should support two-way binding and event listener', async(() => {
|
||||||
|
const listenerSpy = jasmine.createSpy('$rootScope.listener');
|
||||||
|
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
|
||||||
|
$rootScope['value'] = 'world';
|
||||||
|
$rootScope['listener'] = listenerSpy;
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({selector: 'ng2', template: `model: {{model}};`})
|
||||||
|
class Ng2Component implements OnChanges {
|
||||||
|
ngOnChangesCount = 0;
|
||||||
|
@Input() model = '?';
|
||||||
|
@Output() modelChange = new EventEmitter();
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
switch (this.ngOnChangesCount++) {
|
||||||
|
case 0:
|
||||||
|
expect(changes.model.currentValue).toBe('world');
|
||||||
|
this.modelChange.emit('newC');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(changes.model.currentValue).toBe('newC');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Called too many times! ' + JSON.stringify(changes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Ng2Component],
|
||||||
|
entryComponents: [Ng2Component],
|
||||||
|
imports: [BrowserModule, UpgradeModule]
|
||||||
|
})
|
||||||
|
class Ng2Module {
|
||||||
|
ngDoBootstrap() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = html(`
|
||||||
|
<div>
|
||||||
|
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
|
||||||
|
| value: {{value}}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
|
||||||
|
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
|
||||||
|
expect(listenerSpy).toHaveBeenCalledWith('newC');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should run change-detection on every digest (by default)', async(() => {
|
it('should run change-detection on every digest (by default)', async(() => {
|
||||||
let ng2Component: Ng2Component;
|
let ng2Component: Ng2Component;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user