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 anInjectorandNgModuleReffor which a “switch” injector is created (in purple). The numbers show the path that is followed when injectingNgModuleRefand the injector with a thicker outline is theNgModuleRefthat 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 theAppModuleis not considered after step 5, but instead the resolution continues from the first purple injector. This allows the resolution to properly resolveNgModuleReffromLazyModuleas 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 theNgModuleitself, 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 isLazyModin 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,AppModulewill happily resolve its ownNgModuleRef<AppModule>and that will be injected, whereasAppModuleRef<LazyModule>was expected.The suggestion proposed by @gkalpak of changing
childInjectorintoInjector.NULLdoes 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 atLazyModas desired. However this approach has a major flaw: a custom injector created usingInjector.createthat provides additional tokens will be ignored altogether, which is most certainly an issue.One could argue that
ComponentFactory.createcould inspect theinjectorto 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
ContentModuleinFeatureModuleand therefore it should work.The problem seems to be how we create the component in
@angular/elementsand 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 onInjectors, 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
AppModuleto injectNgModuleRef<unknown>instead ofInjectorand 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 theContentServiceis provided). InFeatureRoutingModuleyou load theContentComponentdirectly, soContentModuleis never loaded into the app.For service provided in
@NgModule.providersto 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 inContentModuleimportRouterModule.forChild([{path: '', component: ContentComponent}]).ContentServicein theFeatureModule(which is already loaded into the app).ContentServiceon 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