Skip to content

Instantly share code, notes, and snippets.

@dirkluijk dirkluijk/Sharing Angular modules.md Secret
Last active Dec 11, 2018

Embed
What would you like to do?
Sharing Angular modules

Share your Angular modules like a boss

What forRoot and forChild have to do with lazy-loading

You just created a beautiful Angular component. As a good developer, 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 customisable. Dependency injection is the key to success! However, be careful with lazy loading.

In this article, I want to show you some tips and tricks for configuration in Angular. Let's make your modules 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 the prefix configurable.

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

Now, 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: '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 {}

We will now have 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. And 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() instead:

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
    };
  }
}

The benefit is that we can now 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.