Skip to content

Commit 0671e54

Browse files
crisbetoalxhub
authored andcommitted
fix(ivy): wrong context passed to ngOnDestroy when resolved multiple times (#35249)
When the same provider is resolved multiple times on the same node, the first invocation had the correct context, but all subsequent ones were incorrect because we were registering the hook multiple times under different indexes in `destroyHooks`. Fixes #35167. PR Close #35249
1 parent 45c7b23 commit 0671e54

File tree

2 files changed

+148
-10
lines changed

2 files changed

+148
-10
lines changed

packages/core/src/render3/di_setup.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,26 +81,18 @@ function resolveProvider(
8181
const cptViewProvidersCount =
8282
tNode.providerIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift;
8383

84-
if (isClassProvider(provider) || isTypeProvider(provider)) {
85-
const prototype = ((provider as ClassProvider).useClass || provider).prototype;
86-
const ngOnDestroy = prototype.ngOnDestroy;
87-
88-
if (ngOnDestroy) {
89-
(tView.destroyHooks || (tView.destroyHooks = [])).push(tInjectables.length, ngOnDestroy);
90-
}
91-
}
92-
9384
if (isTypeProvider(provider) || !provider.multi) {
9485
// Single provider case: the factory is created and pushed immediately
9586
const factory = new NodeInjectorFactory(providerFactory, isViewProvider, ɵɵdirectiveInject);
9687
const existingFactoryIndex = indexOf(
9788
token, tInjectables, isViewProvider ? beginIndex : beginIndex + cptViewProvidersCount,
9889
endIndex);
99-
if (existingFactoryIndex == -1) {
90+
if (existingFactoryIndex === -1) {
10091
diPublicInInjector(
10192
getOrCreateNodeInjectorForNode(
10293
tNode as TElementNode | TContainerNode | TElementContainerNode, lView),
10394
tView, token);
95+
registerDestroyHooksIfSupported(tView, provider, tInjectables.length);
10496
tInjectables.push(token);
10597
tNode.directiveStart++;
10698
tNode.directiveEnd++;
@@ -157,6 +149,7 @@ function resolveProvider(
157149
if (!isViewProvider && doesViewProvidersFactoryExist) {
158150
lInjectablesBlueprint[existingViewProvidersFactoryIndex].providerFactory = factory;
159151
}
152+
registerDestroyHooksIfSupported(tView, provider, tInjectables.length);
160153
tInjectables.push(token);
161154
tNode.directiveStart++;
162155
tNode.directiveEnd++;
@@ -167,6 +160,10 @@ function resolveProvider(
167160
lView.push(factory);
168161
} else {
169162
// Cases 1.b and 2.b
163+
registerDestroyHooksIfSupported(
164+
tView, provider, existingProvidersFactoryIndex > -1 ?
165+
existingProvidersFactoryIndex :
166+
existingViewProvidersFactoryIndex);
170167
multiFactoryAdd(
171168
lInjectablesBlueprint ![isViewProvider ? existingViewProvidersFactoryIndex : existingProvidersFactoryIndex],
172169
providerFactory, !isViewProvider && isComponent);
@@ -178,6 +175,24 @@ function resolveProvider(
178175
}
179176
}
180177

178+
/**
179+
* Registers the `ngOnDestroy` hook of a provider, if the provider supports destroy hooks.
180+
* @param tView `TView` in which to register the hook.
181+
* @param provider Provider whose hook should be registered.
182+
* @param contextIndex Index under which to find the context for the hook when it's being invoked.
183+
*/
184+
function registerDestroyHooksIfSupported(
185+
tView: TView, provider: Exclude<Provider, any[]>, contextIndex: number) {
186+
const providerIsTypeProvider = isTypeProvider(provider);
187+
if (providerIsTypeProvider || isClassProvider(provider)) {
188+
const prototype = ((provider as ClassProvider).useClass || provider).prototype;
189+
const ngOnDestroy = prototype.ngOnDestroy;
190+
if (ngOnDestroy) {
191+
(tView.destroyHooks || (tView.destroyHooks = [])).push(contextIndex, ngOnDestroy);
192+
}
193+
}
194+
}
195+
181196
/**
182197
* Add a factory in a multi factory.
183198
*/

packages/core/test/acceptance/providers_spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,129 @@ describe('providers', () => {
223223
expect(logs).toEqual(['OnDestroy Existing']);
224224
});
225225

226+
it('should invoke ngOnDestroy with the correct context when providing a type provider multiple times on the same node',
227+
() => {
228+
const resolvedServices: (DestroyService | undefined)[] = [];
229+
const destroyContexts: (DestroyService | undefined)[] = [];
230+
let parentService: DestroyService|undefined;
231+
let childService: DestroyService|undefined;
232+
233+
@Injectable()
234+
class DestroyService {
235+
constructor() { resolvedServices.push(this); }
236+
ngOnDestroy() { destroyContexts.push(this); }
237+
}
238+
239+
@Directive({selector: '[dir-one]', providers: [DestroyService]})
240+
class DirOne {
241+
constructor(service: DestroyService) { childService = service; }
242+
}
243+
244+
@Directive({selector: '[dir-two]', providers: [DestroyService]})
245+
class DirTwo {
246+
constructor(service: DestroyService) { childService = service; }
247+
}
248+
249+
@Component({template: '<div dir-one dir-two></div>', providers: [DestroyService]})
250+
class App {
251+
constructor(service: DestroyService) { parentService = service; }
252+
}
253+
254+
TestBed.configureTestingModule({declarations: [App, DirOne, DirTwo]});
255+
const fixture = TestBed.createComponent(App);
256+
fixture.detectChanges();
257+
fixture.destroy();
258+
259+
expect(parentService).toBeDefined();
260+
expect(childService).toBeDefined();
261+
expect(parentService).not.toBe(childService);
262+
expect(resolvedServices).toEqual([parentService, childService]);
263+
expect(destroyContexts).toEqual([parentService, childService]);
264+
});
265+
266+
onlyInIvy('Destroy hook of useClass provider is invoked correctly')
267+
.it('should invoke ngOnDestroy with the correct context when providing a class provider multiple times on the same node',
268+
() => {
269+
const resolvedServices: (DestroyService | undefined)[] = [];
270+
const destroyContexts: (DestroyService | undefined)[] = [];
271+
const token = new InjectionToken<any>('token');
272+
let parentService: DestroyService|undefined;
273+
let childService: DestroyService|undefined;
274+
275+
@Injectable()
276+
class DestroyService {
277+
constructor() { resolvedServices.push(this); }
278+
ngOnDestroy() { destroyContexts.push(this); }
279+
}
280+
281+
@Directive(
282+
{selector: '[dir-one]', providers: [{provide: token, useClass: DestroyService}]})
283+
class DirOne {
284+
constructor(@Inject(token) service: DestroyService) { childService = service; }
285+
}
286+
287+
@Directive(
288+
{selector: '[dir-two]', providers: [{provide: token, useClass: DestroyService}]})
289+
class DirTwo {
290+
constructor(@Inject(token) service: DestroyService) { childService = service; }
291+
}
292+
293+
@Component({
294+
template: '<div dir-one dir-two></div>',
295+
providers: [{provide: token, useClass: DestroyService}]
296+
})
297+
class App {
298+
constructor(@Inject(token) service: DestroyService) { parentService = service; }
299+
}
300+
301+
TestBed.configureTestingModule({declarations: [App, DirOne, DirTwo]});
302+
const fixture = TestBed.createComponent(App);
303+
fixture.detectChanges();
304+
fixture.destroy();
305+
306+
expect(parentService).toBeDefined();
307+
expect(childService).toBeDefined();
308+
expect(parentService).not.toBe(childService);
309+
expect(resolvedServices).toEqual([parentService, childService]);
310+
expect(destroyContexts).toEqual([parentService, childService]);
311+
});
312+
313+
onlyInIvy('ngOnDestroy hooks for multi providers were not supported in ViewEngine')
314+
.it('should not invoke ngOnDestroy on multi providers', () => {
315+
// TODO(FW-1866): currently we only assert that the hook was called,
316+
// but we should also be checking that the correct context was passed in.
317+
let destroyCalls = 0;
318+
const SERVICES = new InjectionToken<any>('SERVICES');
319+
320+
@Injectable()
321+
class DestroyService {
322+
ngOnDestroy() { destroyCalls++; }
323+
}
324+
325+
@Injectable()
326+
class OtherDestroyService {
327+
ngOnDestroy() { destroyCalls++; }
328+
}
329+
330+
@Component({
331+
template: '<div></div>',
332+
providers: [
333+
{provide: SERVICES, useClass: DestroyService, multi: true},
334+
{provide: SERVICES, useClass: OtherDestroyService, multi: true},
335+
]
336+
})
337+
class App {
338+
constructor(@Inject(SERVICES) s: any) {}
339+
}
340+
341+
TestBed.configureTestingModule({declarations: [App]});
342+
const fixture = TestBed.createComponent(App);
343+
fixture.detectChanges();
344+
fixture.destroy();
345+
346+
expect(destroyCalls).toBe(2);
347+
});
348+
226349
});
227350

228351
describe('components and directives', () => {

0 commit comments

Comments
 (0)