feat(ivy): support lifecycle hooks of ViewContainerRef (#23396)

PR Close #23396
This commit is contained in:
Marc Laval 2018-04-12 13:49:37 +02:00 committed by Igor Minar
parent b1f040f5a2
commit 1a44a0b4a8
5 changed files with 255 additions and 27 deletions

View File

@ -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<T>(
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<T>(
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<T>(
}
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<T>(
try {
template(getRenderFlags(hostView), component);
refreshDirectives();
refreshDynamicChildren();
refreshView();
} finally {
leaveView(oldView);
}

View File

@ -132,10 +132,10 @@
"name": "refreshChildComponents"
},
{
"name": "refreshDirectives"
"name": "refreshDynamicChildren"
},
{
"name": "refreshDynamicChildren"
"name": "refreshView"
},
{
"name": "renderComponent"

View File

@ -555,10 +555,10 @@
"name": "refreshChildComponents"
},
{
"name": "refreshDirectives"
"name": "refreshDynamicChildren"
},
{
"name": "refreshDynamicChildren"
"name": "refreshView"
},
{
"name": "removeListeners"

View File

@ -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) => {});
/** <ng-content></ng-content><view [val]="val"></view> */
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]);
/**
* <parent [val]="1">
* <content [val]="1"></content>
* </parent>
* <parent [val]="2">
* <content [val]="2"></content>
* </parent>
*/
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'
]);
});
});
});

View File

@ -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('<child vcref="">**A**</child><child>**C**</child><child>**B**</child>');
.toEqual(
'<child vcref="">**A**</child><child>**C**</child><child>**C**</child><child>**B**</child>');
});
});
@ -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: `
<ng-template #foo>
<hooks [name]="'C'"></hooks>
</ng-template>
<hooks vcref [tplRef]="foo" [name]="'A'"></hooks>
<hooks [name]="'B'"></hooks>
`
})
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('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
expect(log).toEqual([]);
log.length = 0;
fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>C</hooks><hooks>B</hooks>');
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'
]);
});
});
});