3

I have following example application layout:

app.module
 |
 |-- dashboard.module                 (lazy loaded)
 |-- dashboard-routing.module         (imported by dashboard.module)
 |-- dashboard.service                (provided by dashboard.module)
 |-- dashboard.component              (depends on dashboard.service)
 |
 |-- notifications.module             (lazy loaded)
 |-- notifications-routing.module     (imported by notifications.module)
 |-- notifications.service            (provided by notifications.module)
 |-- notifications.component          (depends on notifications.service)
 |
 |-- my-notifications-dashboard-widget.component <- this should be added
 |
 |-- ...

Every module (dashboard, notifications, ...) is getting lazy loaded.

Now I'd like to add some MyNotificationsDashboardWidgetComponent, which depends on the notifications.service. This widget then should get injected within the dashboard.service.

This works well, as long as the provided WidgetComponent is inside the dashboard.module. But if I put the component within the lazy loaded notifications.module, it of course doesn't work.

Now I'd like to know, how to solve this in a good, module oriented way.

My requirements:

  • There should only be one notifications.service, which should be used by notifications.component and my-notifications-dashboard-widget.component.

  • The big modules (dashboard, notifications) should be lazy loaded.

  • The dashboard.module shouldn't know anything about the notifications.module and / or the my-notifications-dashboard-widget.component.


Ideas:

I guess, that I need some eager loaded module, which contains my-notifications-dashboard-widget.component. So that this component and its provider is known before the dashboard.module is getting loaded.

But then I'd have to put the notifications.service in this module, too, I guess?!


Some code snippets of the current (non-working) approach:

dashboard.service:

@Injectable()
export class DashboardService {
    constructor(@Inject(DASHBOARD_WIDGET) widgets) {
        // inject all provided widgets
        widgets.forEach(w => console.log(w.type));
    }
}

notifications.module:

@NgModule({
    imports: [ NotificationsRoutingModule ],
    declarations: [ NotificationsComponent ],
    providers: [
        NotificationsService,
        // this is currently not working, because of lazy loading
        {
            provide: DASHBOARD_WIDGET,
            useValue: {
                type: 'my-notifications'
                component: MyNotificationsDashboardWidgetComponent
            },
            multi: true
        }
    ],
    entryComponents: [ MyNotificationsDashboardWidgetComponent ]
})
export class NotificationsModule { }

notifications.component:

@Component({ ... })
export class NotificationsComponent {
    constructor(service: NotificationsService) { ... }
}

my-notifications-dashboard-widget.component:

@Component({ ... })
export class MyNotificationsDashboardWidgetComponent {
    constructor(service: NotificationsService) { ... }
}

3 Answers 3

4

Keep in mind that providers at the NgModule level are always registered with the root injector. There is only one root injector, and any provider registered with the root injector is available application-wide. The exception to this rule is lazy-loaded modules - lazy loaded modules create their own scoped injector. In other words, any providers registered with a module that is lazy-loaded will not be registered with the root injector.

In order to register providers with the root injector, regardless of whether or not they are lazy-loaded, you should follow the forRoot convention. The idea is that the forRoot method returns the module with providers, so that it can be imported by AppModule.

@NgModule({
    providers: []
})
export class MyModule {
     static forRoot(): ModuleWithProviders {
         return {
             NgModule: MyModule,
             providers: [
                 MyProvider
             ]
         }
     }
}

Note: There is also the forChild method you can implement that returns a module without providers. The intention for this method is so that you can re-use components from a module (including lazy-loaded modules) without actually registering any providers.

Sign up to request clarification or add additional context in comments.

6 Comments

