angular: Elements lazy load make unable to use providers in modules
🐞 bug report
Affected Package
The issue is caused by package @angular/elements.
Is this a regression?
I’m not sure.
Description
When you export a Custom Element that contains a lazy load module, you aren’t able to create a service without the providedIn: ‘root’. If you create a service with a @Injectable() and than make the provider in a module under the lazy load module, it’ll break the application and throw a no provider error.
The providers in the module under a Lazy load route
🔬 Minimal Reproduction
https://richardlnnr.github.io/angular-lazy-load-injectable/
https://github.com/richardlnnr/angular-lazy-load-injectable
🔥 Exception or Error
core.js:6228 ERROR NullInjectorError: R3InjectorError(AppModule)[ContentService -> ContentService -> ContentService]:
NullInjectorError: No provider for ContentService!
at NullInjector.get (http://localhost:4200/vendor.js:8310:27)
at R3Injector.get (http://localhost:4200/vendor.js:22304:33)
at R3Injector.get (http://localhost:4200/vendor.js:22304:33)
at R3Injector.get (http://localhost:4200/vendor.js:22304:33)
at NgModuleRef$1.get (http://localhost:4200/vendor.js:39605:33)
at Object.get (http://localhost:4200/vendor.js:37339:35)
at getOrCreateInjectable (http://localhost:4200/vendor.js:12112:39)
at Module.ɵɵdirectiveInject (http://localhost:4200/vendor.js:26119:12)
at NodeInjectorFactory.ContentComponent_Factory [as factory] (http://localhost:4200/feature-feature-module.js:26:162)
at getNodeInjectable (http://localhost:4200/vendor.js:12257:44)
ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(AppModule)[ContentService -> ContentService -> ContentService]:
NullInjectorError: No provider for ContentService!
NullInjectorError: R3InjectorError(AppModule)[ContentService -> ContentService -> ContentService]:
NullInjectorError: No provider for ContentService!
at NullInjector.get (core.js:1085)
at R3Injector.get (core.js:16955)
at R3Injector.get (core.js:16955)
at R3Injector.get (core.js:16955)
at NgModuleRef$1.get (core.js:36329)
at Object.get (core.js:33972)
at getOrCreateInjectable (core.js:5848)
at Module.ɵɵdirectiveInject (core.js:21103)
at NodeInjectorFactory.ContentComponent_Factory [as factory] (content.component.ts:9)
at getNodeInjectable (core.js:5993)
at resolvePromise (zone-evergreen.js:798)
at resolvePromise (zone-evergreen.js:750)
at zone-evergreen.js:860
at ZoneDelegate.invokeTask (zone-evergreen.js:399)
at Object.onInvokeTask (core.js:41632)
at ZoneDelegate.invokeTask (zone-evergreen.js:398)
at Zone.runTask (zone-evergreen.js:167)
at drainMicroTaskQueue (zone-evergreen.js:569)
🌍 Your Environment
Angular Version:
Angular CLI: 9.1.7
Node: 10.18.0
OS: linux x64
Angular: 9.1.9
... animations, common, compiler, compiler-cli, core, elements
... forms, platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.901.7
@angular-devkit/build-angular 0.901.7
@angular-devkit/build-optimizer 0.901.7
@angular-devkit/build-webpack 0.901.7
@angular-devkit/core 9.1.7
@angular-devkit/schematics 9.1.7
@angular/cli 9.1.7
@ngtools/webpack 9.1.7
@schematics/angular 9.1.7
@schematics/update 0.901.7
rxjs 6.5.5
typescript 3.8.3
webpack 4.42.0
Anything else relevant?
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 9
- Comments: 18 (7 by maintainers)
Below is a diagram of the injector tree.
On the left, the typical scenario is outlined. In
ComponentFactory.create
, one can pass anInjector
andNgModuleRef
for which a “switch” injector is created (in purple). The numbers show the path that is followed when injectingNgModuleRef
and the injector with a thicker outline is theNgModuleRef
that is resolved. The numbers in red indicate that this request is “tainted” with a default value, which is interpreted in the purple injectors to instruct them to skip their NgModule injector. This is why theAppModule
is not considered after step 5, but instead the resolution continues from the first purple injector. This allows the resolution to properly resolveNgModuleRef
fromLazyModule
as desired.From the above, it can be observed that the node injector tree and NgModule injector tree are only ever connected through the switch injector which is aware of the tainted resolutions, allowing for the system to work.
The scenario on the right is how things are setup with
@angular/elements
. When registering an NgElement, you pass it anInjector
. This is typically theNgModule
itself, or a derived injector usingInjector.create({ parent: ngModuleInjector })
. This injector will be considered to represent the “view injector”, hence a “tainted” resolution will first attempt to use this injector (step 2) before considered its NgModule injector (which isLazyMod
in this case). However, no switch injector will be hit this time, so there is nothing to detect the magic default that should prevent resolution further up the tree. As such,AppModule
will happily resolve its ownNgModuleRef<AppModule>
and that will be injected, whereasAppModuleRef<LazyModule>
was expected.The suggestion proposed by @gkalpak of changing
childInjector
intoInjector.NULL
does indeed prevent this particular issue, as doing so changes step 2 to end up in the null injector, allowing the switch injector to switch to the NgModule injector tree which starts atLazyMod
as desired. However this approach has a major flaw: a custom injector created usingInjector.create
that provides additional tokens will be ignored altogether, which is most certainly an issue.One could argue that
ComponentFactory.create
could inspect theinjector
to see if it corresponds with a module injector, or a view injector and configure the switch injector accordingly. Unfortunately, I don’t think this can work correctly in the presence of custom injectors, as there is no way to detect what would be the cut-off point between a custom injector and an NgModule injector (essentially the point where the switch injector would typically exist).A possible resolution could be to move the recognition of the magic default value into
NgModuleRef
, so that module injectors themselves will identity the cut-off point instead of the switch injector created byComponentFactory.create
.Errata: where I mention the switch injector the original design doc refers to it as merge injector.
It turns out my analysis in https://github.com/angular/angular/issues/37441#issuecomment-639737971 is completely wrong 😁 I didn’t realize that you are importing
ContentModule
inFeatureModule
and therefore it should work.The problem seems to be how we create the component in
@angular/elements
and in particular how we pass in the injector. This line seems to be the problem:https://github.com/angular/angular/blob/1197965e69d658c92422d49b1f2a1ba90498af67/packages/elements/src/component-factory-strategy.ts#L153
Replacing that with
Injector.NULL
(which is how components are created outside of@angular/elements
) seems to fix the problem. I am not an expert onInjector
s, so I am not sure what is the correct way to fix this (and how much of a breaking change it will be).I have the same problem in my project
@richardlnnr A silly workaround could be to create a “fake” element injector by instantiating a component and using its injector:
Then change
AppModule
to injectNgModuleRef<unknown>
instead ofInjector
and manually create an injector using the above fn:The problem is that you never load_UPDATE: This is incorrect. See https://github.com/angular/angular/issues/37441#issuecomment-646151990._ContentModule
(where theContentService
is provided). InFeatureRoutingModule
you load theContentComponent
directly, soContentModule
is never loaded into the app.For service provided in
@NgModule.providers
to be available you need to ensure that the corresponding module is loaded into the app. In your case, you have the following options:providedIn: 'root'
(which is the recommended way in most cases, as it allows tree-shaking) 😁FeatureRoutingModule
, load theContentModule
(vialoadChildren: () => import('./content/content.module').then(m => m.ContentModule)
) and inContentModule
importRouterModule.forChild([{path: '', component: ContentComponent}])
.ContentService
in theFeatureModule
(which is already loaded into the app).ContentService
on theContentComponent
(via @Component.providers).Closing since everything works as expected, but feel free to continue the discussion below (in case I missed something or you need more info).
I have the same problem here
I tried to correct this project to have a working solution: https://stackblitz.com/edit/angular-ivy-jwxrj1
the trick is to get the new injector in the moduleFatcory obtained from lazyModule.create(). This injector has the LazyService instance to make it available for injection! Hope this could help