8000 fix(ivy): component destroy hook called twice when configured as provider by crisbeto · Pull Request #28470 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

fix(ivy): component destroy hook called twice when configured as provider #28470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fixup! fix(ivy): component destroy hook called twice when configured …
…as provider
  • Loading branch information
crisbeto committed Feb 21, 2019
commit a5817539a4e09cdbe61a2b03d5d6b68f0c805b05
2 changes: 1 addition & 1 deletion integration/_payload-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 207765,
"main": 209904,
"polyfills": 38390
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ export function isTypeProvider(value: SingleProvider): value is TypeProvider {
return typeof value === 'function';
}

export function isClassProvider(value: SingleProvider): value is ClassProvider {
return !!(value as StaticClassProvider | ClassProvider).useClass;
}

function hasDeps(value: ClassProvider | ConstructorProvider | StaticClassProvider):
value is ClassProvider&{deps: any[]} {
return !!(value as any).deps;
Expand Down
7 changes: 0 additions & 7 deletions packages/core/src/render3/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,13 +520,6 @@ export function getNodeInjectable(
setTNodeAndViewData(tNode, lData);
try {
value = lData[index] = factory.factory(null, tData, lData, tNode);
const tView = lData[TVIEW];
if (value && factory.isProvider && value.ngOnDestroy) {
const type = value.constructor as Type<any>;
if (!getComponentDef(type) && !getDirectiveDef(type)) {
(tView.destroyHooks || (tView.destroyHooks = [])).push(index, value.ngOnDestroy);
}
}
} finally {
if (factory.injectImpl) setInjectImplementation(previousInjectImplementation);
setIncludeViewProviders(previousIncludeViewProviders);
Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/render3/di_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@


import {resolveForwardRef} from '../di/forward_ref';
import {Provider} from '../di/interface/provider';
import {isTypeProvider, providerToFactory} from '../di/r3_injector';
import {ClassProvider, Provider} from '../di/interface/provider';
import {isClassProvider, isTypeProvider, providerToFactory} from '../di/r3_injector';

import {DirectiveDef} from '.';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from './di';
Expand Down Expand Up @@ -81,10 +81,19 @@ function resolveProvider(
const cptViewProvidersCount =
tNode.providerIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift;

if (isClassProvider(provider) || isTypeProvider(provider)) {
const prototype = ((provider as ClassProvider).useClass || provider).prototype;
const ngOnDestroy = prototype.ngOnDestroy;

if (ngOnDestroy) {
const tView = lView[TVIEW];
(tView.destroyHooks || (tView.destroyHooks = [])).push(tInjectables.length, ngOnDestroy);
}
}

if (isTypeProvider(provider) || !provider.multi) {
// Single provider case: the factory is created and pushed immediately
const factory =
new NodeInjectorFactory(providerFactory, isViewProvider, true, directiveInject);
const factory = new NodeInjectorFactory(providerFactory, isViewProvider, directiveInject);
const existingFactoryIndex = indexOf(
token, tInjectables, isViewProvider ? beginIndex : beginIndex + cptViewProvidersCount,
endIndex);
Expand Down Expand Up @@ -246,7 +255,7 @@ function multiFactory(
this: NodeInjectorFactory, _: null, tData: TData, lData: LView, tNode: TElementNode) => any,
index: number, isViewProvider: boolean, isComponent: boolean,
f: () => any): NodeInjectorFactory {
const factory = new NodeInjectorFactory(factoryFn, isViewProvider, true, directiveInject);
const factory = new NodeInjectorFactory(factoryFn, isViewProvider, directiveInject);
factory.multi = [];
factory.index = index;
factory.componentProviders = 0;
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/render3/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2067,8 +2067,7 @@ function baseResolveDirective<T>(
tView: TView, viewData: LView, def: DirectiveDef<T>,
directiveFactory: (t: Type<T>| null) => any) {
tView.data.push(def);
const nodeInjectorFactory =
new NodeInjectorFactory(directiveFactory, isComponentDef(def), false, null);
const nodeInjectorFactory = new NodeInjectorFactory(directiveFactory, isComponentDef(def), null);
tView.blueprint.push(nodeInjectorFactory);
viewData.push(nodeInjectorFactory);
}
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/render3/interfaces/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,6 @@ export class NodeInjectorFactory {
* Set to `true` if the token is declared in `viewProviders` (or if it is component).
*/
isViewProvider: boolean,
/**
* Set to `true` if the token is a provider, and not a directive.
*/
public isProvider: boolean,
injectImplementation: null|(<T>(token: Type<T>|InjectionToken<T>, flags: InjectFlags) => T)) {
this.canSeeViewProviders = isViewProvider;
this.injectImpl = injectImplementation;
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import {ViewEncapsulation} from '../metadata/view';

import {attachPatchData} from './context_discovery';
import {callHooks} from './hooks';
import {LContainer, NATIVE, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
import {ComponentDef} from './interfaces/definition';
import {NodeInjectorFactory} from './interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
Expand Down Expand Up @@ -495,8 +495,16 @@ function removeListeners(lView: LView): void {
function executeOnDestroys(view: LView): void {
const tView = view[TVIEW];
let destroyHooks: HookData|null;

if (tView != null && (destroyHooks = tView.destroyHooks) != null) {
callHooks(view, destroyHooks);
for (let i = 0; i < destroyHooks.length; i += 2) {
const context = view[destroyHooks[i] as number];

// Only call the destroy hook if the context has been requested.
if (!(context instanceof NodeInjectorFactory)) {
(destroyHooks[i + 1] as() => void).call(context);
}
}
}
}

Expand Down
52 changes: 52 additions & 0 deletions packages/core/test/acceptance/component_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component, InjectionToken} from '@angular/core';
import {TestBed} from '@angular/core/testing';


describe('component', () => {
describe('view destruction', () => {
it('should invoke onDestroy only once when a component is registered as a provider', () => {
const testToken = new InjectionToken<ParentWithOnDestroy>('testToken');
let destroyCalls = 0;

@Component({
selector: 'comp-with-on-destroy',
template: '',
providers: [{provide: testToken, useExisting: ParentWithOnDestroy}]
})
class ParentWithOnDestroy {
ngOnDestroy() { destroyCalls++; }
}

@Component({selector: 'child', template: ''})
class ChildComponent {
// We need to inject the parent so the provider is instantiated.
constructor(_parent: ParentWithOnDestroy) {}
}

@Component({
template: `
<comp-with-on-destroy>
<child></child>
</comp-with-on-destroy>
`
})
class App {
}

TestBed.configureTestingModule({declarations: [App, ParentWithOnDestroy, ChildComponent]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(destroyCalls).toBe(1, 'Expected `ngOnDestroy` to only be called once.');
});
});
});
184 changes: 184 additions & 0 deletions packages/core/test/acceptance/providers_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component, Injectable} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {onlyInIvy} from '@angular/private/testing';


describe('providers', () => {
describe('lifecycles', () => {
it('should inherit ngOnDestroy hooks on providers', () => {
const logs: string[] = [];

@Injectable()
class SuperInjectableWithDestroyHook {
ngOnDestroy() { logs.push('OnDestroy'); }
}

@Injectable()
class SubInjectableWithDestroyHook extends SuperInjectableWithDestroyHook {
}

@Component({template: '', providers: [SubInjectableWithDestroyHook]})
class App {
constructor(foo: SubInjectableWithDestroyHook) {}
}

TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual(['OnDestroy']);
});

it('should not call ngOnDestroy for providers that have not been requested', () => {
const logs: string[] = [];

@Injectable()
class InjectableWithDestroyHook {
ngOnDestroy() { logs.push('OnDestroy'); }
}

@Component({template: '', providers: [InjectableWithDestroyHook]})
class App {
}

TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual([]);
});

it('should only call ngOnDestroy once for multiple instances', () => {
const logs: string[] = [];

@Injectable()
class InjectableWithDestroyHook {
ngOnDestroy() { logs.push('OnDestroy'); }
}

@Component({selector: 'my-cmp', template: ''})
class MyComponent {
constructor(foo: InjectableWithDestroyHook) {}
}

@Component({
template: `
<my-cmp></my-cmp>
<my-cmp></my-cmp>
`,
providers: [InjectableWithDestroyHook]
})
class App {
}

TestBed.configureTestingModule({declarations: [App, MyComponent]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual(['OnDestroy']);
});

it('should call ngOnDestroy when providing same token via useClass', () => {
const logs: string[] = [];

@Injectable()
class InjectableWithDestroyHook {
ngOnDestroy() { logs.push('OnDestroy'); }
}

@Component({
template: '',
providers: [{provide: InjectableWithDestroyHook, useClass: InjectableWithDestroyHook}]
})
class App {
constructor(foo: InjectableWithDestroyHook) {}
}

TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual(['OnDestroy']);
});

onlyInIvy('Destroy hook of useClass provider is invoked correctly')
.it('should only call ngOnDestroy of value when providing via useClass', () => {
const logs: string[] = [];

@Injectable()
class InjectableWithDestroyHookToken {
ngOnDestroy() { logs.push('OnDestroy Token'); }
}

@Injectable()
class InjectableWithDestroyHookValue {
ngOnDestroy() { logs.push('OnDestroy Value'); }
}

@Component({
template: '',
providers: [
{provide: InjectableWithDestroyHookToken, useClass: InjectableWithDestroyHookValue}
]
})
class App {
constructor(foo: InjectableWithDestroyHookToken) {}
}

TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual(['OnDestroy Value']);
});

it('should only call ngOnDestroy of value when providing via useExisting', () => {
const logs: string[] = [];

@Injectable()
class InjectableWithDestroyHookToken {
ngOnDestroy() { logs.push('OnDestroy Token'); }
}

@Injectable()
class InjectableWithDestroyHookExisting {
ngOnDestroy() { logs.push('OnDestroy Existing'); }
}

@Component({
template: '',
providers: [
InjectableWithDestroyHookExisting, {
provide: InjectableWithDestroyHookToken,
useExisting: InjectableWithDestroyHookExisting
}
]
})
class App {
constructor(foo1: InjectableWithDestroyHookExisting, foo2: InjectableWithDestroyHookToken) {
}
}

TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();

expect(logs).toEqual(['OnDestroy Existing']);
});

});
});
Loading
0