fix(ivy): properly destroy views created by ComponentFactory (#27676)

PR Close #27676
This commit is contained in:
Pawel Kozlowski 2018-12-14 15:11:14 +01:00 committed by Miško Hevery
parent f1c9d6a81f
commit b00aeeff37
5 changed files with 83 additions and 89 deletions

View File

@ -22,11 +22,10 @@ import {assertComponentType, assertDefined} from './assert';
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component'; import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
import {getComponentDef} from './definition'; import {getComponentDef} from './definition';
import {NodeInjector} from './di'; import {NodeInjector} from './di';
import {createLView, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions'; import {addToViewTree, createLView, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions';
import {ComponentDef, RenderFlags} from './interfaces/definition'; import {ComponentDef, RenderFlags} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node'; import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
import {RElement, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {RElement, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
import {HEADER_OFFSET, LView, LViewFlags, RootContext, TVIEW} from './interfaces/view'; import {HEADER_OFFSET, LView, LViewFlags, RootContext, TVIEW} from './interfaces/view';
import {enterView, leaveView} from './state'; import {enterView, leaveView} from './state';
import {defaultScheduler, getTNode} from './util'; import {defaultScheduler, getTNode} from './util';
@ -169,6 +168,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
const componentView = createRootComponentView( const componentView = createRootComponentView(
hostRNode, this.componentDef, rootLView, rendererFactory, renderer); hostRNode, this.componentDef, rootLView, rendererFactory, renderer);
tElementNode = getTNode(0, rootLView) as TElementNode; tElementNode = getTNode(0, rootLView) as TElementNode;
// Transform the arrays of native nodes into a structure that can be consumed by the // Transform the arrays of native nodes into a structure that can be consumed by the
@ -207,6 +207,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
component = createRootComponent( component = createRootComponent(
componentView, this.componentDef, rootLView, rootContext, [LifecycleHooksFeature]); componentView, this.componentDef, rootLView, rootContext, [LifecycleHooksFeature]);
addToViewTree(rootLView, HEADER_OFFSET, componentView);
refreshDescendantViews(rootLView, RenderFlags.Create); refreshDescendantViews(rootLView, RenderFlags.Create);
} finally { } finally {
leaveView(oldLView, true); leaveView(oldLView, true);

View File

@ -455,16 +455,18 @@ export function getParentState(state: LView | LContainer, rootView: LView): LVie
} }
/** /**
* Removes all listeners and call all onDestroys in a given view. * Calls onDestroys hooks for all directives and pipes in a given view and then removes all
* listeners. Listeners are removed as the last step so events delivered in the onDestroys hooks
* can be propagated to @Output listeners.
* *
* @param view The LView to clean up * @param view The LView to clean up
*/ */
function cleanUpView(viewOrContainer: LView | LContainer): void { function cleanUpView(viewOrContainer: LView | LContainer): void {
if ((viewOrContainer as LView).length >= HEADER_OFFSET) { if ((viewOrContainer as LView).length >= HEADER_OFFSET) {
const view = viewOrContainer as LView; const view = viewOrContainer as LView;
removeListeners(view);
executeOnDestroys(view); executeOnDestroys(view);
executePipeOnDestroys(view); executePipeOnDestroys(view);
removeListeners(view);
const hostTNode = view[HOST_NODE]; const hostTNode = view[HOST_NODE];
// For component views only, the local renderer is destroyed as clean up time. // For component views only, the local renderer is destroyed as clean up time.
if (hostTNode && hostTNode.type === TNodeType.Element && isProceduralRenderer(view[RENDERER])) { if (hostTNode && hostTNode.type === TNodeType.Element && isProceduralRenderer(view[RENDERER])) {

View File

@ -1104,77 +1104,70 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}); });
describe('ngOnDestroy', () => { describe('ngOnDestroy', () => {
fixmeIvy('FW-763: LView tree not properly constructed / destroyed') it('should be called on view destruction', fakeAsync(() => {
.it('should be called on view destruction', fakeAsync(() => { const ctx = createCompFixture('<div testDirective="dir"></div>');
const ctx = createCompFixture('<div testDirective="dir"></div>'); ctx.detectChanges(false);
ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['dir.ngOnDestroy']); expect(directiveLog.filter(['ngOnDestroy'])).toEqual(['dir.ngOnDestroy']);
})); }));
fixmeIvy('FW-763: LView tree not properly constructed / destroyed') it('should be called after processing the content and view children', fakeAsync(() => {
.it('should be called after processing the content and view children', fakeAsync(() => { TestBed.overrideComponent(AnotherComponent, {
TestBed.overrideComponent(AnotherComponent, { set: new Component(
set: new Component({ {selector: 'other-cmp', template: '<div testDirective="viewChild"></div>'})
selector: 'other-cmp', });
template: '<div testDirective="viewChild"></div>'
})
});
const ctx = createCompFixture( const ctx = createCompFixture(
'<div testDirective="parent"><div *ngFor="let x of [0,1]" testDirective="contentChild{{x}}"></div>' + '<div testDirective="parent"><div *ngFor="let x of [0,1]" testDirective="contentChild{{x}}"></div>' +
'<other-cmp></other-cmp></div>', '<other-cmp></other-cmp></div>',
TestComponent); TestComponent);
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ expect(directiveLog.filter(['ngOnDestroy'])).toEqual([
'contentChild0.ngOnDestroy', 'contentChild1.ngOnDestroy', 'contentChild0.ngOnDestroy', 'contentChild1.ngOnDestroy', 'viewChild.ngOnDestroy',
'viewChild.ngOnDestroy', 'parent.ngOnDestroy' 'parent.ngOnDestroy'
]); ]);
})); }));
fixmeIvy('FW-763: LView tree not properly constructed / destroyed') it('should be called in reverse order so the child is always notified before the parent',
.it('should be called in reverse order so the child is always notified before the parent', fakeAsync(() => {
fakeAsync(() => { const ctx = createCompFixture(
const ctx = createCompFixture( '<div testDirective="parent"><div testDirective="child"></div></div><div testDirective="sibling"></div>');
'<div testDirective="parent"><div testDirective="child"></div></div><div testDirective="sibling"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ expect(directiveLog.filter(['ngOnDestroy'])).toEqual([
'child.ngOnDestroy', 'parent.ngOnDestroy', 'sibling.ngOnDestroy' 'child.ngOnDestroy', 'parent.ngOnDestroy', 'sibling.ngOnDestroy'
]); ]);
})); }));
fixmeIvy('FW-763: LView tree not properly constructed / destroyed') it('should deliver synchronous events to parent', fakeAsync(() => {
.it('should deliver synchronous events to parent', fakeAsync(() => { const ctx = createCompFixture('<div (destroy)="a=$event" onDestroyDirective></div>');
const ctx =
createCompFixture('<div (destroy)="a=$event" onDestroyDirective></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.destroy(); ctx.destroy();
expect(ctx.componentInstance.a).toEqual('destroyed'); expect(ctx.componentInstance.a).toEqual('destroyed');
})); }));
fixmeIvy('FW-763: LView tree not properly constructed / destroyed')
.it('should call ngOnDestroy on pipes', fakeAsync(() => {
const ctx = createCompFixture('{{true | pipeWithOnDestroy }}');
ctx.detectChanges(false); it('should call ngOnDestroy on pipes', fakeAsync(() => {
ctx.destroy(); const ctx = createCompFixture('{{true | pipeWithOnDestroy }}');
expect(directiveLog.filter(['ngOnDestroy'])).toEqual([ ctx.detectChanges(false);
'pipeWithOnDestroy.ngOnDestroy' ctx.destroy();
]);
}));
fixmeIvy('FW-763: LView tree not properly constructed / destroyed') expect(directiveLog.filter(['ngOnDestroy'])).toEqual([
'pipeWithOnDestroy.ngOnDestroy'
]);
}));
fixmeIvy('FW-848: ngOnDestroy hooks are not called on providers')
.it('should call ngOnDestroy on an injectable class', fakeAsync(() => { .it('should call ngOnDestroy on an injectable class', fakeAsync(() => {
TestBed.overrideDirective( TestBed.overrideDirective(
TestDirective, {set: {providers: [InjectableWithLifecycle]}}); TestDirective, {set: {providers: [InjectableWithLifecycle]}});

View File

@ -768,37 +768,35 @@ function declareTests(config?: {useJit: boolean}) {
expect(childComponent.myHost).toBeAnInstanceOf(SomeDirective); expect(childComponent.myHost).toBeAnInstanceOf(SomeDirective);
}); });
fixmeIvy( it('should support events via EventEmitter on regular elements', async(() => {
'FW-763: LView tree not properly constructed / destroyed for dynamically inserted components') TestBed.configureTestingModule(
.it('should support events via EventEmitter on regular elements', async(() => { {declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]});
TestBed.configureTestingModule( const template = '<div emitter listener></div>';
{declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]}); TestBed.overrideComponent(MyComp, {set: {template}});
const template = '<div emitter listener></div>'; const fixture = TestBed.createComponent(MyComp);
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
const tc = fixture.debugElement.children[0]; const tc = fixture.debugElement.children[0];
const emitter = tc.injector.get(DirectiveEmittingEvent); const emitter = tc.injector.get(DirectiveEmittingEvent);
const listener = tc.injector.get(DirectiveListeningEvent); const listener = tc.injector.get(DirectiveListeningEvent);
expect(listener.msg).toEqual(''); expect(listener.msg).toEqual('');
let eventCount = 0; let eventCount = 0;
emitter.event.subscribe({ emitter.event.subscribe({
next: () => { next: () => {
eventCount++; eventCount++;
if (eventCount === 1) { if (eventCount === 1) {
expect(listener.msg).toEqual('fired !'); expect(listener.msg).toEqual('fired !');
fixture.destroy(); fixture.destroy();
emitter.fireEvent('fired again !'); emitter.fireEvent('fired again !');
} else { } else {
expect(listener.msg).toEqual('fired !'); expect(listener.msg).toEqual('fired !');
} }
} }
}); });
emitter.fireEvent('fired !'); emitter.fireEvent('fired !');
})); }));
fixmeIvy( fixmeIvy(
'FW-665: Discovery util fails with Unable to find the given context data for the given target') 'FW-665: Discovery util fails with Unable to find the given context data for the given target')

View File

@ -616,9 +616,8 @@ describe('Query API', () => {
expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']);
}); });
fixmeIvy( fixmeIvy('unknown').it(
'FW-763 - LView tree not properly constructed / destroyed for dynamically inserted components') 'should remove manually projected templates if their parent view is destroyed', () => {
.it('should remove manually projected templates if their parent view is destroyed', () => {
const template = ` const template = `
<manual-projecting #q><ng-template #tpl><div text="1"></div></ng-template></manual-projecting> <manual-projecting #q><ng-template #tpl><div text="1"></div></ng-template></manual-projecting>
<div *ngIf="shouldShow"> <div *ngIf="shouldShow">