fix(ivy): ngOnChanges to receive SimpleChanges with non minified property names as keys (#22352)

PR Close #22352
This commit is contained in:
Marc Laval 2018-02-21 18:12:02 +01:00 committed by Victor Berchet
parent f791862e52
commit 7effb0016c
4 changed files with 475 additions and 28 deletions

View File

@ -44,6 +44,7 @@ export function defineComponent<T>(componentDefinition: ComponentDefArgs<T>): Co
h: componentDefinition.hostBindings || noop,
attributes: componentDefinition.attributes || null,
inputs: invertObject(componentDefinition.inputs),
inputsPropertyName: componentDefinition.inputsPropertyName || null,
outputs: invertObject(componentDefinition.outputs),
methods: invertObject(componentDefinition.methods),
rendererType: resolveRendererType2(componentDefinition.rendererType) || null,
@ -72,10 +73,12 @@ type OnChangesExpando = OnChanges & {
export function NgOnChangesFeature(definition: DirectiveDef<any>): void {
const inputs = definition.inputs;
const proto = definition.type.prototype;
const inputsPropertyName = definition.inputsPropertyName;
// Place where we will store SimpleChanges if there is a change
Object.defineProperty(proto, PRIVATE_PREFIX, {value: undefined, writable: true});
for (let pubKey in inputs) {
const minKey = inputs[pubKey];
const propertyName = inputsPropertyName && inputsPropertyName[minKey] || pubKey;
const privateMinKey = PRIVATE_PREFIX + minKey;
// Create a place where the actual value will be stored and make it non-enumerable
Object.defineProperty(proto, privateMinKey, {value: undefined, writable: true});
@ -94,7 +97,7 @@ export function NgOnChangesFeature(definition: DirectiveDef<any>): void {
if (simpleChanges == null) {
simpleChanges = this[PRIVATE_PREFIX] = {};
}
simpleChanges[pubKey] = new SimpleChange(this[privateMinKey], value, isFirstChange);
simpleChanges[propertyName] = new SimpleChange(this[privateMinKey], value, isFirstChange);
(existingDesc && existingDesc.set) ? existingDesc.set.call(this, value) :
this[privateMinKey] = value;
}

View File

@ -37,23 +37,29 @@ export interface DirectiveDef<T> {
diPublic: ((def: DirectiveDef<any>) => void)|null;
/**
* List of inputs which are part of the components public API.
*
* The key is minified property name whereas the value is the original unminified name.
* A dictionary mapping the inputs' minified property names to their public API names, which
* are their aliases if any, or their original unminified property names
* (as in `@Input('alias') propertyName: any;`).
*/
readonly inputs: {[P in keyof T]: P};
/**
* List of outputs which are part of the components public API.
* A dictionary mapping the inputs' minified property names to the original unminified property
* names.
*
* The key is minified property name whereas the value is the original unminified name.=
* An entry is added if and only if the alias is different from the property name.
*/
readonly inputsPropertyName: {[P in keyof T]: P};
/**
* A dictionary mapping the outputs' minified property names to their public API names, which
* are their aliases if any, or their original unminified property names
* (as in `@Output('alias') propertyName: any;`).
*/
readonly outputs: {[P in keyof T]: P};
/**
* List of methods which are part of the components public API.
*
* The key is minified property name whereas the value is the original unminified name.
* A dictionary mapping the methods' minified names to their original unminified ones.
*/
readonly methods: {[P in keyof T]: P};
@ -150,6 +156,7 @@ export interface DirectiveDefArgs<T> {
factory: () => T | [T];
attributes?: string[];
inputs?: {[P in keyof T]?: string};
inputsPropertyName?: {[P in keyof T]?: string};
outputs?: {[P in keyof T]?: string};
methods?: {[P in keyof T]?: string};
features?: DirectiveDefFeature[];

View File

@ -1193,6 +1193,7 @@ describe('compiler specification', () => {
factory: function LifecycleComp_Factory() { return new LifecycleComp(); },
template: function LifecycleComp_Template(ctx: $LifecycleComp$, cm: $boolean$) {},
inputs: {nameMin: 'name'},
inputsPropertyName: {nameMin: 'nameMin'},
features: [$r3$.ɵNgOnChangesFeature]
});
// /NORMATIVE

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentTemplate, defineComponent, defineDirective} from '../../src/render3/index';
import {SimpleChanges} from '../../src/core';
import {ComponentTemplate, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
import {bind, componentRefresh, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, projection, projectionDef, store, text} from '../../src/render3/instructions';
import {containerEl, renderToHtml} from './render_util';
@ -362,7 +363,6 @@ describe('lifecycles', () => {
});
describe('doCheck', () => {
let events: string[];
let allEvents: string[];
@ -1888,6 +1888,436 @@ describe('lifecycles', () => {
});
describe('onChanges', () => {
let events: string[];
beforeEach(() => { events = []; });
const Comp = createOnChangesComponent('comp', (ctx: any, cm: boolean) => {
if (cm) {
projectionDef(0);
elementStart(1, 'div');
{ projection(2, 0); }
elementEnd();
}
});
const Parent = createOnChangesComponent('parent', (ctx: any, cm: boolean) => {
if (cm) {
elementStart(0, Comp);
elementEnd();
}
elementProperty(0, 'val1', bind(ctx.a));
elementProperty(0, 'publicName', bind(ctx.b));
Comp.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
});
const ProjectedComp = createOnChangesComponent('projected', (ctx: any, cm: boolean) => {
if (cm) {
text(0, 'content');
}
});
function createOnChangesComponent(name: string, template: ComponentTemplate<any>) {
return class Component {
// @Input() val1: string;
// @Input('publicName') 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(',')}]`);
}
static ngComponentDef = defineComponent({
type: Component,
tag: name,
factory: () => new Component(),
features: [NgOnChangesFeature],
inputs: {a: 'val1', b: 'publicName'},
inputsPropertyName: {b: 'val2'}, template
});
};
}
class Directive {
// @Input() val1: string;
// @Input('publicName') 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(',')}]`);
}
static ngDirectiveDef = defineDirective({
type: Directive,
factory: () => new Directive(),
features: [NgOnChangesFeature],
inputs: {a: 'val1', b: 'publicName'},
inputsPropertyName: {b: 'val2'}
});
}
it('should call onChanges method after inputs are set in creation and update mode', () => {
/** <comp [val1]="val1" [publicName]="val2"></comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Comp);
elementEnd();
}
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
Comp.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
}
renderToHtml(Template, {val1: '1', val2: 'a'});
expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']);
renderToHtml(Template, {val1: '2', val2: 'b'});
expect(events).toEqual([
'comp=comp val1=1 val2=a - changed=[val1,val2]',
'comp=comp val1=2 val2=b - changed=[val1,val2]'
]);
});
it('should call parent onChanges before child onChanges', () => {
/**
* <parent></parent>
* parent temp: <comp></comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Parent);
elementEnd();
}
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
Parent.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
}
renderToHtml(Template, {val1: '1', val2: 'a'});
expect(events).toEqual([
'comp=parent val1=1 val2=a - changed=[val1,val2]',
'comp=comp val1=1 val2=a - changed=[val1,val2]'
]);
});
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>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Parent);
elementEnd();
elementStart(2, Parent);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(2, 'val1', bind(2));
elementProperty(2, 'publicName', bind(2));
Parent.ngComponentDef.h(1, 0);
Parent.ngComponentDef.h(3, 2);
componentRefresh(1, 0);
componentRefresh(3, 2);
}
renderToHtml(Template, {});
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]'
]);
});
it('should call onChanges every time a new view is created (if block)', () => {
/**
* % if (condition) {
* <comp></comp>
* % }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
container(0);
}
containerRefreshStart(0);
{
if (ctx.condition) {
if (embeddedViewStart(0)) {
elementStart(0, Comp);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
Comp.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
embeddedViewEnd();
}
}
containerRefreshEnd();
}
renderToHtml(Template, {condition: true});
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
renderToHtml(Template, {condition: false});
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
renderToHtml(Template, {condition: true});
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]'
]);
});
it('should call onChanges in hosts before their content children', () => {
/**
* <comp>
* <projected-comp></projected-comp>
* </comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Comp);
{ elementStart(2, ProjectedComp); }
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(2, 'val1', bind(2));
elementProperty(2, 'publicName', bind(2));
Comp.ngComponentDef.h(1, 0);
ProjectedComp.ngComponentDef.h(3, 2);
componentRefresh(1, 0);
componentRefresh(3, 2);
}
renderToHtml(Template, {});
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=projected val1=2 val2=2 - changed=[val1,val2]'
]);
});
it('should call onChanges in host and its content children before next host', () => {
/**
* <comp [val]="1">
* <projected-comp [val]="1"></projected-comp>
* </comp>
* <comp [val]="2">
* <projected-comp [val]="1"></projected-comp>
* </comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Comp);
{ elementStart(2, ProjectedComp); }
elementEnd();
elementStart(4, Comp);
{ elementStart(6, ProjectedComp); }
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(2, 'val1', bind(2));
elementProperty(2, 'publicName', bind(2));
elementProperty(4, 'val1', bind(3));
elementProperty(4, 'publicName', bind(3));
elementProperty(6, 'val1', bind(4));
elementProperty(6, 'publicName', bind(4));
Comp.ngComponentDef.h(1, 0);
ProjectedComp.ngComponentDef.h(3, 2);
Comp.ngComponentDef.h(5, 4);
ProjectedComp.ngComponentDef.h(7, 6);
componentRefresh(1, 0);
componentRefresh(3, 2);
componentRefresh(5, 4);
componentRefresh(7, 6);
}
renderToHtml(Template, {});
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]'
]);
});
it('should be called on directives after component', () => {
/** <comp directive></comp> */
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Comp, null, [Directive]);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
Comp.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
}
renderToHtml(Template, {});
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
]);
renderToHtml(Template, {});
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
]);
});
it('should be called on directives on an element', () => {
/** <div directive></div> */
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, 'div', null, [Directive]);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
Directive.ngDirectiveDef.h(1, 0);
componentRefresh(1, 0);
}
renderToHtml(Template, {});
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
renderToHtml(Template, {});
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
});
it('should call onChanges properly in for loop', () => {
/**
* <comp [val]="1"></comp>
* % for (let j = 2; j < 5; j++) {
* <comp [val]="j"></comp>
* % }
* <comp [val]="5"></comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Comp);
elementEnd();
container(2);
elementStart(3, Comp);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(3, 'val1', bind(5));
elementProperty(3, 'publicName', bind(5));
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(4, 3);
containerRefreshStart(2);
{
for (let j = 2; j < 5; j++) {
if (embeddedViewStart(0)) {
elementStart(0, Comp);
elementEnd();
}
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
Comp.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
embeddedViewEnd();
}
}
containerRefreshEnd();
componentRefresh(1, 0);
componentRefresh(4, 3);
}
renderToHtml(Template, {});
// 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]'
]);
});
it('should call onChanges properly in for loop with children', () => {
/**
* <parent [val]="1"></parent>
* % for (let j = 2; j < 5; j++) {
* <parent [val]="j"></parent>
* % }
* <parent [val]="5"></parent>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, Parent);
elementEnd();
container(2);
elementStart(3, Parent);
elementEnd();
}
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(3, 'val1', bind(5));
elementProperty(3, 'publicName', bind(5));
Parent.ngComponentDef.h(1, 0);
Parent.ngComponentDef.h(4, 3);
containerRefreshStart(2);
{
for (let j = 2; j < 5; j++) {
if (embeddedViewStart(0)) {
elementStart(0, Parent);
elementEnd();
}
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
Parent.ngComponentDef.h(1, 0);
componentRefresh(1, 0);
embeddedViewEnd();
}
}
containerRefreshEnd();
componentRefresh(1, 0);
componentRefresh(4, 3);
}
renderToHtml(Template, {});
// 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]'
]);
});
});
describe('hook order', () => {
let events: string[];
@ -1897,6 +2327,8 @@ describe('lifecycles', () => {
return class Component {
val: string = '';
ngOnChanges() { events.push(`changes ${name}${this.val}`); }
ngOnInit() { events.push(`init ${name}${this.val}`); }
ngDoCheck() { events.push(`check ${name}${this.val}`); }
@ -1910,7 +2342,8 @@ describe('lifecycles', () => {
type: Component,
tag: name,
factory: () => new Component(),
inputs: {val: 'val'}, template
inputs: {val: 'val'}, template,
features: [NgOnChangesFeature]
});
};
}
@ -1940,16 +2373,16 @@ describe('lifecycles', () => {
renderToHtml(Template, {});
expect(events).toEqual([
'init comp1', 'check comp1', 'init comp2', 'check comp2', 'contentInit comp1',
'contentCheck comp1', 'contentInit comp2', 'contentCheck comp2', 'viewInit comp1',
'viewCheck comp1', 'viewInit comp2', 'viewCheck comp2'
'changes comp1', 'init comp1', 'check comp1', 'changes comp2', 'init comp2', 'check comp2',
'contentInit comp1', 'contentCheck comp1', 'contentInit comp2', 'contentCheck comp2',
'viewInit comp1', 'viewCheck comp1', 'viewInit comp2', 'viewCheck comp2'
]);
events = [];
renderToHtml(Template, {});
expect(events).toEqual([
'check comp1', 'check comp2', 'contentCheck comp1', 'contentCheck comp2', 'viewCheck comp1',
'viewCheck comp2'
'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1',
'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2'
]);
});
@ -1988,22 +2421,25 @@ describe('lifecycles', () => {
renderToHtml(Template, {});
expect(events).toEqual([
'init parent1', 'check parent1', 'init parent2',
'check parent2', 'contentInit parent1', 'contentCheck parent1',
'contentInit parent2', 'contentCheck parent2', 'init comp1',
'check comp1', 'contentInit comp1', 'contentCheck comp1',
'viewInit comp1', 'viewCheck comp1', 'init comp2',
'check comp2', 'contentInit comp2', 'contentCheck comp2',
'viewInit comp2', 'viewCheck comp2', 'viewInit parent1',
'viewCheck parent1', 'viewInit parent2', 'viewCheck parent2'
'changes parent1', 'init parent1', 'check parent1',
'changes parent2', 'init parent2', 'check parent2',
'contentInit parent1', 'contentCheck parent1', 'contentInit parent2',
'contentCheck parent2', 'changes comp1', 'init comp1',
'check comp1', 'contentInit comp1', 'contentCheck comp1',
'viewInit comp1', 'viewCheck comp1', 'changes comp2',
'init comp2', 'check comp2', 'contentInit comp2',
'contentCheck comp2', 'viewInit comp2', 'viewCheck comp2',
'viewInit parent1', 'viewCheck parent1', 'viewInit parent2',
'viewCheck parent2'
]);
events = [];
renderToHtml(Template, {});
expect(events).toEqual([
'check parent1', 'check parent2', 'contentCheck parent1', 'contentCheck parent2',
'check comp1', 'contentCheck comp1', 'viewCheck comp1', 'check comp2', 'contentCheck comp2',
'viewCheck comp2', 'viewCheck parent1', 'viewCheck parent2'
'changes parent1', 'check parent1', 'changes parent2', 'check parent2',
'contentCheck parent1', 'contentCheck parent2', 'check comp1', 'contentCheck comp1',
'viewCheck comp1', 'check comp2', 'contentCheck comp2', 'viewCheck comp2',
'viewCheck parent1', 'viewCheck parent2'
]);
});