how can I initialize a 3rd party module within my .forRoot() in angular

1.5k Views Asked by At

I am building an internal library for our projects, using Angular 6+. I am using the .forRoot() approach to register global services as documented.

My library will be using ngx-toastr to provide notifications. As I don't want each project to deal directly with all the options of ngx-toastr, I am abstracting most of it behind a notifications service.

The way ngx-toastr works, you can set global options by passing those global options to ToastrModule.forRoot().

how can I configure ToastrModule as part of my own forRoot()?

It's clearly a bad idea to add any code inside the .forRoot(), but is the right way to initialize it directly in my @NgModule()? like this:

@NgModule({
    imports: [ToastrModule.forRoot(/* options go here? */)],
    declarations: [],
    exports: []
})
export class ToolsCoreModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: ToolsCoreModule,
            providers: [],
        };
    }
}

How would this interact if someone on the team decides to also call ToastrModule.forRoot() within their own module initialization?

2

There are 2 best solutions below

0
On

The root module that exports your notifications service via providers should be the only one that uses ToastrModule.forRoot() in an imports declaration.

The forRoot() call is a convention and not something enforced by the framework. Others shouldn't be calling this when defining their NgModules if it has already been called higher up.

As mentioned in the Angular documentation:

Only call and import a .forRoot() result in the root application module, AppModule. Importing it in any other module, particularly in a lazy-loaded module, is contrary to the intent and will likely produce a runtime error.

...

forRoot() and forChild() are conventional names for methods that configure services in root and feature modules respectively.

If your global configuration for ToastrModule never changes, I don't think there's anything wrong doing it the way you've questioned in your snippet.

However, if the configuration might change depending on consumers of ToolsCoreModule, you shouldn't be calling forRoot() for each different configuration, as that's wrong by convention. In this case, you could consider creating wrappers around the third argument of toast calls to pass common configuration (e.g. this.toastrService.error(msg, title, config)).

0
On

As @migh mentioned, forRoot() should only be called by AppModule, but a nested forRoot() within a forRoot() provider is still called by the AppModule, isn't it?

So you can do the following

@NgModule({
    imports: [],
    declarations: [],
    exports: [ToastrModule]
})
export class ToolsCoreModule {
    static forRoot(/* your options */): ModuleWithProviders {
        return {
            ngModule: ToolsCoreModule,
            providers: [...ToastrModule.forRoot(/* toastr options */).providers],
        };
    }
}

How would this interact if someone on the team decides to also call ToastrModule.forRoot() within their own module initialization?

You'll may get a runtime error or a strange behavior.

To avoid the 'strange behavior', you should also add this pattern to your module that prevents double registration:

constructor(@Optional() @SkipSelf() parentModule?: ToolsCoreModule)
{
    if (parentModule)
    {
        throw new Error(
            "ToolsCoreModule is already loaded. Import it in the AppModule only!");
    }
}

That pattern is from the official documentation: https://angular.io/guide/singleton-services#prevent-reimport-of-the-greetingmodule