diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 35b24224be..a4241e3404 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -32,7 +32,13 @@ export {queryRefresh} from './query'; * Enum used by the lifecycle (l) instruction to determine which lifecycle hook is requesting * processing. */ -export const enum LifecycleHook {ON_INIT = 1, ON_DESTROY = 2, ON_CHANGES = 4} +export const enum LifecycleHook { + ON_INIT = 1, + ON_DESTROY = 2, + ON_CHANGES = 4, + AFTER_VIEW_INIT = 8, + AFTER_VIEW_CHECKED = 16 +} /** * Directive (D) sets a property on all component instances using this constant as a key and the @@ -124,6 +130,9 @@ let bindingIndex: number; */ let cleanup: any[]|null; +/** Index in the data array at which view hooks begin to be stored. */ +let viewHookStartIndex: number|null; + /** * Swap the current state with a new state. * @@ -141,11 +150,9 @@ export function enterView(newViewState: ViewState, host: LElement | LView | null data = newViewState.data; bindingIndex = newViewState.bindingStartIndex || 0; ngStaticData = newViewState.ngStaticData; + creationMode = newViewState.creationMode; - if (creationMode = !data) { - // Absence of data implies creationMode. - (newViewState as{data: any[]}).data = data = []; - } + viewHookStartIndex = newViewState.viewHookStartIndex; cleanup = newViewState.cleanup; renderer = newViewState.renderer; @@ -162,7 +169,10 @@ export function enterView(newViewState: ViewState, host: LElement | LView | null * Used in lieu of enterView to make it clear when we are exiting a child view. This makes * the direction of traversal (up or down the view tree) a bit clearer. */ -export const leaveView: (newViewState: ViewState) => void = enterView as any; +export function leaveView(newViewState: ViewState): void { + executeViewHooks(); + enterView(newViewState, null); +} export function createViewState( viewId: number, renderer: Renderer3, ngStaticData: NgStaticData): ViewState { @@ -170,14 +180,16 @@ export function createViewState( parent: currentView, id: viewId, // -1 for component views node: null !, // until we initialize it in createNode. - data: null !, // Hack use as a marker for creationMode + data: [], ngStaticData: ngStaticData, cleanup: null, renderer: renderer, child: null, tail: null, next: null, - bindingStartIndex: null + bindingStartIndex: null, + creationMode: true, + viewHookStartIndex: null }; return newView; @@ -314,6 +326,7 @@ export function renderComponentOrTemplate( if (rendererFactory.end) { rendererFactory.end(); } + viewState.creationMode = false; leaveView(oldView); } } @@ -959,21 +972,61 @@ function generateInitialInputs( * * e.g. l(LifecycleHook.ON_DESTROY, ctx, ctx.onDestroy); * - * @param lifeCycle + * @param lifecycle * @param self * @param method */ -export function lifecycle(lifeCycle: LifecycleHook.ON_DESTROY, self: any, method: Function): void; -export function lifecycle(lifeCycle: LifecycleHook): boolean; -export function lifecycle(lifeCycle: LifecycleHook, self?: any, method?: Function): boolean { - if (lifeCycle === LifecycleHook.ON_INIT) { +export function lifecycle(lifecycle: LifecycleHook.ON_DESTROY, self: any, method: Function): void; +export function lifecycle( + lifecycle: LifecycleHook.AFTER_VIEW_INIT, self: any, method: Function): void; +export function lifecycle( + lifecycle: LifecycleHook.AFTER_VIEW_CHECKED, self: any, method: Function): void; +export function lifecycle(lifecycle: LifecycleHook): boolean; +export function lifecycle(lifecycle: LifecycleHook, self?: any, method?: Function): boolean { + if (lifecycle === LifecycleHook.ON_INIT) { return creationMode; - } else if (lifeCycle === LifecycleHook.ON_DESTROY) { + } else if (lifecycle === LifecycleHook.ON_DESTROY) { (cleanup || (currentView.cleanup = cleanup = [])).push(method, self); + } else if ( + creationMode && (lifecycle === LifecycleHook.AFTER_VIEW_INIT || + lifecycle === LifecycleHook.AFTER_VIEW_CHECKED)) { + if (viewHookStartIndex == null) { + currentView.viewHookStartIndex = viewHookStartIndex = data.length; + } + data.push(lifecycle, method, self); } return false; } +/** Iterates over view hook functions and calls them. */ +export function executeViewHooks(): void { + if (viewHookStartIndex == null) return; + + // Instead of using splice to remove init hooks after their first run (expensive), we + // shift over the AFTER_CHECKED hooks as we call them and truncate once at the end. + let checkIndex = viewHookStartIndex as number; + let writeIndex = checkIndex; + while (checkIndex < data.length) { + // Call lifecycle hook with its context + data[checkIndex + 1].call(data[checkIndex + 2]); + + if (data[checkIndex] === LifecycleHook.AFTER_VIEW_CHECKED) { + // We know if the writeIndex falls behind that there is an init that needs to + // be overwritten. + if (writeIndex < checkIndex) { + data[writeIndex] = data[checkIndex]; + data[writeIndex + 1] = data[checkIndex + 1]; + data[writeIndex + 2] = data[checkIndex + 2]; + } + writeIndex += 3; + } + checkIndex += 3; + } + + // Truncate once at the writeIndex + data.length = writeIndex; +} + ////////////////////////// //// ViewContainer & View @@ -1142,7 +1195,7 @@ export function viewEnd(): void { if (viewIdChanged) { insertView(container, viewNode, containerState.nextIndex - 1); - creationMode = false; + currentView.creationMode = false; } leaveView(currentView !.parent !); ngDevMode && assertEqual(isParent, false, 'isParent'); @@ -1175,6 +1228,7 @@ export const componentRefresh: try { template(directive, creationMode); } finally { + hostView.creationMode = false; leaveView(oldView); } }; diff --git a/packages/core/src/render3/interfaces.ts b/packages/core/src/render3/interfaces.ts index 4533909d0f..783a1cfdd2 100644 --- a/packages/core/src/render3/interfaces.ts +++ b/packages/core/src/render3/interfaces.ts @@ -286,6 +286,19 @@ export interface LNodeInjector { * don't have to edit the data array based on which views are present. */ export interface ViewState { + /** + * Whether or not the view is in creationMode. + * + * This must be stored in the view rather than using `data` as a marker so that + * we can properly support embedded views. Otherwise, when exiting a child view + * back into the parent view, `data` will be defined and `creationMode` will be + * improperly reported as false. + */ + creationMode: boolean; + + /** The index in the data array at which view hooks begin to be stored. */ + viewHookStartIndex: number|null; + /** * The parent view is needed when we exit the view and must restore the previous * `ViewState`. Without this, the render method would have to keep a stack of diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 54be79e75b..820d44753a 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -12,22 +12,26 @@ import {containerEl, renderToHtml} from './render_util'; describe('lifecycles', () => { + function getParentTemplate(type: any) { + return (ctx: any, cm: boolean) => { + if (cm) { + E(0, type.ngComponentDef); + { D(1, type.ngComponentDef.n(), type.ngComponentDef); } + e(); + } + p(0, 'val', b(ctx.val)); + type.ngComponentDef.h(1, 0); + type.ngComponentDef.r(1, 0); + }; + } + describe('onInit', () => { let events: string[]; beforeEach(() => { events = []; }); let Comp = createOnInitComponent('comp', (ctx: any, cm: boolean) => {}); - let Parent = createOnInitComponent('parent', (ctx: any, cm: boolean) => { - if (cm) { - E(0, Comp.ngComponentDef); - { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } - e(); - } - p(0, 'val', b(ctx.val)); - Comp.ngComponentDef.h(1, 0); - Comp.ngComponentDef.r(1, 0); - }); + let Parent = createOnInitComponent('parent', getParentTemplate(Comp)); function createOnInitComponent(name: string, template: ComponentTemplate) { return class Component { @@ -104,8 +108,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', 1); - Parent.ngComponentDef.h(1, 0); p(2, 'val', 2); + Parent.ngComponentDef.h(1, 0); Parent.ngComponentDef.h(3, 2); Parent.ngComponentDef.r(1, 0); Parent.ngComponentDef.r(3, 2); @@ -175,8 +179,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', 1); - Comp.ngComponentDef.h(1, 0); p(3, 'val', 5); + Comp.ngComponentDef.h(1, 0); Comp.ngComponentDef.h(4, 3); cR(2); { @@ -225,8 +229,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', 1); - Parent.ngComponentDef.h(1, 0); p(3, 'val', 5); + Parent.ngComponentDef.h(1, 0); Parent.ngComponentDef.h(4, 3); cR(2); { @@ -270,15 +274,7 @@ describe('lifecycles', () => { }); let Comp = createDoCheckComponent('comp', (ctx: any, cm: boolean) => {}); - let Parent = createDoCheckComponent('parent', (ctx: any, cm: boolean) => { - if (cm) { - E(0, Comp.ngComponentDef); - { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } - e(); - } - Comp.ngComponentDef.h(1, 0); - Comp.ngComponentDef.r(1, 0); - }); + let Parent = createDoCheckComponent('parent', getParentTemplate(Comp)); function createDoCheckComponent(name: string, template: ComponentTemplate) { return class Component { @@ -363,21 +359,348 @@ describe('lifecycles', () => { }); + describe('ngAfterViewInit', () => { + let events: string[]; + let allEvents: string[]; + + beforeEach(() => { + events = []; + allEvents = []; + }); + + let Comp = createAfterViewInitComponent('comp', function(ctx: any, cm: boolean) {}); + let Parent = createAfterViewInitComponent('parent', getParentTemplate(Comp)); + + function createAfterViewInitComponent(name: string, template: ComponentTemplate) { + return class Component { + val: string = ''; + ngAfterViewInit() { + events.push(`${name}${this.val}`); + allEvents.push(`${name}${this.val} init`); + } + ngAfterViewChecked() { allEvents.push(`${name}${this.val} check`); } + + static ngComponentDef = defineComponent({ + type: Component, + tag: name, + factory: () => new Component(), + refresh: (directiveIndex: number, elementIndex: number) => { + r(directiveIndex, elementIndex, template); + const comp = D(directiveIndex) as Component; + l(LifecycleHook.AFTER_VIEW_INIT, comp, comp.ngAfterViewInit); + l(LifecycleHook.AFTER_VIEW_CHECKED, comp, comp.ngAfterViewChecked); + }, + inputs: {val: 'val'}, + template: template + }); + }; + } + + it('should be called on init and not in update mode', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.r(1, 0); + } + + renderToHtml(Template, {}); + expect(events).toEqual(['comp']); + + renderToHtml(Template, {}); + expect(events).toEqual(['comp']); + }); + + it('should be called every time a view is initialized (if block)', () => { + /* + * % if (condition) { + * + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + cR(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.r(1, 0); + v(); + } + } + cr(); + } + + renderToHtml(Template, {condition: true}); + expect(events).toEqual(['comp']); + + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp']); + + renderToHtml(Template, {condition: true}); + expect(events).toEqual(['comp', 'comp']); + + }); + + it('should be called in children before parents', () => { + /** + * + * + * parent temp: + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.r(1, 0); + } + + renderToHtml(Template, {}); + expect(events).toEqual(['comp', 'parent']); + + }); + + it('should be called for entire subtree before being called in any parent view comps', () => { + /** + * + * + * + * parent temp: + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + E(2, Parent.ngComponentDef); + { D(3, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + p(0, 'val', 1); + p(2, 'val', 2); + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.h(3, 2); + Parent.ngComponentDef.r(1, 0); + Parent.ngComponentDef.r(3, 2); + } + renderToHtml(Template, {}); + expect(events).toEqual(['comp1', 'comp2', 'parent1', 'parent2']); + + }); + + it('should be called in correct order with for loops', () => { + /** + * + * % for (let i = 0; i < 4; i++) { + * + * % } + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + C(2); + c(); + E(3, Comp.ngComponentDef); + { D(4, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', 1); + p(3, 'val', 4); + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.h(4, 3); + cR(2); + { + for (let i = 2; i < 4; i++) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', i); + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.r(1, 0); + v(); + } + } + cr(); + Comp.ngComponentDef.r(1, 0); + Comp.ngComponentDef.r(4, 3); + } + + renderToHtml(Template, {}); + expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp4']); + + }); + + it('should be called in correct order with for loops with children', () => { + /** + * + * % for(let i = 0; i < 4; i++) { + * + * % } + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + C(2); + c(); + E(3, Parent.ngComponentDef); + { D(4, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + p(0, 'val', 1); + p(3, 'val', 4); + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.h(4, 3); + cR(2); + { + for (let i = 2; i < 4; i++) { + if (V(0)) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + p(0, 'val', i); + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.r(1, 0); + v(); + } + } + cr(); + Parent.ngComponentDef.r(1, 0); + Parent.ngComponentDef.r(4, 3); + } + + renderToHtml(Template, {}); + expect(events).toEqual( + ['comp2', 'parent2', 'comp3', 'parent3', 'comp1', 'comp4', 'parent1', 'parent4']); + + }); + + + describe('ngAfterViewChecked', () => { + + it('should call ngAfterViewChecked every update', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.r(1, 0); + } + + renderToHtml(Template, {}); + expect(allEvents).toEqual(['comp init', 'comp check']); + + renderToHtml(Template, {}); + expect(allEvents).toEqual(['comp init', 'comp check', 'comp check']); + }); + + it('should call ngAfterViewChecked with bindings', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b(ctx.myVal)); + Comp.ngComponentDef.h(1, 0); + Comp.ngComponentDef.r(1, 0); + } + + renderToHtml(Template, {myVal: 5}); + expect(allEvents).toEqual(['comp5 init', 'comp5 check']); + + renderToHtml(Template, {myVal: 6}); + expect(allEvents).toEqual(['comp5 init', 'comp5 check', 'comp6 check']); + }); + + it('should be called in correct order with for loops with children', () => { + /** + * + * % for(let i = 0; i < 4; i++) { + * + * % } + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + C(2); + c(); + E(3, Parent.ngComponentDef); + { D(4, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + p(0, 'val', 1); + p(3, 'val', 4); + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.h(4, 3); + cR(2); + { + for (let i = 2; i < 4; i++) { + if (V(0)) { + E(0, Parent.ngComponentDef); + { D(1, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + p(0, 'val', i); + Parent.ngComponentDef.h(1, 0); + Parent.ngComponentDef.r(1, 0); + v(); + } + } + cr(); + Parent.ngComponentDef.r(1, 0); + Parent.ngComponentDef.r(4, 3); + } + + renderToHtml(Template, {}); + expect(allEvents).toEqual([ + 'comp2 init', 'comp2 check', 'parent2 init', 'parent2 check', 'comp3 init', 'comp3 check', + 'parent3 init', 'parent3 check', 'comp1 init', 'comp1 check', 'comp4 init', 'comp4 check', + 'parent1 init', 'parent1 check', 'parent4 init', 'parent4 check' + ]); + + }); + + }); + + }); + describe('onDestroy', () => { let events: string[]; beforeEach(() => { events = []; }); let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {}); - let Parent = createOnDestroyComponent('parent', function(ctx: any, cm: boolean) { - if (cm) { - E(0, Comp.ngComponentDef); - { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } - e(); - } - Comp.ngComponentDef.h(1, 0); - Comp.ngComponentDef.r(1, 0); - }); + let Parent = createOnDestroyComponent('parent', getParentTemplate(Comp)); function createOnDestroyComponent(name: string, template: ComponentTemplate) { return class Component { @@ -456,8 +779,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', b('1')); - Comp.ngComponentDef.h(1, 0); p(2, 'val', b('2')); + Comp.ngComponentDef.h(1, 0); Comp.ngComponentDef.h(3, 2); Comp.ngComponentDef.r(1, 0); Comp.ngComponentDef.r(3, 2); @@ -584,8 +907,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', b('1')); - Comp.ngComponentDef.h(1, 0); p(3, 'val', b('3')); + Comp.ngComponentDef.h(1, 0); Comp.ngComponentDef.h(4, 3); cR(2); { @@ -665,8 +988,8 @@ describe('lifecycles', () => { e(); } p(0, 'val', b('1')); - Comp.ngComponentDef.h(1, 0); p(3, 'val', b('5')); + Comp.ngComponentDef.h(1, 0); Comp.ngComponentDef.h(4, 3); cR(2); {