DI problem with standalone component
Which @angular/* package(s) are the source of the bug?
common
Is this a regression?
Yes
Description
I have a standalone component AppComponent and a module GuideModule and use createApplication() and @angular/elements to create custom element. I got an error when angular instantiate GuideListComponent (.in GuideModule ). Why it happens, I imported MatDialogModule in GuideModule.
AppComponent*
@Component({ standalone: true, selector: 'pz-creator-app', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], encapsulation: ViewEncapsulation.ShadowDom, imports: [ CommonModule, // Features GuideModule, RouterModule, ], }) export class AppComponent implements OnDestroy {
GuideModule
@NgModule({ declarations: [ GuideListComponent, GuideEditorComponent, SettingsDialogComponent, ], imports: [ MatDialogModule, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], exports: [], }) export class GuideModule {}
main.ts
createApplication({ providers: [ INTERCEPTORS, GLOBAL_CONFIG, importProvidersFrom([ HttpClientModule, MatSnackBarModule, AppRoutingModule, BrowserModule, BrowserAnimationsModule, CampaignDataAccessModule.forRoot(), ]), ], }).then((appRef) => { const ElementConstructor = createCustomElement(AppComponent, { injector: appRef.injector, }); customElements.define('pz-creator-app', ElementConstructor); });
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
`
ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(Environment Injector)[MatDialog -> MatDialog]:
NullInjectorError: No provider for MatDialog!
NullInjectorError: R3InjectorError(Environment Injector)[MatDialog -> MatDialog]:
NullInjectorError: No provider for MatDialog!
at NullInjector.get (main.js:164607:21)
at R3Injector.get (main.js:165110:27)
at R3Injector.get (main.js:165110:27)
at ChainedInjector.get (main.js:173498:32)
at lookupTokenUsingModuleInjector (main.js:161055:31)
at getOrCreateInjectable (main.js:161107:10)
at Module.ɵɵdirectiveInject (main.js:169929:10)
at NodeInjectorFactory.GuideListComponent_Factory [as factory] (main.js:872:559)
at getNodeInjectable (main.js:161318:38)
at instantiateRootComponent (main.js:172027:21)
at resolvePromise (polyfills.js:1438:19)
at resolvePromise (polyfills.js:1385:9)
at polyfills.js:1512:9
at ZoneDelegate.invokeTask (polyfills.js:459:171)
at Object.onInvokeTask (main.js:187695:25)
at ZoneDelegate.invokeTask (polyfills.js:459:54)
at Zone.runTask (polyfills.js:220:37)
at drainMicroTaskQueue (polyfills.js:664:23)
`
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 14.2.2
Node: 18.3.0 (Unsupported)
Package Manager: npm 8.11.0
OS: darwin x64
Angular: 14.2.1
... animations, cdk, common, compiler, compiler-cli, core
... elements, forms, language-service, material
... platform-browser, platform-browser-dynamic, router
... service-worker
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1402.2
@angular-devkit/build-angular 14.2.2
@angular-devkit/core 14.2.2
@angular-devkit/schematics 14.2.2
@angular/cli 14.2.2
@angular/flex-layout 14.0.0-beta.40
@schematics/angular 14.2.2
rxjs 7.5.4
typescript 4.7.3
Anything else?
For a workaround, it works when I extract providers of GuideModule and add them to createApplication()
main.ts
createApplication({ providers: [ INTERCEPTORS, GLOBAL_CONFIG, importProvidersFrom([ ... GuideModule, ]), ], }).then((appRef) => { const ElementConstructor = createCustomElement(AppComponent, { injector: appRef.injector, }); customElements.define('pz-creator-app', ElementConstructor); });
Stackblitz
Could you please create a minimal reproduce scenario in stackblitz. It is impossible to determine a root cause without a runnable reproduce scenario.
@pkozlowski-opensource https://stackblitz.com/edit/angular-ivy-c4szz7?file=src%2Fapp%2Fapp.component.html
This seems to be an interesting case, which I agree behaves unexpectedly.
The router creates the routed component dynamically, entering this code path:
https://github.com/angular/angular/blob/dbb0dfe70731b1050ed7a7d6892ec01c3c8a0513/packages/core/src/linker/view_container_ref.ts#L408-L413
Here, this.parentInjector is a node injector that with a the ChainedInjector of the standalone component as parent; it chains the component's environment injector (containing a provier for MatDialog) with a node injector, with the node injector being requested first.
Then, requesting the EnvironmentInjector through the ChainedInjector we end up looking in the node injector first, which arrives at the EnvironmentInjector that was used during setup of the custom element: the application injector. The standalone environment injector that contains MatDialog will not have been consulted at this point, meaning that from that moment onwards the router has created the dynamic component in the environment of the app, not of the standalone component.
I believe this is the same as, or at least very similar to, https://github.com/angular/angular/issues/37441#issuecomment-646992679. It's unfortunate that this issue still manifests itself with standalone.
@pkozlowski-opensource and myself looked into this again today, and we've come to the conclusion that this is working as designed (although confusing).
What happens is that the <router-outlet> creates the routed component GuideListComponent in the context of the environment injector of the RouterModule, which is the root injector that is created using createApplication. GuideListComponent is not itself a standalone component, so there is no guarantee that the providers from its NgModule graph are present as would be the case with standalone components (standalone components were specifically designed to avoid this pitfall).
The injector that actually contains a provider for MatDialog is the standalone injector of AppComponent, but as previously mentioned the router uses its own environment injector, which is the root environment. Therefore, the standalone injector of AppComponent is not visible to the routed component instance.
One solution could be to make GuideListComponent itself standalone, and importing MatDialogModule from its `import array.
We do recognize that this behavior can be confusing and is not ideal, but using standalone components across the board aims to avoid cases like these. In a hybrid setup like the reproduction, the interaction between the various DI hierarchies becomes hard to reason about.
Thanks for the explanation @JoostK I think Angular team should have a guide for this problem in the official document. Because importing a module into a standalone component is a common case.
This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.