Benjamin M
Benjamin M

Reputation: 24567

Angular, Lazy Loading, Inter-Module Dependencies, best practice

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:


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) { ... }
}

Upvotes: 3

Views: 1223

Answers (3)

Benjamin M
Benjamin M

Reputation: 24567

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!

Upvotes: -1

Michael Kang
Michael Kang

Reputation: 52867

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.

Upvotes: 4

Hasnain Bukhari
Hasnain Bukhari

Reputation: 468

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.

Upvotes: 2

Related Questions