That sounds promising. Could you elaborate on this? If I include DashboardModule.forRoot() within my AppModule, then I'd have my whole dashboard module loaded. But I guess, I just need the service which injects the providers. And then can later lazy load the rest of the dashboard module (components, etc.)
Its just a convention. If you're concerned about registering components unnecessarily (because its only needed from a lazy loaded module) forRoot can return any module you like, including an empty module that only has providers. The forRoot/forChild convention exists so that you can register providers with the root injector, even when modules are lazy loaded.
Thank you. I understand forRoot/forChild. But: How should I register MyNotificationsDashboardWidgetComponent? I can't call forChild from nofications.module, because this module is lazy loaded. Or can I?
Sure, you can. You can import forChild from any module, and leverage its components (just make sure forChild doesn't return any providers that are intended to be application-wide singletons)
I'll start again: notifications.module is lazy loaded. dashboard.module will be eager laded using forRoot. When I now have import: [ DashboardModule.forChild(...) ] within the lazy loaded notifications.module, then it will execute the forChild only when I hit the /notifications route. That's too late. I must have the forChild loaded before the DashboardModule loads, so the dashboard knows all available widget types (which are provided by other (lazy loaded) modules, i.e. the notifications.module)
|
2

In this case you are creating only one widget and in future, you might need more widgets to add.

Solution is, Make anohter shared module and create such component in that module. New shared module will be independent of other modules and all components inside it.

Import shared module in other modules and use it as you want.

1 Comment

Yeah, of course it is meant for multiple widgets. I just didn't want to pollute the question with 500 additional lines. ... And what about the services? Maybe you could add some example of the layout?
-1

Thanks for your inspiration. I've now created a working solution. It still looks a bit strange to me, and some things are a mystery to me, but at least it works.

Would be nice, if you could leave a comment with some additional information

app.module:

@NgModule({
    imports: [
        AppRoutingModule,
        NotificationModule.forRoot()
    ]
})
export class AppModule { }

app-routing.module:

@NgModule({
    imports: [
        RouterModule.forRoot([
            {
                path: '/dashboard',
                loadChildren: 'app/dashboard/dashboard.module#DashboardModule'
            },
            {
                path: '/notifications',
                loadChildren: 'app/notification/notification.module#NotificationModule'
            }
        ])
    ]
})
export class AppRoutingModule { }

dashboard.module:

@NgModule({
    providers: [ DashboardService ]
})
export class DashboardModule {
    provideWidget(type: string, component: Type<any>) {
        return [
            {
                provide: ANALYZE_FOR_ENTRY_COMPONENTS,
                useValue: component,
                multi: true
            },
            {
                provide: DASHBOARD_WIDGET,
                useValue: {
                    type: type,
                    component: component
                },
                multi: true
            }
        ];
    }
}

notification.module:

@NgModule({
    imports: [
        NotificationRoutingModule
    ],
    declarations: [
        NotificationComponent
    ]
})
export class NotificationModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: NotificationRootModule,
            providers: [ ]
        }
    }
}

notification-root.module:

@NgModule({
    declarations: [
        TestWidgetComponent
    ],
    providers: [
        NotificationService,
        DashboardModule.provideWidget('TEST', TestWidgetComponent)
    ]
})
export class NotificationRootModule { }

Questions:

I guess I haven't understood forRoot and ModuleWithProviders completely. I don't understand why I have to create a separate NotificationRootModule for this to work.

Now the TestWidgetComponent and the NotificationService are compiled into the app module. Though everything works as expected.

But: If I instead use ngModule: NotificationModule within NotificationModule.forRoot(), then it will put the whole module (including NotificationRoutingModule and NotificationComponent) inside the app module. (I can see that the browser doesn't lazy load anything when opening /notifications URL.)

And now I don't know where the providers belong. I can put them within @NgModule({ providers }) of the NotificationRootModule or within the forRoot() { return { providers } }. It seems to make no difference.

I hope you can enlighten me.

Thanks you!

1 Comment

The reason you should implement forRoot is so that you can have a singleton service that is shared application wide. If you don't do it, then your lazy loaded module will create its own copy of the service. This is likely not what you want... or maybe it is.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.