fix(ivy): ngOnChanges only runs for binding updates (#27965)

PR Close #27965
This commit is contained in:
Ben Lesh
2018-12-20 17:23:25 -08:00
committed by Andrew Kushnir
parent b0caf02d4f
commit 8ebdb437dc
47 changed files with 1468 additions and 1397 deletions

View File

@ -9,7 +9,7 @@
import {NgForOf as NgForOfDef, NgIf as NgIfDef, NgTemplateOutlet as NgTemplateOutletDef} from '@angular/common';
import {IterableDiffers, TemplateRef, ViewContainerRef} from '@angular/core';
import {DirectiveType, NgOnChangesFeature, defineDirective, directiveInject} from '../../src/render3/index';
import {DirectiveType, defineDirective, directiveInject} from '../../src/render3/index';
export const NgForOf: DirectiveType<NgForOfDef<any>> = NgForOfDef as any;
export const NgIf: DirectiveType<NgIfDef> = NgIfDef as any;
@ -40,7 +40,6 @@ NgForOf.ngDirectiveDef = defineDirective({
type: NgTemplateOutletDef,
selectors: [['', 'ngTemplateOutlet', '']],
factory: () => new NgTemplateOutletDef(directiveInject(ViewContainerRef as any)),
features: [NgOnChangesFeature],
inputs:
{ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'}
});

View File

@ -8,7 +8,7 @@
import {ElementRef, QueryList} from '@angular/core';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature} from '../../src/render3/index';
import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -357,7 +357,6 @@ describe('host bindings', () => {
template: (rf: RenderFlags, ctx: InitHookComp) => {},
consts: 0,
vars: 0,
features: [NgOnChangesFeature],
hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => {
if (rf & RenderFlags.Create) {
allocHostVars(1);

View File

@ -7,7 +7,7 @@
*/
import {Inject, InjectionToken} from '../../src/core';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty, load} from '../../src/render3/index';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty} from '../../src/render3/index';
import {ComponentFixture, createComponent} from './render_util';
@ -501,8 +501,7 @@ describe('InheritDefinitionFeature', () => {
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {someInput: 'someInput'}
inputs: {someInput: 'someInput'},
});
}
@ -519,6 +518,9 @@ describe('InheritDefinitionFeature', () => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['subDir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'someInput', bind('Weee'));
}
}, 1, 0, [SubDirective]);
const fixture = new ComponentFixture(App);

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentFactoryResolver, OnDestroy, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1941,10 +1941,15 @@ describe('lifecycles', () => {
});
describe('onChanges', () => {
let events: string[];
let events: ({type: string, name: string, [key: string]: any})[];
beforeEach(() => { events = []; });
/**
* <div>
* <ng-content/>
* </div>
*/
const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
projectionDef();
@ -1953,15 +1958,20 @@ describe('lifecycles', () => {
elementEnd();
}
}, 2);
/**
* <comp [val1]="a" [publicVal2]="b"/>
*/
const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.a));
elementProperty(0, 'publicName', bind(ctx.b));
elementProperty(0, 'publicVal2', bind(ctx.b));
}
}, 1, 2, [Comp]);
const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
text(0, 'content');
@ -1974,22 +1984,28 @@ describe('lifecycles', () => {
directives: any[] = []) {
return class Component {
// @Input() val1: string;
// @Input('publicName') val2: string;
// @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) {
events.push(
`comp=${name} val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`);
ngOnChanges(changes: SimpleChanges) {
if (changes.a && this.a !== changes.a.currentValue) {
throw Error(
`SimpleChanges invalid expected this.a ${this.a} to equal currentValue ${changes.a.currentValue}`);
}
if (changes.b && this.b !== changes.b.currentValue) {
throw Error(
`SimpleChanges invalid expected this.b ${this.b} to equal currentValue ${changes.b.currentValue}`);
}
events.push({type: 'onChanges', name: 'comp - ' + name, changes});
}
static ngComponentDef = defineComponent({
type: Component,
selectors: [[name]],
factory: () => new Component(),
features: [NgOnChangesFeature],
consts: consts,
vars: vars,
inputs: {a: 'val1', b: ['publicName', 'val2']}, template,
inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template,
directives: directives
});
};
@ -1997,49 +2013,64 @@ describe('lifecycles', () => {
class Directive {
// @Input() val1: string;
// @Input('publicName') val2: string;
// @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) {
events.push(
`dir - val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`);
ngOnChanges(changes: SimpleChanges) {
events.push({type: 'onChanges', name: 'dir - dir', changes});
}
static ngDirectiveDef = defineDirective({
type: Directive,
selectors: [['', 'dir', '']],
factory: () => new Directive(),
features: [NgOnChangesFeature],
inputs: {a: 'val1', b: ['publicName', 'val2']}
inputs: {a: 'val1', b: ['publicVal2', 'val2']}
});
}
const defs = [Comp, Parent, Directive, ProjectedComp];
it('should call onChanges method after inputs are set in creation and update mode', () => {
/** <comp [val1]="val1" [publicName]="val2"></comp> */
/** <comp [val1]="val1" [publicVal2]="val2"></comp> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
elementProperty(0, 'publicVal2', bind(ctx.val2));
}
}, 1, 2, defs);
// First changes happen here.
const fixture = new ComponentFixture(App);
events = [];
fixture.component.val1 = '1';
fixture.component.val2 = 'a';
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(
undefined, '1', false), // we cleared `events` above, this is the second change
'val2': new SimpleChange(undefined, 'a', false),
}
}]);
events = [];
fixture.component.val1 = '2';
fixture.component.val2 = 'b';
fixture.update();
expect(events).toEqual(['comp=comp val1=2 val2=b - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange('1', '2', false),
'val2': new SimpleChange('a', 'b', false),
}
}]);
});
it('should call parent onChanges before child onChanges', () => {
@ -2053,28 +2084,42 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
elementProperty(0, 'publicVal2', bind(ctx.val2));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
// We're clearing events after the first change here
events = [];
fixture.component.val1 = '1';
fixture.component.val2 = 'a';
fixture.update();
expect(events).toEqual([
'comp=parent val1=1 val2=a - changed=[val1,val2]',
'comp=comp val1=1 val2=a - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, '1', false),
'val2': new SimpleChange(undefined, 'a', false),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, '1', false),
'val2': new SimpleChange(undefined, 'a', false),
}
},
]);
});
it('should call all parent onChanges across view before calling children onChanges', () => {
/**
* <parent [val]="1"></parent>
* <parent [val]="2"></parent>
*
* parent temp: <comp [val]="val"></comp>
* <parent [val1]="1"></parent>
* <parent [val1]="2"></parent>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2084,18 +2129,46 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
}
}, 2, 4, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]',
'comp=parent val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
]);
});
@ -2121,7 +2194,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
embeddedViewEnd();
}
@ -2131,19 +2204,51 @@ describe('lifecycles', () => {
}, 1, 0, defs);
const fixture = new ComponentFixture(App);
// Show the `comp` component, causing it to initialize. (first change is true)
fixture.component.condition = true;
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
// Hide the `comp` component, no onChanges should fire
fixture.component.condition = false;
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
// Show the `comp` component, it initializes again. (first change is true)
fixture.component.condition = true;
fixture.update();
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}
]);
});
@ -2161,26 +2266,40 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
}
}, 2, 4, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=projected val1=2 val2=2 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
]);
});
it('should call onChanges in host and its content children before next host', () => {
/**
* <comp [val]="1">
* <projected [val]="1"></projected>
* <comp [val1]="1" [publicVal2]="1">
* <projected [val1]="2" [publicVal2]="2"></projected>
* </comp>
* <comp [val]="2">
* <projected [val]="1"></projected>
* <comp [val1]="3" [publicVal2]="3">
* <projected [val1]="4" [publicVal2]="4"></projected>
* </comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2194,75 +2313,130 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
elementProperty(2, 'val1', bind(3));
elementProperty(2, 'publicName', bind(3));
elementProperty(2, 'publicVal2', bind(3));
elementProperty(3, 'val1', bind(4));
elementProperty(3, 'publicName', bind(4));
elementProperty(3, 'publicVal2', bind(4));
}
}, 4, 8, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=projected val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=projected val1=4 val2=4 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
]);
});
it('should be called on directives after component', () => {
/** <comp directive></comp> */
/**
* <comp dir [val1]="1" [publicVal2]="1"></comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'comp', ['dir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'dir - dir',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
]);
// Update causes no changes to be fired, since the bindings didn't change.
events = [];
fixture.update();
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
]);
expect(events).toEqual([]);
});
it('should be called on directives on an element', () => {
/** <div directive></div> */
/**
* <div dir [val]="1" [publicVal2]="1"></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', ['dir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'dir - dir',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
events = [];
fixture.update();
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([]);
});
it('should call onChanges properly in for loop', () => {
/**
* <comp [val]="1"></comp>
* <comp [val1]="1" [publicVal2]="1"></comp>
* % for (let j = 2; j < 5; j++) {
* <comp [val]="j"></comp>
* <comp [val1]="j" [publicVal2]="j"></comp>
* % }
* <comp [val]="5"></comp>
* <comp [val1]="5" [publicVal2]="5"></comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2273,9 +2447,9 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5));
elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1);
{
for (let j = 2; j < 5; j++) {
@ -2285,7 +2459,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
elementProperty(0, 'publicVal2', bind(j));
}
embeddedViewEnd();
}
@ -2299,21 +2473,56 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called
// before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=5 val2=5 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=comp val1=4 val2=4 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
]);
});
it('should call onChanges properly in for loop with children', () => {
/**
* <parent [val]="1"></parent>
* <parent [val1]="1" [publicVal2]="1"></parent>
* % for (let j = 2; j < 5; j++) {
* <parent [val]="j"></parent>
* <parent [val1]="j" [publicVal2]="j"></parent>
* % }
* <parent [val]="5"></parent>
* <parent [val1]="5" [publicVal2]="5"></parent>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2324,9 +2533,9 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5));
elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1);
{
for (let j = 2; j < 5; j++) {
@ -2336,7 +2545,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
elementProperty(0, 'publicVal2', bind(j));
}
embeddedViewEnd();
}
@ -2350,19 +2559,144 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called
// before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]',
'comp=parent val1=5 val2=5 - changed=[val1,val2]',
'comp=parent val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]',
'comp=parent val1=3 val2=3 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=parent val1=4 val2=4 - changed=[val1,val2]',
'comp=comp val1=4 val2=4 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=5 val2=5 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
]);
});
it('should not call onChanges if props are set directly', () => {
let events: SimpleChanges[] = [];
let compInstance: MyComp;
class MyComp {
value = 0;
ngOnChanges(changes: SimpleChanges) { events.push(changes); }
static ngComponentDef = defineComponent({
type: MyComp,
factory: () => {
// Capture the instance so we can test setting the property directly
compInstance = new MyComp();
return compInstance;
},
template: (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'data-a', bind(ctx.a));
}
},
selectors: [['mycomp']],
inputs: {
value: 'value',
},
consts: 1,
vars: 1,
});
}
/**
* <my-comp [value]="1"></my-comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'mycomp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'value', bind(1));
}
}, 1, 1, [MyComp]);
const fixture = new ComponentFixture(App);
events = [];
// Try setting the property directly
compInstance !.value = 2;
fixture.update();
expect(events).toEqual([]);
});
});
describe('hook order', () => {
@ -2394,7 +2728,6 @@ describe('lifecycles', () => {
consts: consts,
vars: vars,
inputs: {val: 'val'}, template,
features: [NgOnChangesFeature],
directives: directives
});
};
@ -2412,6 +2745,11 @@ describe('lifecycles', () => {
element(0, 'comp');
element(1, 'comp');
}
// This template function is a little weird in that the `elementProperty` calls
// below are directly setting values `1` and `2`, where normally there would be
// a call to `bind()` that would do the work of seeing if something changed.
// This means when `fixture.update()` is called below, ngOnChanges should fire,
// even though the *value* itself never changed.
if (rf & RenderFlags.Update) {
elementProperty(0, 'val', 1);
elementProperty(1, 'val', 2);
@ -2426,7 +2764,7 @@ describe('lifecycles', () => {
]);
events = [];
fixture.update();
fixture.update(); // Changes are made due to lack of `bind()` call in template fn.
expect(events).toEqual([
'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1',
'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2'

View File

@ -1,325 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core';
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
import {DirectiveDef, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
describe('NgOnChangesFeature', () => {
it('should patch class', () => {
class MyDirective implements OnChanges, DoCheck {
public log: Array<string|SimpleChange> = [];
public valA: string = 'initValue';
public set valB(value: string) { this.log.push(value); }
public get valB() { return 'works'; }
ngDoCheck(): void { this.log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.log.length = 0;
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']);
});
it('should inherit the behavior from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'},
});
}
class SubDirective extends SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valC: 'valC'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('should not run the parent doCheck if it is not called explicitly on super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('ERROR: Child overrides it without super call'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
ngDoCheck(): void { log.push('sub ngDoCheck'); }
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'sub ngDoCheck']);
});
it('should run the parent doCheck if it is inherited from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('super ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'super ngDoCheck']);
});
it('should apply the feature to inherited properties if on sub class', () => {
const log: any[] = [];
class SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'subDir', '']],
factory: () => new SuperDirective(),
features: [],
inputs: {valC: 'valC'},
});
}
class SubDirective extends SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'superDir', '']],
factory: () => new SubDirective(),
// Inheritance must always be before OnChanges feature.
features: [
InheritDefinitionFeature,
NgOnChangesFeature,
],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('correctly computes firstChange', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange|undefined> = [];
public valA: string = 'initValue';
// TODO(issue/24571): remove '!'.
public valB !: string;
ngOnChanges(changes: SimpleChanges): void {
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA1 = new SimpleChange(undefined, 'first', true);
const changeB1 = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]);
myDir.log.length = 0;
myDir.valA = 'third';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA2 = new SimpleChange('first', 'third', false);
expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]);
});
it('should not create a getter when only a setter is originally defined', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange> = [];
public set onlySetter(value: string) { this.log.push(value); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('onlySetter', changes['onlySetter']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {onlySetter: 'onlySetter'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.onlySetter = 'someValue';
expect(myDir.onlySetter).toBeUndefined();
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeSetter = new SimpleChange(undefined, 'someValue', true);
expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]);
});
});

View File

@ -8,7 +8,7 @@
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {ViewEncapsulation} from '../../src/metadata';
import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1631,7 +1631,6 @@ describe('ViewContainerRef', () => {
textBinding(0, interpolation1('', cmp.name, ''));
}
},
features: [NgOnChangesFeature],
inputs: {name: 'name'}
});
}
@ -1796,12 +1795,13 @@ describe('ViewContainerRef', () => {
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
expect(log).toEqual([]);
// Below will *NOT* cause onChanges to fire, because only bindings trigger onChanges
componentRef.instance.name = 'D';
log.length = 0;
fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>');
expect(log).toEqual([
'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A',
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
]);