fix(ivy): ngOnDestroy hooks should be called on providers (#27955)

PR Close #27955
This commit is contained in:
Marc Laval 2019-01-07 17:07:39 +01:00 committed by Kara Erickson
parent 996435b79a
commit e775313188
7 changed files with 139 additions and 41 deletions

View File

@ -503,6 +503,10 @@ 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) {
(tView.destroyHooks || (tView.destroyHooks = [])).push(index, value.ngOnDestroy);
}
} finally {
if (factory.injectImpl) setInjectImplementation(previousInjectImplementation);
setIncludeViewProviders(previousIncludeViewProviders);

View File

@ -83,7 +83,8 @@ function resolveProvider(
if (isTypeProvider(provider) || !provider.multi) {
// Single provider case: the factory is created and pushed immediately
const factory = new NodeInjectorFactory(providerFactory, isViewProvider, directiveInject);
const factory =
new NodeInjectorFactory(providerFactory, isViewProvider, true, directiveInject);
const existingFactoryIndex = indexOf(
token, tInjectables, isViewProvider ? beginIndex : beginIndex + cptViewProvidersCount,
endIndex);
@ -245,7 +246,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, directiveInject);
const factory = new NodeInjectorFactory(factoryFn, isViewProvider, true, directiveInject);
factory.multi = [];
factory.index = index;
factory.componentProviders = 0;

View File

@ -1732,7 +1732,8 @@ 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), null);
const nodeInjectorFactory =
new NodeInjectorFactory(directiveFactory, isComponentDef(def), false, null);
tView.blueprint.push(nodeInjectorFactory);
viewData.push(nodeInjectorFactory);
}

View File

@ -234,6 +234,10 @@ 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;

View File

@ -1167,23 +1167,22 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
]);
}));
fixmeIvy('FW-848: ngOnDestroy hooks are not called on providers')
.it('should call ngOnDestroy on an injectable class', fakeAsync(() => {
TestBed.overrideDirective(
TestDirective, {set: {providers: [InjectableWithLifecycle]}});
it('should call ngOnDestroy on an injectable class', fakeAsync(() => {
TestBed.overrideDirective(
TestDirective, {set: {providers: [InjectableWithLifecycle]}});
const ctx = createCompFixture('<div testDirective="dir"></div>', TestComponent);
const ctx = createCompFixture('<div testDirective="dir"></div>', TestComponent);
ctx.debugElement.children[0].injector.get(InjectableWithLifecycle);
ctx.detectChanges(false);
ctx.debugElement.children[0].injector.get(InjectableWithLifecycle);
ctx.detectChanges(false);
ctx.destroy();
ctx.destroy();
// We don't care about the exact order in this test.
expect(directiveLog.filter(['ngOnDestroy']).sort()).toEqual([
'dir.ngOnDestroy', 'injectable.ngOnDestroy'
]);
}));
// We don't care about the exact order in this test.
expect(directiveLog.filter(['ngOnDestroy']).sort()).toEqual([
'dir.ngOnDestroy', 'injectable.ngOnDestroy'
]);
}));
});
});

View File

@ -465,38 +465,37 @@ class TestComp {
expect(comp.componentInstance.a).toBe('aValue');
});
fixmeIvy('FW-848: ngOnDestroy hooks are not called on providers')
.it('should support ngOnDestroy for lazy providers', () => {
let created = false;
let destroyed = false;
it('should support ngOnDestroy for lazy providers', () => {
let created = false;
let destroyed = false;
class SomeInjectable {
constructor() { created = true; }
ngOnDestroy() { destroyed = true; }
}
class SomeInjectable {
constructor() { created = true; }
ngOnDestroy() { destroyed = true; }
}
@Component({providers: [SomeInjectable], template: ''})
class SomeComp {
}
@Component({providers: [SomeInjectable], template: ''})
class SomeComp {
}
TestBed.configureTestingModule({declarations: [SomeComp]});
TestBed.configureTestingModule({declarations: [SomeComp]});
let compRef = TestBed.createComponent(SomeComp).componentRef;
expect(created).toBe(false);
expect(destroyed).toBe(false);
let compRef = TestBed.createComponent(SomeComp).componentRef;
expect(created).toBe(false);
expect(destroyed).toBe(false);
// no error if the provider was not yet created
compRef.destroy();
expect(created).toBe(false);
expect(destroyed).toBe(false);
// no error if the provider was not yet created
compRef.destroy();
expect(created).toBe(false);
expect(destroyed).toBe(false);
compRef = TestBed.createComponent(SomeComp).componentRef;
compRef.injector.get(SomeInjectable);
expect(created).toBe(true);
compRef.destroy();
expect(destroyed).toBe(true);
});
compRef = TestBed.createComponent(SomeComp).componentRef;
compRef.injector.get(SomeInjectable);
expect(created).toBe(true);
compRef.destroy();
expect(destroyed).toBe(true);
});
it('should instantiate view providers lazily', () => {
let created = false;

View File

@ -1265,6 +1265,96 @@ describe('providers', () => {
expect(injector.get(Some).location).toEqual('From app component');
});
});
describe('lifecycles', () => {
it('should execute ngOnDestroy hooks on providers (and only this one)', () => {
const logs: string[] = [];
@Injectable()
class InjectableWithLifeCycleHooks {
ngOnChanges() { logs.push('Injectable OnChanges'); }
ngOnInit() { logs.push('Injectable OnInit'); }
ngDoCheck() { logs.push('Injectable DoCheck'); }
ngAfterContentInit() { logs.push('Injectable AfterContentInit'); }
ngAfterContentChecked() { logs.push('Injectable AfterContentChecked'); }
ngAfterViewInit() { logs.push('Injectable AfterViewInit'); }
ngAfterViewChecked() { logs.push('Injectable gAfterViewChecked'); }
ngOnDestroy() { logs.push('Injectable OnDestroy'); }
}
@Component({template: `<span></span>`, providers: [InjectableWithLifeCycleHooks]})
class MyComponent {
constructor(foo: InjectableWithLifeCycleHooks) {}
static ngComponentDef = defineComponent({
type: MyComponent,
selectors: [['my-comp']],
factory: () => new MyComponent(directiveInject(InjectableWithLifeCycleHooks)),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: MyComponent) => {
if (rf & RenderFlags.Create) {
element(0, 'span');
}
},
features: [ProvidersFeature([InjectableWithLifeCycleHooks])]
});
}
@Component({
template: `
<div>
% if (ctx.condition) {
<my-comp></my-comp>
% }
</div>
`,
})
class App {
public condition = true;
static ngComponentDef = defineComponent({
type: App,
selectors: [['app-cmp']],
factory: () => new App(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, ctx: App) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ container(1); }
elementEnd();
}
if (rf & RenderFlags.Update) {
containerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = embeddedViewStart(1, 2, 1);
{
if (rf1 & RenderFlags.Create) {
element(0, 'my-comp');
}
}
embeddedViewEnd();
}
}
containerRefreshEnd();
}
},
directives: [MyComponent]
});
}
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html).toEqual('<div><my-comp><span></span></my-comp></div>');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('<div></div>');
expect(logs).toEqual(['Injectable OnDestroy']);
});
});
});
interface ComponentTest {
providers?: Provider[];