From 031b599a5528e1df5779695eb75b738a5a3076fe Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 5 Jul 2023 22:19:37 -0700 Subject: [PATCH] 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 --- .../render3/features/standalone_feature.ts | 8 +-- .../core/test/acceptance/standalone_spec.ts | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/core/src/render3/features/standalone_feature.ts b/packages/core/src/render3/features/standalone_feature.ts index 3d61ead06b21f..e847b92444a01 100644 --- a/packages/core/src/render3/features/standalone_feature.ts +++ b/packages/core/src/render3/features/standalone_feature.ts @@ -19,7 +19,7 @@ import {createEnvironmentInjector} from '../ng_module_ref'; * collected from the imports graph rooted at a given standalone component. */ class StandaloneService implements OnDestroy { - cachedInjectors = new Map(); + cachedInjectors = new Map, EnvironmentInjector|null>(); constructor(private _injector: EnvironmentInjector) {} @@ -28,16 +28,16 @@ class StandaloneService implements OnDestroy { return null; } - if (!this.cachedInjectors.has(componentDef.id)) { + if (!this.cachedInjectors.has(componentDef)) { const providers = internalImportProvidersFrom(false, componentDef.type); const standaloneInjector = providers.length > 0 ? createEnvironmentInjector( [providers], this._injector, `Standalone[${componentDef.type.name}]`) : null; - this.cachedInjectors.set(componentDef.id, standaloneInjector); + this.cachedInjectors.set(componentDef, standaloneInjector); } - return this.cachedInjectors.get(componentDef.id)!; + return this.cachedInjectors.get(componentDef)!; } ngOnDestroy() { diff --git a/packages/core/test/acceptance/standalone_spec.ts b/packages/core/test/acceptance/standalone_spec.ts index b148f3734bd12..95c467f843222 100644 --- a/packages/core/test/acceptance/standalone_spec.ts +++ b/packages/core/test/acceptance/standalone_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CommonModule} from '@angular/common'; -import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, Injector, Input, isStandalone, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core'; +import {CommonModule, NgComponentOutlet} from '@angular/common'; +import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, inject, Injectable, Injector, Input, isStandalone, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; describe('standalone components, directives, and pipes', () => { @@ -217,6 +217,69 @@ describe('standalone components, directives, and pipes', () => { .toEqual('OuterInner(Service)Service'); }); + it('should correctly associate an injector with a standalone component def', () => { + @Injectable() + class MyServiceA { + } + + @Injectable() + class MyServiceB { + } + + @NgModule({ + providers: [MyServiceA], + }) + class MyModuleA { + } + + @NgModule({ + providers: [MyServiceB], + }) + class MyModuleB { + } + + @Component({ + selector: 'duplicate-selector', + template: `ComponentA: {{ service ? 'service found' : 'service not found' }}`, + standalone: true, + imports: [MyModuleA], + }) + class ComponentA { + service = inject(MyServiceA, {optional: true}); + } + + @Component({ + selector: 'duplicate-selector', + template: `ComponentB: {{ service ? 'service found' : 'service not found' }}`, + standalone: true, + imports: [MyModuleB], + }) + class ComponentB { + service = inject(MyServiceB, {optional: true}); + } + + @Component({ + selector: 'app-cmp', + template: ` + + + `, + standalone: true, + imports: [NgComponentOutlet], + }) + class AppCmp { + ComponentA = ComponentA; + ComponentB = ComponentB; + } + + const fixture = TestBed.createComponent(AppCmp); + fixture.detectChanges(); + + const textContent = fixture.nativeElement.textContent; + expect(textContent).toContain('ComponentA: service found'); + expect(textContent).toContain('ComponentB: service found'); + }); + it('should dynamically insert a standalone component', () => { class Service { value = 'Service';