Skip to content

Commit

Permalink
fix(core): ensure that standalone components get correct injector ins…
Browse files Browse the repository at this point in the history
…tances (#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
  • Loading branch information
AndrewKushnir authored and dylhunn committed Jul 10, 2023
1 parent 5528b72 commit 031b599
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 6 deletions.
8 changes: 4 additions & 4 deletions packages/core/src/render3/features/standalone_feature.ts
Expand Up @@ -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<string, EnvironmentInjector|null>();
cachedInjectors = new Map<ComponentDef<unknown>, EnvironmentInjector|null>();

constructor(private _injector: EnvironmentInjector) {}

Expand All @@ -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() {
Expand Down
67 changes: 65 additions & 2 deletions packages/core/test/acceptance/standalone_spec.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -217,6 +217,69 @@ describe('standalone components, directives, and pipes', () => {
.toEqual('Outer<inner-cmp>Inner(Service)</inner-cmp>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: `
<ng-container [ngComponentOutlet]="ComponentA" />
<ng-container [ngComponentOutlet]="ComponentB" />
`,
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';
Expand Down

0 comments on commit 031b599

Please sign in to comment.