Sharing Angular modules like a boss

What forRoot and forChild have to do with lazy loading

You just created a beautiful Angular component. Now, being the exemplary developer that you are, you want to share it with others.

In Angular, you achieve this by moving it to a separate NgModule. The next step is to make your Angular module customizable. Dependency injection is the key to success!

However, be careful with lazy loading.

Please enjoy this walkthrough and these tips and tricks for configuration in Angular. Let’s make it awesome!

Step 1: the FooModule

Imagine a simple FooModule, with a FooComponent:

import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';

@NgModule({
  declarations: [FooComponent],
  exports: [FooComponent],
})
export class FooModule {}

The reason we added the FooComponent to the exports array, is that other modules that import this module, can now use the FooComponent as well.

Now imagine that the FooComponent shows a label with a prefix:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'foo-component',
  template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {
  @Input() label: string;
  prefix = 'Foo';
}

Of course, this is a very simplified example, but you get the idea. This component can now be used as:

<foo-component [label]="'Some label'"></foo-component>

Let’s add some configuration.

Step 2: adding configuration

The second step is to make the prefixconfigurable.

Of course we could just add an input property binding with @Input, like we did for label. However, imagine that we want to configure the prefix globally for every FooComponent.

A good way to achieve this, is with a configuration class and dependency injection:

import { Component, Input } from '@angular/core';
import { FooConfig } from './foo.config'; 

@Component({
  selector: 'foo',
  template: '<h1>{{ prefix }}: {{ label }}</h1>'
})
export class FooComponent {

@Input() label: string; prefix: string; constructor(config: FooConfig) { this.prefix = config.prefix; } }

This FooConfig can be a configuration class, e.g.:

export class FooConfig {
  prefix = 'Foo';
}

If you want to use a TypeScript interface instead, you have to use an InjectionToken. This is because TypeScript interfaces do not exist at runtime.

Let’s provide a default config in our FooModule so that anyone can use this module right away:

import { NgModule } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config'; 

@NgModule({
  declarations: [FooComponent],
  exports: [FooComponent],
  providers: [FooConfig]
})
export class FooModule {}

Anyone importing this FooModule will use the default config. It is also possible to provide a custom configuration (if preferred):

import { NgModule } from '@angular/core';
import { FooConfig, FooModule } from '@foo/lib'; 

@NgModule({
  imports: [FooModule],
  providers: [{
    provide: FooConfig,
    useValue: {
      prefix: 'Custom prefix'
    }
  }]
})
export class AppModule {}

Any FooComponent in this Angular will now use the custom prefix.

Step 3: use in example application

Imagine an app with the following routing structure:

  • AppModule (root module)
    • DashboardModule (route “/dashboard”)
    • UsersModule (route “/users”)

Now, when both DashboardModule and UsersModule need to render FooComponents, they should also both import the FooModule:

@NgModule({imports: [FooModule]}) // (simplified)
export class DashboardModule {}
@NgModule({imports: [FooModule]}) // (simplified)
export class UsersModule {}

However, we still want to have a global configuration, so we keep providing the FooConfig in the root module:

@NgModule({
  imports: [], // (simplified)
  providers: [{
    provide: FooConfig,
    useValue: {
      prefix: 'Custom prefix'
    }
  }]
})
export class AppModule {}

This works fine, because the injector of AppModule is the top-most injector for every other module.

Step 4: optimise for lazy-loading

Now, imagine that the UsersModule of the previous example app is loaded lazily:

const routes: ROUTES = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full'},
  // lazy loading:
  { path: 'users', loadChildren: 'src/app/users/users.module#UsersModule' }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes),
    DashboardModule // contains the dashboard route
  ]
  providers: [{
    provide: FooConfig,
    useValue: {
      prefix: 'Custom prefix'
    }
  }]
})
export class AppModule {}

This creates an issue: the UsersModule – which is loaded lazily – will not inherit the custom config from the root module, but the default config from the FooModule.

What happened?

It seems that a module which is lazily loaded will be the top-most injector at bootstrap time.

To solve this, we have to:

  • Import FooModule WITH a default config in root module (which we can optionally override)
  • Import FooModule WITHOUT a default config in child modules (in order to use its FooComponent)

Instead of making two different modules, Angular provides a ModuleWithProviders interface for this:

import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';

@NgModule({
  declarations: [FooComponent],
  exports: [FooComponent],
  providers: [], // NOTE: no default config here anymore!
})
export class FooModule {}

export const fooModuleWithConfig: ModuleWithProviders = {
  ngModule: FooModule,
  providers: [FooConfig]
};

export const fooModuleWithoutConfig: ModuleWithProviders = {
  ngModule: FooModule
};

Now we can use the fooModuleWithConfig in our root module, and fooModuleWithoutConfig in other modules:

@NgModule({imports: [fooModuleWithConfig]}) // (simplified)
export class AppModule {}
@NgModule({imports: [fooModuleWithoutConfig]}) // (simplified)
export class UsersModule {}

This will solve our issue. While overriding the default config is still possible.

Step 5: stick to conventions

In order to make the intended use more visible, it is recommended to use the static methods forRoot() and forChild():

import { NgModule, ModuleWithProviders } from '@angular/core';
import { FooComponent } from './foo.component';
import { FooConfig } from './foo.config';

@NgModule({
  declarations: [FooComponent],
  exports: [FooComponent],
  providers: [], // NOTE: no default config here anymore!
})
export class FooModule {

  static forRoot(config: FooConfig): ModuleWithProviders {
    return {
      ngModule: FooModule,
      providers: [{ provide: FooConfig, useValue: config }]
    };
  }
  
  static forChild(): ModuleWithProviders {
    return {
      ngModule: FooModule
    };
  }
}

An added benefit of this is that we can even require a configuration object as argument for the forRoot() method (like above), or merge it with a default one.

Final use

In the root module:

@NgModule({
  imports: [FooModule.forRoot({ prefix: 'custom' )]
 })
export class AppModule {}

In any (lazy-loaded) child module:

@NgModule({
  imports: [FooModule.forChild()]
})
export class UsersModule {}

Note: As .forChild() in this case does not return any providers at all, you can also just use the “plain” FooModule in child modules:

@NgModule({
  imports: [FooModule]
})
export class UsersModule {}

Code examples

Sharing is caring: go for it! 

One thought on "Sharing Angular modules like a boss"

  1. Logan says:

    Great Job. This Exactly I was looking for Thanks a Lot.

Leave a Reply

Your email address will not be published. Required fields are marked *