diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 1c6d4a06e0..dcddb6588c 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -224,24 +224,42 @@ 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. Only possible for dynamically created views. */ -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 are 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, 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) { + 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 +474,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 +487,17 @@ export function renderEmbeddedTemplate( rf = RenderFlags.Create; } oldView = enterView(viewNode.data, viewNode); - template(rf, context); - refreshDirectives(); - refreshDynamicChildren(); - + if (rf & RenderFlags.Update) { + refreshView(); + } else { + viewNode.data.tView.firstTemplatePass = firstTemplatePass = false; + } } finally { - leaveView(oldView !); + // 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; } @@ -490,8 +513,7 @@ export function renderComponentOrTemplate( } if (template) { template(getRenderFlags(hostView), componentOrContext !); - refreshDynamicChildren(); - refreshDirectives(); + refreshView(); } else { executeInitAndContentHooks(); @@ -1557,7 +1579,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 +1990,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 5ee8c67c0d..a0ad181105 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/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 4aff404156..b45915deb6 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/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 3ac61cf785..3a190ccd0a 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -2436,6 +2436,94 @@ 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 dda5e60885..98b3781da7 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'; @@ -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**'); }); }); @@ -822,4 +824,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' + ]); + }); + }); });