fix(ivy): correct onDestroy order for projected components (#21650)

PR Close #21650
This commit is contained in:
Kara Erickson 2018-01-18 14:22:52 -08:00 committed by Misko Hevery
parent 1fe55e252c
commit 97b928053d
4 changed files with 256 additions and 110 deletions

View File

@ -630,14 +630,14 @@ export function elementEnd() {
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Element);
const query = previousOrParentNode.query;
query && query.addNode(previousOrParentNode);
saveContentHooks();
queueLifecycleHooks();
}
/**
* Loops through the directives on a node and queues their afterContentInit and
* afterContentChecked hooks, if they exist.
* Loops through the directives on a node and queues their afterContentInit,
* afterContentChecked, and onDestroy hooks, if they exist.
*/
function saveContentHooks(): void {
function queueLifecycleHooks(): void {
// It's necessary to loop through the directives at elementEnd() (rather than storing
// the hooks at creation time) so we can preserve the current hook order. All hooks
// for projected components and directives must be called *before* their hosts.
@ -648,12 +648,15 @@ function saveContentHooks(): void {
for (let i = start, end = start + size; i < end; i++) {
const instance = data[i];
if (instance.ngAfterContentInit != null) {
(contentHooks || (currentView.contentHooks = contentHooks = []))
.push(LifecycleHook.AFTER_INIT, instance.ngAfterContentInit, instance);
(contentHooks || (currentView.contentHooks = contentHooks = [
])).push(LifecycleHook.AFTER_INIT, instance.ngAfterContentInit, instance);
}
if (instance.ngAfterContentChecked != null) {
(contentHooks || (currentView.contentHooks = contentHooks = []))
.push(LifecycleHook.AFTER_CHECKED, instance.ngAfterContentChecked, instance);
(contentHooks || (currentView.contentHooks = contentHooks = [
])).push(LifecycleHook.AFTER_CHECKED, instance.ngAfterContentChecked, instance);
}
if (instance.ngOnDestroy != null) {
(cleanup || (currentView.cleanup = cleanup = [])).push(instance.ngOnDestroy, instance);
}
}
}
@ -1028,20 +1031,16 @@ function generateInitialInputs(
* @param self
* @param method
*/
export function lifecycle(lifecycle: LifecycleHook.ON_DESTROY, self: any, method: Function): void;
export function lifecycle(
lifecycle: LifecycleHook.AFTER_INIT, self: any, method: Function): void;
export function lifecycle(lifecycle: LifecycleHook.AFTER_INIT, self: any, method: Function): void;
export function lifecycle(
lifecycle: LifecycleHook.AFTER_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) {
(cleanup || (currentView.cleanup = cleanup = [])).push(method, self);
} else if (
creationMode && (lifecycle === LifecycleHook.AFTER_INIT ||
lifecycle === LifecycleHook.AFTER_CHECKED)) {
creationMode &&
(lifecycle === LifecycleHook.AFTER_INIT || lifecycle === LifecycleHook.AFTER_CHECKED)) {
if (viewHookStartIndex == null) {
currentView.viewHookStartIndex = viewHookStartIndex = data.length;
}

View File

@ -138,7 +138,7 @@ describe('content projection', () => {
});
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual('<child><div><projected-comp>content</projected-comp></div></child>');
.toEqual('<child><div><projected-comp>content</projected-comp></div></child>');
});
it('should project content with container.', () => {

View File

@ -31,14 +31,13 @@ describe('lifecycles', () => {
let Comp = createOnInitComponent('comp', (ctx: any, cm: boolean) => {
if (cm) {
m(0, pD());
E(1, 'div'); {
P(2, 0);
}
E(1, 'div');
{ P(2, 0); }
e();
}
});
let Parent = createOnInitComponent('parent', getParentTemplate(Comp));
let ProjectedComp = createOnInitComponent('projected', (ctx: any, cm: boolean)=> {
let ProjectedComp = createOnInitComponent('projected', (ctx: any, cm: boolean) => {
if (cm) {
T(0, 'content');
}
@ -429,9 +428,7 @@ describe('lifecycles', () => {
if (cm) {
m(0, pD());
E(1, Comp);
{
P(3, 0);
}
{ P(3, 0); }
e();
}
p(1, 'val', b(ctx.val));
@ -455,12 +452,8 @@ describe('lifecycles', () => {
}
ngAfterContentChecked() { allEvents.push(`${name}${this.val} check`); }
static ngComponentDef = defineComponent({
tag: name,
factory: () => new Component(),
inputs: {val: 'val'},
template: template
});
static ngComponentDef = defineComponent(
{tag: name, factory: () => new Component(), inputs: {val: 'val'}, template: template});
};
}
@ -469,9 +462,7 @@ describe('lifecycles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
Comp.ngComponentDef.h(1, 0);
@ -495,20 +486,20 @@ describe('lifecycles', () => {
if (cm) {
C(0);
}
cR(0); {
cR(0);
{
if (ctx.condition) {
if (V(0)) {
E(0, Comp);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.r(1, 0);
v();
}
} cr();
}
cr();
}
renderToHtml(Template, {condition: true});
@ -523,13 +514,9 @@ describe('lifecycles', () => {
it('should be called on directives after component', () => {
class Directive {
ngAfterContentInit() {
events.push('dir');
}
ngAfterContentInit() { events.push('dir'); }
static ngDirectiveDef = defineDirective({
factory: () => new Directive()
});
static ngDirectiveDef = defineDirective({factory: () => new Directive()});
}
function Template(ctx: any, cm: boolean) {
@ -558,9 +545,7 @@ describe('lifecycles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Parent);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
Parent.ngComponentDef.h(1, 0);
@ -581,14 +566,10 @@ describe('lifecycles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Parent);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
E(3, Parent);
{
T(5, 'content');
}
{ T(5, 'content'); }
e();
}
p(0, 'val', 1);
@ -619,16 +600,14 @@ describe('lifecycles', () => {
E(0, Parent);
{
E(2, ProjectedComp);
{
T(4, 'content');
}
{ T(4, 'content'); }
e();
}
e();
}
Parent.ngComponentDef.h(1, 0);
ProjectedComp.ngComponentDef.h(3, 2);
ProjectedComp.ngComponentDef.r(3,2);
ProjectedComp.ngComponentDef.r(3, 2);
Parent.ngComponentDef.r(1, 0);
}
@ -655,18 +634,14 @@ describe('lifecycles', () => {
E(0, Parent);
{
E(2, ProjectedComp);
{
T(4, 'content');
}
{ T(4, 'content'); }
e();
}
e();
E(5, Parent);
{
E(7, ProjectedComp);
{
T(9, 'content');
}
{ T(9, 'content'); }
e();
}
e();
@ -700,28 +675,23 @@ describe('lifecycles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
C(3);
E(4, Comp);
{
T(6, 'content');
}
{ T(6, 'content'); }
e();
}
p(0, 'val', 1);
p(4, 'val', 4);
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(5, 4);
cR(3); {
cR(3);
{
for (let i = 2; i < 4; i++) {
if (V(0)) {
E(0, Comp);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
p(0, 'val', i);
@ -729,7 +699,8 @@ describe('lifecycles', () => {
Comp.ngComponentDef.r(1, 0);
v();
}
} cr();
}
cr();
Comp.ngComponentDef.r(1, 0);
Comp.ngComponentDef.r(5, 4);
}
@ -741,28 +712,23 @@ describe('lifecycles', () => {
function ForLoopWithChildrenTemplate(ctx: any, cm: boolean) {
if (cm) {
E(0, Parent);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
C(3);
E(4, Parent);
{
T(6, 'content');
}
{ T(6, 'content'); }
e();
}
p(0, 'val', 1);
p(4, 'val', 4);
Parent.ngComponentDef.h(1, 0);
Parent.ngComponentDef.h(5, 4);
cR(3); {
cR(3);
{
for (let i = 2; i < 4; i++) {
if (V(0)) {
E(0, Parent);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
p(0, 'val', i);
@ -770,12 +736,13 @@ describe('lifecycles', () => {
Parent.ngComponentDef.r(1, 0);
v();
}
} cr();
}
cr();
Parent.ngComponentDef.r(1, 0);
Parent.ngComponentDef.r(5, 4);
}
it('should be called in correct order in a for loop with children', () =>{
it('should be called in correct order in a for loop with children', () => {
/**
* <parent [val]="1">content</parent>
* % for(let i = 2; i < 4; i++) {
@ -785,8 +752,8 @@ describe('lifecycles', () => {
*/
renderToHtml(ForLoopWithChildrenTemplate, {});
expect(events)
.toEqual(['parent2', 'comp2', 'parent3', 'comp3', 'parent1', 'parent4', 'comp1', 'comp4']);
expect(events).toEqual(
['parent2', 'comp2', 'parent3', 'comp3', 'parent1', 'parent4', 'comp1', 'comp4']);
});
describe('ngAfterContentChecked', () => {
@ -796,9 +763,7 @@ describe('lifecycles', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp);
{
T(2, 'content');
}
{ T(2, 'content'); }
e();
}
Comp.ngComponentDef.h(1, 0);
@ -828,15 +793,14 @@ describe('lifecycles', () => {
let Comp = createAfterViewInitComponent('comp', (ctx: any, cm: boolean) => {
if (cm) {
m(0, pD());
E(1, 'div'); {
P(2, 0);
}
E(1, 'div');
{ P(2, 0); }
e();
}
});
let Parent = createAfterViewInitComponent('parent', getParentTemplate(Comp));
let ProjectedComp = createAfterViewInitComponent('projected', (ctx: any, cm: boolean)=> {
let ProjectedComp = createAfterViewInitComponent('projected', (ctx: any, cm: boolean) => {
if (cm) {
T(0, 'content');
}
@ -1250,7 +1214,12 @@ describe('lifecycles', () => {
beforeEach(() => { events = []; });
let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {});
let Comp = createOnDestroyComponent('comp', (ctx: any, cm: boolean) => {
if (cm) {
m(0, pD());
P(1, 0);
}
});
let Parent = createOnDestroyComponent('parent', getParentTemplate(Comp));
function createOnDestroyComponent(name: string, template: ComponentTemplate<any>) {
@ -1258,16 +1227,8 @@ describe('lifecycles', () => {
val: string = '';
ngOnDestroy() { events.push(`${name}${this.val}`); }
static ngComponentDef = defineComponent({
tag: name,
factory: () => {
const comp = new Component();
l(LifecycleHook.ON_DESTROY, comp, comp.ngOnDestroy);
return comp;
},
inputs: {val: 'val'},
template: template
});
static ngComponentDef = defineComponent(
{tag: name, factory: () => new Component(), inputs: {val: 'val'}, template: template});
};
}
@ -1416,6 +1377,64 @@ describe('lifecycles', () => {
expect(events).toEqual(['comp', 'parent', 'grandparent']);
});
it('should be called in projected components before their hosts', () => {
const ProjectedComp = createOnDestroyComponent('projected', (ctx: any, cm: boolean) => {});
/**
* % if (showing) {
* <comp [val]="1">
* <projected-comp [val]="1"></projected-comp>
* </comp>
* <comp [val]="2">
* <projected-comp [val]="2"></projected-comp>
* </comp>
* }
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
C(0);
}
cR(0);
{
if (ctx.showing) {
if (V(0)) {
E(0, Comp);
{
E(2, ProjectedComp);
e();
}
e();
E(4, Comp);
{
E(6, ProjectedComp);
e();
}
e();
}
p(0, 'val', 1);
p(2, 'val', 1);
p(4, 'val', 2);
p(6, 'val', 2);
Comp.ngComponentDef.h(1, 0);
ProjectedComp.ngComponentDef.h(3, 2);
Comp.ngComponentDef.h(5, 4);
ProjectedComp.ngComponentDef.h(7, 6);
ProjectedComp.ngComponentDef.r(3, 2);
Comp.ngComponentDef.r(1, 0);
ProjectedComp.ngComponentDef.r(7, 6);
Comp.ngComponentDef.r(5, 4);
v();
}
}
cr();
}
renderToHtml(Template, {showing: true});
renderToHtml(Template, {showing: false});
expect(events).toEqual(['projected1', 'comp1', 'projected2', 'comp2']);
});
it('should be called in consistent order if views are removed and re-added', () => {
/**
@ -1639,4 +1658,136 @@ describe('lifecycles', () => {
});
describe('hook order', () => {
let events: string[];
beforeEach(() => { events = []; });
function createAllHooksComponent(name: string, template: ComponentTemplate<any>) {
return class Component {
val: string = '';
ngOnInit() { events.push(`init ${name}${this.val}`); }
ngDoCheck() { events.push(`check ${name}${this.val}`); }
ngAfterContentInit() { events.push(`contentInit ${name}${this.val}`); }
ngAfterContentChecked() { events.push(`contentCheck ${name}${this.val}`); }
ngAfterViewInit() { events.push(`viewInit ${name}${this.val}`); }
ngAfterViewChecked() { events.push(`viewCheck ${name}${this.val}`); }
static ngComponentDef = defineComponent({
tag: name,
factory: () => new Component(),
hostBindings: function(directiveIndex: number, elementIndex: number): void {
const comp = D(directiveIndex) as Component;
l(LifecycleHook.ON_INIT) && comp.ngOnInit();
comp.ngDoCheck();
},
refresh: function(directiveIndex: number, elementIndex: number): void {
r(directiveIndex, elementIndex, template);
const comp = D(directiveIndex) as Component;
l(LifecycleHook.AFTER_INIT, comp, comp.ngAfterViewInit);
l(LifecycleHook.AFTER_CHECKED, comp, comp.ngAfterViewChecked);
},
inputs: {val: 'val'}, template
});
};
}
it('should call all hooks in correct order', () => {
const Comp = createAllHooksComponent('comp', (ctx: any, cm: boolean) => {});
/**
* <comp [val]="1"></comp>
* <comp [val]="2"></comp>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Comp);
e();
E(2, Comp);
e();
}
p(0, 'val', 1);
p(2, 'val', 2);
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.h(3, 2);
Comp.ngComponentDef.r(1, 0);
Comp.ngComponentDef.r(3, 2);
}
renderToHtml(Template, {});
expect(events).toEqual([
'init comp1', 'check comp1', 'init comp2', 'check comp2', 'contentInit comp1',
'contentCheck comp1', 'contentInit comp2', 'contentCheck comp2', 'viewInit comp1',
'viewCheck comp1', 'viewInit comp2', 'viewCheck comp2'
]);
events = [];
renderToHtml(Template, {});
expect(events).toEqual([
'check comp1', 'check comp2', 'contentCheck comp1', 'contentCheck comp2', 'viewCheck comp1',
'viewCheck comp2'
]);
});
it('should call all hooks in correct order with children', () => {
const Comp = createAllHooksComponent('comp', (ctx: any, cm: boolean) => {});
/** <comp [val]="val"></comp> */
const Parent = createAllHooksComponent('parent', (ctx: any, cm: boolean) => {
if (cm) {
E(0, Comp);
e();
}
p(0, 'val', b(ctx.val));
Comp.ngComponentDef.h(1, 0);
Comp.ngComponentDef.r(1, 0);
});
/**
* <parent [val]="1"></parent>
* <parent [val]="2"></parent>
*/
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, Parent);
e();
E(2, Parent);
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([
'init parent1', 'check parent1', 'init parent2',
'check parent2', 'contentInit parent1', 'contentCheck parent1',
'contentInit parent2', 'contentCheck parent2', 'init comp1',
'check comp1', 'contentInit comp1', 'contentCheck comp1',
'viewInit comp1', 'viewCheck comp1', 'init comp2',
'check comp2', 'contentInit comp2', 'contentCheck comp2',
'viewInit comp2', 'viewCheck comp2', 'viewInit parent1',
'viewCheck parent1', 'viewInit parent2', 'viewCheck parent2'
]);
events = [];
renderToHtml(Template, {});
expect(events).toEqual([
'check parent1', 'check parent2', 'contentCheck parent1', 'contentCheck parent2',
'check comp1', 'contentCheck comp1', 'viewCheck comp1', 'check comp2', 'contentCheck comp2',
'viewCheck comp2', 'viewCheck parent1', 'viewCheck parent2'
]);
});
});
});

View File

@ -214,11 +214,7 @@ describe('outputs', () => {
static ngComponentDef = defineComponent({
tag: 'destroy-comp',
template: function(ctx: any, cm: boolean) {},
factory: () => {
destroyComp = new DestroyComp();
l(LifecycleHook.ON_DESTROY, destroyComp, destroyComp.ngOnDestroy);
return destroyComp;
}
factory: () => destroyComp = new DestroyComp()
});
}