Skip to content

Commit 499fb5c

Browse files
AndrewKushnirdylhunn
authored andcommitted
fix(core): ensure that standalone components get correct injector instances (#50954)
Prior to this change, we've used `componentDef.id` as a key in a Map that acts as a cache to avoid re-creating injector instances for standalone components. In v16, the logic that generates the id has changed from an auto-incremental to a generation based on metadata. If multiple components have similar metadata, their ids might overlap. This commit updates the logic to stop using `componentDef.id` as a key and instead, use the `componentDef` itself. This would ensure that we always have a correct instance of an injector associated with a standalone component instance. Resolves #50724. PR Close #50954
1 parent c086569 commit 499fb5c

File tree

2 files changed

+69
-6
lines changed

2 files changed

+69
-6
lines changed

packages/core/src/render3/features/standalone_feature.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {createEnvironmentInjector} from '../ng_module_ref';
1919
* collected from the imports graph rooted at a given standalone component.
2020
*/
2121
class StandaloneService implements OnDestroy {
22-
cachedInjectors = new Map<string, EnvironmentInjector|null>();
22+
cachedInjectors = new Map<ComponentDef<unknown>, EnvironmentInjector|null>();
2323

2424
constructor(private _injector: EnvironmentInjector) {}
2525

@@ -28,16 +28,16 @@ class StandaloneService implements OnDestroy {
2828
return null;
2929
}
3030

31-
if (!this.cachedInjectors.has(componentDef.id)) {
31+
if (!this.cachedInjectors.has(componentDef)) {
3232
const providers = internalImportProvidersFrom(false, componentDef.type);
3333
const standaloneInjector = providers.length > 0 ?
3434
createEnvironmentInjector(
3535
[providers], this._injector, `Standalone[${componentDef.type.name}]`) :
3636
null;
37-
this.cachedInjectors.set(componentDef.id, standaloneInjector);
37+
this.cachedInjectors.set(componentDef, standaloneInjector);
3838
}
3939

40-
return this.cachedInjectors.get(componentDef.id)!;
40+
return this.cachedInjectors.get(componentDef)!;
4141
}
4242

4343
ngOnDestroy() {

packages/core/test/acceptance/standalone_spec.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {CommonModule} from '@angular/common';
10-
import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, Injector, Input, isStandalone, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core';
9+
import {CommonModule, NgComponentOutlet} from '@angular/common';
10+
import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, inject, Injectable, Injector, Input, isStandalone, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core';
1111
import {TestBed} from '@angular/core/testing';
1212

1313
describe('standalone components, directives, and pipes', () => {
@@ -217,6 +217,69 @@ describe('standalone components, directives, and pipes', () => {
217217
.toEqual('Outer<inner-cmp>Inner(Service)</inner-cmp>Service');
218218
});
219219

220+
it('should correctly associate an injector with a standalone component def', () => {
221+
@Injectable()
222+
class MyServiceA {
223+
}
224+
225+
@Injectable()
226+
class MyServiceB {
227+
}
228+
229+
@NgModule({
230+
providers: [MyServiceA],
231+
})
232+
class MyModuleA {
233+
}
234+
235+
@NgModule({
236+
providers: [MyServiceB],
237+
})
238+
class MyModuleB {
239+
}
240+
241+
@Component({
242+
selector: 'duplicate-selector',
243+
template: `ComponentA: {{ service ? 'service found' : 'service not found' }}`,
244+
standalone: true,
245+
imports: [MyModuleA],
246+
})
247+
class ComponentA {
248+
service = inject(MyServiceA, {optional: true});
249+
}
250+
251+
@Component({
252+
selector: 'duplicate-selector',
253+
template: `ComponentB: {{ service ? 'service found' : 'service not found' }}`,
254+
standalone: true,
255+
imports: [MyModuleB],
256+
})
257+
class ComponentB {
258+
service = inject(MyServiceB, {optional: true});
259+
}
260+
261+
@Component({
262+
selector: 'app-cmp',
263+
template: `
264+
<ng-container [ngComponentOutlet]="ComponentA" />
265+
<ng-container [ngComponentOutlet]="ComponentB" />
266+
`,
267+
standalone: true,
268+
imports: [NgComponentOutlet],
269+
})
270+
class AppCmp {
271+
ComponentA = ComponentA;
272+
ComponentB = ComponentB;
273+
}
274+
275+
const fixture = TestBed.createComponent(AppCmp);
276+
fixture.detectChanges();
277+
278+
const textContent = fixture.nativeElement.textContent;
279+
expect(textContent).toContain('ComponentA: service found');
280+
expect(textContent).toContain('ComponentB: service found');
281+
});
282+
220283
it('should dynamically insert a standalone component', () => {
221284
class Service {
222285
value = 'Service';

0 commit comments

Comments
 (0)