feat(core): add ngAfterViewInit and ngAfterViewChecked support to render3 (#21266)

PR Close #21266
This commit is contained in:
Kara Erickson 2017-12-22 16:41:34 -08:00
parent 229b76cfde
commit 3db91ffd96
3 changed files with 439 additions and 49 deletions

View File

@ -32,7 +32,13 @@ export {queryRefresh} from './query';
* Enum used by the lifecycle (l) instruction to determine which lifecycle hook is requesting * Enum used by the lifecycle (l) instruction to determine which lifecycle hook is requesting
* processing. * 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 * 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; 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. * Swap the current state with a new state.
* *
@ -141,11 +150,9 @@ export function enterView(newViewState: ViewState, host: LElement | LView | null
data = newViewState.data; data = newViewState.data;
bindingIndex = newViewState.bindingStartIndex || 0; bindingIndex = newViewState.bindingStartIndex || 0;
ngStaticData = newViewState.ngStaticData; ngStaticData = newViewState.ngStaticData;
creationMode = newViewState.creationMode;
if (creationMode = !data) { viewHookStartIndex = newViewState.viewHookStartIndex;
// Absence of data implies creationMode.
(newViewState as{data: any[]}).data = data = [];
}
cleanup = newViewState.cleanup; cleanup = newViewState.cleanup;
renderer = newViewState.renderer; 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 * 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. * 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( export function createViewState(
viewId: number, renderer: Renderer3, ngStaticData: NgStaticData): ViewState { viewId: number, renderer: Renderer3, ngStaticData: NgStaticData): ViewState {
@ -170,14 +180,16 @@ export function createViewState(
parent: currentView, parent: currentView,
id: viewId, // -1 for component views id: viewId, // -1 for component views
node: null !, // until we initialize it in createNode. node: null !, // until we initialize it in createNode.
data: null !, // Hack use as a marker for creationMode data: [],
ngStaticData: ngStaticData, ngStaticData: ngStaticData,
cleanup: null, cleanup: null,
renderer: renderer, renderer: renderer,
child: null, child: null,
tail: null, tail: null,
next: null, next: null,
bindingStartIndex: null bindingStartIndex: null,
creationMode: true,
viewHookStartIndex: null
}; };
return newView; return newView;
@ -314,6 +326,7 @@ export function renderComponentOrTemplate<T>(
if (rendererFactory.end) { if (rendererFactory.end) {
rendererFactory.end(); rendererFactory.end();
} }
viewState.creationMode = false;
leaveView(oldView); leaveView(oldView);
} }
} }
@ -959,21 +972,61 @@ function generateInitialInputs(
* *
* e.g. l(LifecycleHook.ON_DESTROY, ctx, ctx.onDestroy); * e.g. l(LifecycleHook.ON_DESTROY, ctx, ctx.onDestroy);
* *
* @param lifeCycle * @param lifecycle
* @param self * @param self
* @param method * @param method
*/ */
export function lifecycle(lifeCycle: LifecycleHook.ON_DESTROY, self: any, method: Function): void; export function lifecycle(lifecycle: LifecycleHook.ON_DESTROY, self: any, method: Function): void;
export function lifecycle(lifeCycle: LifecycleHook): boolean; export function lifecycle(
export function lifecycle(lifeCycle: LifecycleHook, self?: any, method?: Function): boolean { lifecycle: LifecycleHook.AFTER_VIEW_INIT, self: any, method: Function): void;
if (lifeCycle === LifecycleHook.ON_INIT) { 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; return creationMode;
} else if (lifeCycle === LifecycleHook.ON_DESTROY) { } else if (lifecycle === LifecycleHook.ON_DESTROY) {
(cleanup || (currentView.cleanup = cleanup = [])).push(method, self); (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; 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 //// ViewContainer & View
@ -1142,7 +1195,7 @@ export function viewEnd(): void {
if (viewIdChanged) { if (viewIdChanged) {
insertView(container, viewNode, containerState.nextIndex - 1); insertView(container, viewNode, containerState.nextIndex - 1);
creationMode = false; currentView.creationMode = false;
} }
leaveView(currentView !.parent !); leaveView(currentView !.parent !);
ngDevMode && assertEqual(isParent, false, 'isParent'); ngDevMode && assertEqual(isParent, false, 'isParent');
@ -1175,6 +1228,7 @@ export const componentRefresh:
try { try {
template(directive, creationMode); template(directive, creationMode);
} finally { } finally {
hostView.creationMode = false;
leaveView(oldView); leaveView(oldView);
} }
}; };

View File

@ -286,6 +286,19 @@ export interface LNodeInjector {
* don't have to edit the data array based on which views are present. * don't have to edit the data array based on which views are present.
*/ */
export interface ViewState { 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 * 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 * `ViewState`. Without this, the render method would have to keep a stack of

View File

@ -12,22 +12,26 @@ import {containerEl, renderToHtml} from './render_util';
describe('lifecycles', () => { 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', () => { describe('onInit', () => {
let events: string[]; let events: string[];
beforeEach(() => { events = []; }); beforeEach(() => { events = []; });
let Comp = createOnInitComponent('comp', (ctx: any, cm: boolean) => {}); let Comp = createOnInitComponent('comp', (ctx: any, cm: boolean) => {});
let Parent = createOnInitComponent('parent', (ctx: any, cm: boolean) => { let Parent = createOnInitComponent('parent', getParentTemplate(Comp));
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);
});
function createOnInitComponent(name: string, template: ComponentTemplate<any>) { function createOnInitComponent(name: string, template: ComponentTemplate<any>) {
return class Component { return class Component {
@ -104,8 +108,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', 1); p(0, 'val', 1);
Parent.ngComponentDef.h(1, 0);
p(2, 'val', 2); p(2, 'val', 2);
Parent.ngComponentDef.h(1, 0);
Parent.ngComponentDef.h(3, 2); Parent.ngComponentDef.h(3, 2);
Parent.ngComponentDef.r(1, 0); Parent.ngComponentDef.r(1, 0);
Parent.ngComponentDef.r(3, 2); Parent.ngComponentDef.r(3, 2);
@ -175,8 +179,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', 1); p(0, 'val', 1);
Comp.ngComponentDef.h(1, 0);
p(3, 'val', 5); p(3, 'val', 5);
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(4, 3); Comp.ngComponentDef.h(4, 3);
cR(2); cR(2);
{ {
@ -225,8 +229,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', 1); p(0, 'val', 1);
Parent.ngComponentDef.h(1, 0);
p(3, 'val', 5); p(3, 'val', 5);
Parent.ngComponentDef.h(1, 0);
Parent.ngComponentDef.h(4, 3); Parent.ngComponentDef.h(4, 3);
cR(2); cR(2);
{ {
@ -270,15 +274,7 @@ describe('lifecycles', () => {
}); });
let Comp = createDoCheckComponent('comp', (ctx: any, cm: boolean) => {}); let Comp = createDoCheckComponent('comp', (ctx: any, cm: boolean) => {});
let Parent = createDoCheckComponent('parent', (ctx: any, cm: boolean) => { let Parent = createDoCheckComponent('parent', getParentTemplate(Comp));
if (cm) {
E(0, Comp.ngComponentDef);
{ D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.r(1, 0);
});
function createDoCheckComponent(name: string, template: ComponentTemplate<any>) { function createDoCheckComponent(name: string, template: ComponentTemplate<any>) {
return class Component { 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<any>) {
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', () => {
/** <comp></comp> */
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) {
* <comp></comp>
* % }
*/
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></parent>
*
* parent temp: <comp></comp>
*/
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 [val]="1"></parent>
* <parent [val]="2"></parent>
*
* parent temp: <comp [val]="val"></comp>
*/
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', () => {
/**
* <comp [val]="1"></comp>
* % for (let i = 0; i < 4; i++) {
* <comp [val]="i"></comp>
* % }
* <comp [val]="4"></comp>
*/
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', () => {
/**
* <parent [val]="1"></parent>
* % for(let i = 0; i < 4; i++) {
* <parent [val]="i"></parent>
* % }
* <parent [val]="4"></parent>
*/
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', () => {
/** <comp></comp> */
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', () => {
/** <comp [val]="myVal"></comp> */
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', () => {
/**
* <parent [val]="1"></parent>
* % for(let i = 0; i < 4; i++) {
* <parent [val]="i"></parent>
* % }
* <parent [val]="4"></parent>
*/
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', () => { describe('onDestroy', () => {
let events: string[]; let events: string[];
beforeEach(() => { events = []; }); beforeEach(() => { events = []; });
let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {}); let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {});
let Parent = createOnDestroyComponent('parent', function(ctx: any, cm: boolean) { let Parent = createOnDestroyComponent('parent', getParentTemplate(Comp));
if (cm) {
E(0, Comp.ngComponentDef);
{ D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); }
e();
}
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.r(1, 0);
});
function createOnDestroyComponent(name: string, template: ComponentTemplate<any>) { function createOnDestroyComponent(name: string, template: ComponentTemplate<any>) {
return class Component { return class Component {
@ -456,8 +779,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', b('1')); p(0, 'val', b('1'));
Comp.ngComponentDef.h(1, 0);
p(2, 'val', b('2')); p(2, 'val', b('2'));
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(3, 2); Comp.ngComponentDef.h(3, 2);
Comp.ngComponentDef.r(1, 0); Comp.ngComponentDef.r(1, 0);
Comp.ngComponentDef.r(3, 2); Comp.ngComponentDef.r(3, 2);
@ -584,8 +907,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', b('1')); p(0, 'val', b('1'));
Comp.ngComponentDef.h(1, 0);
p(3, 'val', b('3')); p(3, 'val', b('3'));
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(4, 3); Comp.ngComponentDef.h(4, 3);
cR(2); cR(2);
{ {
@ -665,8 +988,8 @@ describe('lifecycles', () => {
e(); e();
} }
p(0, 'val', b('1')); p(0, 'val', b('1'));
Comp.ngComponentDef.h(1, 0);
p(3, 'val', b('5')); p(3, 'val', b('5'));
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(4, 3); Comp.ngComponentDef.h(4, 3);
cR(2); cR(2);
{ {