diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index cfd5c97e99..8fbdaf6f2d 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -44,6 +44,7 @@ export function defineComponent(componentDefinition: ComponentDefArgs): 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): 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): 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; } diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 6689add43c..d156135911 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -37,23 +37,29 @@ export interface DirectiveDef { diPublic: ((def: DirectiveDef) => 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 { 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[]; diff --git a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts index 6840e64d9b..a3c13167d0 100644 --- a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts +++ b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts @@ -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 diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 374b9e6a4f..bb7ec6c223 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -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) { + 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', () => { + /** */ + 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 temp: + */ + + 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 temp: + */ + + 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) { + * + * % } + */ + + 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', () => { + /** + * + * + * + */ + 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', () => { + /** + * + * + * + * + * + * + */ + 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', () => { + /** */ + 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', () => { + /**
*/ + 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', () => { + /** + * + * % for (let j = 2; j < 5; j++) { + * + * % } + * + */ + + 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', () => { + /** + * + * % for (let j = 2; j < 5; j++) { + * + * % } + * + */ + + 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' ]); });