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
* 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<T>(
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);
}
};

View File

@ -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

View File

@ -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<any>) {
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<any>) {
return class Component {
@ -363,13 +359,46 @@ describe('lifecycles', () => {
});
describe('onDestroy', () => {
describe('ngAfterViewInit', () => {
let events: string[];
let allEvents: string[];
beforeEach(() => { events = []; });
beforeEach(() => {
events = [];
allEvents = [];
});
let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {});
let Parent = createOnDestroyComponent('parent', function(ctx: any, cm: boolean) {
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); }
@ -377,8 +406,302 @@ describe('lifecycles', () => {
}
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', () => {
let events: string[];
beforeEach(() => { events = []; });
let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {});
let Parent = createOnDestroyComponent('parent', getParentTemplate(Comp));
function createOnDestroyComponent(name: string, template: ComponentTemplate<any>) {
return class Component {
val: string = '';
@ -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);
{