From f39b57257de00ee1be3046e5db906edc9208e9e1 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 12 Apr 2018 13:49:37 +0200 Subject: [PATCH 1/4] feat(ivy): support lifecycle hooks of ViewContainerRef --- packages/core/src/render3/instructions.ts | 49 ++++---- .../hello_world/bundle.golden_symbols.json | 4 +- packages/core/test/render3/lifecycle_spec.ts | 84 +++++++++++++ .../test/render3/view_container_ref_spec.ts | 119 +++++++++++++++++- 4 files changed, 232 insertions(+), 24 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 1c6d4a06e090e..6903d397e4e8a 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -225,23 +225,32 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | 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 function leaveView(newView: LView): void { - if (!checkNoChangesMode) { - executeHooks( - directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, creationMode); +export function leaveView(newView: LView, creationOnly?: boolean): void { + if (!creationOnly) { + if (!checkNoChangesMode) { + executeHooks( + directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, + creationMode); + } + // Views should be clean and in update mode after being checked, so these bits are cleared + currentView.flags &= ~(LViewFlags.CreationMode | LViewFlags.Dirty); } - // Views should be clean and in update mode after being checked, so these bits are cleared - currentView.flags &= ~(LViewFlags.CreationMode | LViewFlags.Dirty); currentView.lifecycleStage = LifecycleStage.Init; currentView.bindingIndex = -1; enterView(newView, null); } -/** Refreshes directives in this view and triggers any init/content hooks. */ -function refreshDirectives() { - executeInitAndContentHooks(); - +/** Refreshes the view: dynamic children and directives, triggering any init/content hooks. */ +function refreshView() { const tView = currentView.tView; + if (!checkNoChangesMode) { + executeInitHooks(currentView, tView, creationMode); + } + refreshDynamicChildren(); + if (!checkNoChangesMode) { + executeHooks(directives !, tView.contentHooks, tView.contentCheckHooks, creationMode); + } + // This needs to be set before children are processed to support recursive components tView.firstTemplatePass = firstTemplatePass = false; @@ -456,10 +465,11 @@ export function renderEmbeddedTemplate( const _isParent = isParent; const _previousOrParentNode = previousOrParentNode; let oldView: LView; + let rf: RenderFlags = RenderFlags.Update; try { isParent = true; previousOrParentNode = null !; - let rf: RenderFlags = RenderFlags.Update; + if (viewNode == null) { const tView = getOrCreateTView(template, directives || null, pipes || null); const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways); @@ -468,13 +478,12 @@ export function renderEmbeddedTemplate( rf = RenderFlags.Create; } oldView = enterView(viewNode.data, viewNode); - template(rf, context); - refreshDirectives(); - refreshDynamicChildren(); - + if (rf & RenderFlags.Update) { + refreshView(); + } } finally { - leaveView(oldView !); + leaveView(oldView !, (rf & RenderFlags.Create) === RenderFlags.Create); isParent = _isParent; previousOrParentNode = _previousOrParentNode; } @@ -490,8 +499,7 @@ export function renderComponentOrTemplate( } if (template) { template(getRenderFlags(hostView), componentOrContext !); - refreshDynamicChildren(); - refreshDirectives(); + refreshView(); } else { executeInitAndContentHooks(); @@ -1557,7 +1565,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV /** Marks the end of an embedded view. */ export function embeddedViewEnd(): void { - refreshDirectives(); + refreshView(); isParent = false; const viewNode = previousOrParentNode = currentView.node as LViewNode; const containerNode = previousOrParentNode.parent as LContainerNode; @@ -1968,8 +1976,7 @@ export function detectChangesInternal( try { template(getRenderFlags(hostView), component); - refreshDirectives(); - refreshDynamicChildren(); + refreshView(); } finally { leaveView(oldView); } diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 5ee8c67c0d1e4..a0ad181105641 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -132,10 +132,10 @@ "name": "refreshChildComponents" }, { - "name": "refreshDirectives" + "name": "refreshDynamicChildren" }, { - "name": "refreshDynamicChildren" + "name": "refreshView" }, { "name": "renderComponent" diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 3ac61cf785c90..2c9ef5110e2c6 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -2436,6 +2436,90 @@ describe('lifecycles', () => { }); + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng + it('should call all hooks in correct order with view and content', () => { + const Content = createAllHooksComponent('content', (rf: RenderFlags, ctx: any) => {}); + + const View = createAllHooksComponent('view', (rf: RenderFlags, ctx: any) => {}); + + /** */ + const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + projectionDef(0); + projection(1, 0); + elementStart(2, 'view'); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(2, 'val', bind(ctx.val)); + } + }, [View]); + + /** + * + * + * + * + * + * + */ + function Template(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'parent'); + elementStart(1, 'content'); + elementEnd(); + elementEnd(); + elementStart(2, 'parent'); + elementStart(3, 'content'); + elementEnd(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'val', bind(1)); + elementProperty(1, 'val', bind(1)); + elementProperty(2, 'val', bind(2)); + elementProperty(3, 'val', bind(2)); + } + } + + const defs = [Parent, Content]; + renderToHtml(Template, {}, defs); + expect(events).toEqual([ + 'changes parent1', 'init parent1', + 'check parent1', 'changes content1', + 'init content1', 'check content1', + 'changes parent2', 'init parent2', + 'check parent2', 'changes content2', + 'init content2', 'check content2', + 'contentInit content1', 'contentCheck content1', + 'contentInit parent1', 'contentCheck parent1', + 'contentInit content2', 'contentCheck content2', + 'contentInit parent2', 'contentCheck parent2', + 'changes view1', 'init view1', + 'check view1', 'contentInit view1', + 'contentCheck view1', 'viewInit view1', + 'viewCheck view1', 'changes view2', + 'init view2', 'check view2', + 'contentInit view2', 'contentCheck view2', + 'viewInit view2', 'viewCheck view2', + 'viewInit content1', 'viewCheck content1', + 'viewInit parent1', 'viewCheck parent1', + 'viewInit content2', 'viewCheck content2', + 'viewInit parent2', 'viewCheck parent2' + ]); + + events = []; + renderToHtml(Template, {}, defs); + expect(events).toEqual([ + 'check parent1', 'check content1', 'check parent2', 'check content2', + 'contentCheck content1', 'contentCheck parent1', 'contentCheck content2', + 'contentCheck parent2', 'check view1', 'contentCheck view1', 'viewCheck view1', + 'check view2', 'contentCheck view2', 'viewCheck view2', 'viewCheck content1', + 'viewCheck parent1', 'viewCheck content2', 'viewCheck parent2' + ]); + + }); + }); }); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index dda5e608854ec..64240da030112 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -8,7 +8,7 @@ import {Component, Directive, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '../../src/core'; import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di'; -import {defineComponent, defineDirective, definePipe, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; +import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {pipe, pipeBind1} from '../../src/render3/pipe'; @@ -822,4 +822,121 @@ describe('ViewContainerRef', () => { }); }); }); + + describe('life cycle hooks', () => { + + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref + const log: string[] = []; + it('should call all hooks in correct order', () => { + @Component({selector: 'hooks', template: `{{name}}`}) + class ComponentWithHooks { + name: string; + + private log(msg: string) { log.push(msg); } + + ngOnChanges() { this.log('onChanges-' + this.name); } + ngOnInit() { this.log('onInit-' + this.name); } + ngDoCheck() { this.log('doCheck-' + this.name); } + + ngAfterContentInit() { this.log('afterContentInit-' + this.name); } + ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } + + ngAfterViewInit() { this.log('afterViewInit-' + this.name); } + ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } + + static ngComponentDef = defineComponent({ + type: ComponentWithHooks, + selectors: [['hooks']], + factory: () => new ComponentWithHooks(), + template: (rf: RenderFlags, cmp: ComponentWithHooks) => { + if (rf & RenderFlags.Create) { + text(0); + } + if (rf & RenderFlags.Update) { + textBinding(0, interpolation1('', cmp.name, '')); + } + }, + features: [NgOnChangesFeature()], + inputs: {name: 'name'} + }); + } + + @Component({ + template: ` + + + + + + ` + }) + class SomeComponent { + static ngComponentDef = defineComponent({ + type: SomeComponent, + selectors: [['some-comp']], + factory: () => new SomeComponent(), + template: (rf: RenderFlags, cmp: SomeComponent) => { + if (rf & RenderFlags.Create) { + container(0, (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + elementStart(0, 'hooks'); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'name', bind('C')); + } + }); + elementStart(1, 'hooks', ['vcref', '']); + elementEnd(); + elementStart(2, 'hooks'); + elementEnd(); + } + if (rf & RenderFlags.Update) { + const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); + elementProperty(1, 'tplRef', bind(tplRef)); + elementProperty(1, 'name', bind('A')); + elementProperty(2, 'name', bind('B')); + } + }, + directives: [ComponentWithHooks, DirectiveWithVCRef] + }); + } + + const fixture = new ComponentFixture(SomeComponent); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); + expect(fixture.html).toEqual('AB'); + expect(log).toEqual([]); + + log.length = 0; + fixture.update(); + expect(fixture.html).toEqual('ACB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C', + 'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.update(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); + }); }); From c45d28c4cf405d3eb597661b65016cceab6dfc83 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Wed, 18 Apr 2018 16:59:56 +0200 Subject: [PATCH 2/4] fixup! feat(ivy): support lifecycle hooks of ViewContainerRef --- packages/core/src/render3/instructions.ts | 2 ++ packages/core/test/bundling/todo/bundle.golden_symbols.json | 4 ++-- packages/core/test/render3/view_container_ref_spec.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 6903d397e4e8a..c953a835110de 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -481,6 +481,8 @@ export function renderEmbeddedTemplate( template(rf, context); if (rf & RenderFlags.Update) { refreshView(); + } else { + viewNode.data.tView.firstTemplatePass = firstTemplatePass = false; } } finally { leaveView(oldView !, (rf & RenderFlags.Create) === RenderFlags.Create); diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index d2f31dfa1f871..38a58d3a1817e 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -555,10 +555,10 @@ "name": "refreshChildComponents" }, { - "name": "refreshDirectives" + "name": "refreshDynamicChildren" }, { - "name": "refreshDynamicChildren" + "name": "refreshView" }, { "name": "removeListeners" diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 64240da030112..98b3781da7641 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -414,9 +414,11 @@ describe('ViewContainerRef', () => { const fixture = new ComponentFixture(SomeComponent); directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); + directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); fixture.update(); expect(fixture.html) - .toEqual('**A****C****B**'); + .toEqual( + '**A****C****C****B**'); }); }); From c4605703ac7401e7f4c1ae42dba0b8924210c92b Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 19 Apr 2018 11:58:22 +0200 Subject: [PATCH 3/4] fixup! feat(ivy): support lifecycle hooks of ViewContainerRef --- packages/core/src/render3/instructions.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index c953a835110de..f6f96f426c5e9 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -224,6 +224,11 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | 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. + * + * @param newView New state to become active + * @param creationOnly An optional boolean to indicate that the view was processed in creation mode + * only, i.e. the first update will be done later + * @returns the previous state */ export function leaveView(newView: LView, creationOnly?: boolean): void { if (!creationOnly) { @@ -232,7 +237,7 @@ export function leaveView(newView: LView, creationOnly?: boolean): void { directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, creationMode); } - // Views should be clean and in update mode after being checked, so these bits are cleared + // Views are clean and in update mode after being checked, so these bits are cleared currentView.flags &= ~(LViewFlags.CreationMode | LViewFlags.Dirty); } currentView.lifecycleStage = LifecycleStage.Init; @@ -240,7 +245,12 @@ export function leaveView(newView: LView, creationOnly?: boolean): void { enterView(newView, null); } -/** Refreshes the view: dynamic children and directives, triggering any init/content hooks. */ +/** + * Refreshes the view, executing the following steps in that order: + * triggers init hooks, refreshes dynamic children, triggers content hooks, sets host bindings, + * refreshes child components. + * Note: view hooks are triggered later when leaving the view. + * */ function refreshView() { const tView = currentView.tView; if (!checkNoChangesMode) { @@ -485,7 +495,10 @@ export function renderEmbeddedTemplate( viewNode.data.tView.firstTemplatePass = firstTemplatePass = false; } } finally { - leaveView(oldView !, (rf & RenderFlags.Create) === RenderFlags.Create); + // renderEmbeddedTemplate() is called twice in fact, once for creation only and then once for + // update. When for creation only, leaveView() must not trigger view hooks, nor clean flags. + const isCreationOnly = (rf & RenderFlags.Create) === RenderFlags.Create; + leaveView(oldView !, isCreationOnly); isParent = _isParent; previousOrParentNode = _previousOrParentNode; } From 8dc9705a5a0c6111481110e654234ad0f095ca73 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Mon, 23 Apr 2018 16:54:15 -0700 Subject: [PATCH 4/4] fixup! feat(ivy): support lifecycle hooks of ViewContainerRef --- packages/core/src/render3/instructions.ts | 3 +-- packages/core/test/render3/lifecycle_spec.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index f6f96f426c5e9..dcddb6588cf2a 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -227,8 +227,7 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null) * * @param newView New state to become active * @param creationOnly An optional boolean to indicate that the view was processed in creation mode - * only, i.e. the first update will be done later - * @returns the previous state + * only, i.e. the first update will be done later. Only possible for dynamically created views. */ export function leaveView(newView: LView, creationOnly?: boolean): void { if (!creationOnly) { diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 2c9ef5110e2c6..3a190ccd0a895 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -2466,12 +2466,16 @@ describe('lifecycles', () => { function Template(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'parent'); - elementStart(1, 'content'); - elementEnd(); + { + elementStart(1, 'content'); + elementEnd(); + } elementEnd(); elementStart(2, 'parent'); - elementStart(3, 'content'); - elementEnd(); + { + elementStart(3, 'content'); + elementEnd(); + } elementEnd(); } if (rf & RenderFlags.Update) {