8

According to my understanding of Angular 2 rc5, to make a service from another module (not AppModule) available as a singleton to every component, even those lazy-loaded, we don't include the service in the providers array of that other module. We instead export it with RouterModule.forRoot() and import the result in AppModule

According to the docs:

The SharedModule should only provide the UserService when imported by the root AppModule. The SharedModule.forRoot method helps us meet this challenge...the SharedModule does not have providers...When we add the SharedModule to the imports of the AppModule, we call forRoot. In doing so, the AppModule gains the exported classes and the SharedModule delivers the singleton UserService provider at the same time

I'm really struggling with how to make a 3rd-party service (a service used by a module in the imports array of my AppModule) available to lazy loaded routes. I have no control over this 3rd-party module, so I cannot just remove that service from the NgModule.providers array of that module and place it inside RouterModule.forRoot() as I would with one of my services.

The specific service is MdIconRegistry, which is in providers for the MdIconModule of Angular Material 2 alpha 7-3. This service is used to register svg icons that can then be displayed on the page with the <md-icon svgIcon='iconName'> tag. So:

  • I imported MdIconModule in my root AppModule
  • I used the service in question to register svg icons in my AppComponent

The icon is visible and works well, but only in the modules that were loaded at launch. Lazy-loaded modules cannot see these icons, so I suspect that the Angular injector is not injecting the same instance of the MdIconRegistry service.

tl;dr: How can I make the service from a 3rd-party module a singleton available to my lazy-loaded components?

Here is a plunker that demonstrates the problem (coded in typescript).

PS: This just got the attention of the MdIconModule developer on github.

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165

2 Answers2

11
I do not think it has anything to do with the component being lazy-loaded.

LazyLoadedComponent is not part of the AppModule – it is part of the LazyModule. According to the docs, a component can only be part of one module. If you try adding LazyLoadedComponent to AppModule also, you would get an error to that effect. So LazyLoadedComponent is not even seeing MdIconModule at all. You can confirm this by looking at the template output in the debugger – it is unchanged.

<md-icon svgIcon="play"></md-icon>

The solution appears to be adding the MdIconModule to the LazyModule, and while this alone does not fix the problem, it does add an error to the output.

Error retrieving icon: Error: Unable to find icon with the name ":play"

And the template output now looks like this, so we know it is loading.

<md-icon role="img" svgicon="play" ng-reflect-svg-icon="play" aria-label="play"></md-icon>

I added the call to addSvgIconSet from LazyLoadedComponent, and that got it working… so this proves there is an instance of the MdIconRegistry service per component – not what you want, but may point you in the right direction.

Here’s the new plunk - http://plnkr.co/edit/YDyJYu?p=preview

After further review, I found this in the docs:

Why is a service provided in a lazy loaded module visible only to that module?

Unlike providers of the modules loaded at launch, providers of lazy loaded modules are module-scoped.

Final Update! Here is the answer. MdIconModule is not properly setup for lazy loaded components... but we can easily create our own module that IS properly set up and use that instead.

import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';

import { MdIcon } from '@angular2-material/icon';
import { MdIconRegistry } from '@angular2-material/icon';

@NgModule({
  imports: [HttpModule],
  exports: [MdIcon],
  declarations: [MdIcon]
})
export class MdIconModuleWithProviders {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: MdIconModuleWithProviders,
      providers: [ MdIconRegistry ]
    };
  }
}

Plunk updated and fully working. (sorry, updated the same one) -> http://plnkr.co/edit/YDyJYu?p=preview

One might submit a pull request such that Angular Material exports modules of both styles.

James
  • 2,823
  • 22
  • 17
  • Hey James thanks a lot for your feedback. You make a good point about importing `MdIconModule` to `LazyModule` and I will upload my plunker to reflect it. There is still a problem: as you found out, you need to call `addSvgIconSet()` again because the `MdRegistryService` is not a singleton. Loading the icon set in each module is not practical however because it means that a new http request for the same `svg` file is made at every route change. My initial question remains: How can I make this service (`MdIconRegistry`) a singleton available to `LazyModule`? – BeetleJuice Aug 18 '16 at 18:33
  • Correct, I updated the answer. It looks like this DOES have to do with the lazy loading, and the scope is restricted by design. – James Aug 18 '16 at 19:00
  • Angular provides facilities for making a service a singleton that applies even to lazy routes (I discuss it with links in the OP). This requires that modules be written a certain way (services should not be placed in the `providers` array or a new instance will be injected into everything that imports the module). The challenge here is that I'm faced with a service that is only useful as a singleton, but whose module is not written the "right way" and whose module I have no control over. I also reported it here: https://github.com/angular/material2/issues/1071 – BeetleJuice Aug 18 '16 at 19:13
  • Please see the updated answer, I got it working with a new module. – James Aug 18 '16 at 20:20
  • Great job on this James!! I'll pass it along. – BeetleJuice Aug 18 '16 at 21:34
0

New to Angular 6 there is a new way to register a provider as a singleton. Inside the @Injectable() decorator for a service, use the providedIn attribute. Set its value to 'root'. Then you won't need to add it to the providers list of the root module, or in this case you could also set it to your MdIconModuleWithProviders module like this:

@Injectable({
  providedIn: MdIconModuleWithProviders // or 'root' for singleton
})
export class MdIconRegistry {
...
thenninger
  • 506
  • 3
  • 16
  • 1
    Thanks for your feedback. I don't think this would have solved my problem though because `MdIconRegistry` is a 3rd-party class over which I do not have control. Thankfully, the 3rd party module has since been updated so that I can import `Module.forRoot()` where I need the service (my `AppModule`) and `Module.forChild()` in my lazy-loaded modules. `forRoot()` provides the service, but `forChild()` does not. – BeetleJuice Jun 25 '18 at 00:10