Working with Angular and Typescript ; as I have refactored and re-written components I’ve been learning to take advantage of one of the more powerful features - dependency injection.

Building Angular components, most developers will have already used this to inject features like the HTTP client or the FormBuilder to be used in a component. A common service example might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class RequestService {
  constructor(private readonly http: HttpClient) {}

  public getExample(): Observable<HttpResponse> {
    return this.http.get('/api/example');
  }
}

Angular’s own features can be injected via their class identity (e.g. HttpClient). You can also inject any components, services or pipes that are registered within a module.

Angular also provides a mechanism for custom injections, this means items like configurations or instances of external libraries can be passed into any constructor within your module or any item within the dependency tree. To do this the library needs to provide an InjectionToken which represents the key in the dependency tree, and use the Inject decorator to provide an instance into a class constructor (this is covered later in this post).

Creating The Service

I recently worked on refactoring some code where in a component it had some code to handle getting a session object from sessionStorage. When working with Angular’s ng serve making changes will often result in a refresh, destroying any existing session. This feature allowed session storage to be turned on and keep it between refreshes by allowing the component to check if a value existed in sessionStorage object.

Using the principles in SOLID I moved the code into a service. Having a service do one thing it made the code easier to understand. The unit test for this is also smaller and can cover most of the component.

The service has one additional property which is storagePrefix, used to make each instance of the service unique to it’s component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { Injectable } from '@angular/core';

@Injectable()
export class ComponentService {
  private storagePrefix = 'my-app';

  public init(storagePrefix: string) {
    this.storagePrefix = storagePrefix;
  }

  public load(key: string): string | undefined {
    return sessionStorage.getItem(`${this.storagePrefix}-${key}`);
  }

  public save(key: string, value: string): void {
    sessionStorage.setItem(`${this.storagePrefix}-${key}`, value);
  }
}

In it’s current implementation it’s usable, but it has a few issues. The main one is that it uses sessionStorage but trusts that it exists on the global object, this means we should always expect it to be available, and is a bad practice. It also limits this service to only use sessionStorage and no other key/value store. It would be better if we could provide it to our service instead.

We can also set the storagePrefix via our app configuration and means our component doesn’t need to understand this configuration (yet!) so it avoids us having to call the init method within a component’s ngOnInit.

By making these injectable, we will not only be able to provide our sessionStorage but we can support any module or library that conforms to the Web Storage API . This means we no longer need to rely on the global object and we have more control over what we inject.

The powerful feature of dependency injection is the ability to pass instances of other non-angular applications. For example you could pass the instance of a jQuery application and call it’s methods from your application (you’ll also want to check out NgZone when running these, but I’ll discuss that in a future post).

This allows for the transition of applications over time within an organisation, rather than having to replace all applications in one go. It’s a good way to safely inject features from third-party libraries and means passing mock versions is easier for tests.

Creating an injectable service and module

Making features injectable

First we need to define the interface of the object want to use for the configuration. This interface provides a contract in the forRoot method described below and tells developers what options they can provide to the application

Our configuration will require a storagePrefix as a string - this is need to set the name of the store. We also pass our Storage instance, but with this we can set a default value in our component

1
2
3
4
export interface ComponentServiceConfig {
  storagePrefix: string;
  storageInterface?: Storage;
}

Next we will create an InjectionToken, a token that is usually constant representation of something we want to provide via dependency injection. You can use any type of object as your token type, and can even provide a factory method to provide dependencies. Check out the Tree-shakeable Injection Token to see how you can do some advanced use. In our case we will just have the component config object.

1
2
import { InjectionToken } from '@angular/core';
const COMPONENT_SERVICE_CONFIG = new InjectionToken<ComponentServiceConfig>('COMPONENT_SERVICE_CONFIG');

Now we have the token service we will provide a simple configuration with this token which will be an object, but a token can be used to inject anything from a string to a function, or even a class instance.

Making the Angular module configurable

To make the service configurable, it should be provided via a NgModule - a regular module might look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
// Module specific imports ...

@NgModule({
  imports: [CommonModule, ReactiveFormsModule],
  declarations: [FeatureFormComponent, FeatureViewComponent, FeaturePipe],
  exports: [FeatureFormComponent, FeatureViewComponent],
  provides: [ComponentService]
})
export class LibraryModule {}

To make this module accept a configuration we need to add a method that can return an instance of our module with custom providers. Providers are an important part of Angular’s dependency injection as these are the instance of what child components can inject - all will hopefully become clear through the code example.

First we will add a static forRoot method to the class. This method provides a factory function where we can pass a configuration in the application imports.

The factory returns an instance of that module with the provided configuration, and this passed into the items with Inject . The module can then be further used without the forRoot as long as it’s configured at the root of the application (see SkipSelf )

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { NgModule, ModuleWithProviders, Optional, SkipSelf } from '@angular/core';

// Here we can create a default object
const defaultConfig: ComponentServiceConfig = {
  storagePrefix: '',
  storageInterface: sessionStorage; // In our default value we will trust the global is there 
}

@NgModule({
  ...
})
export class LibraryModule {
  constructor(
    @Optional()
    @SkipSelf()
    parentModule: LibraryModule,
  ) {}
  static forRoot(config?: ComponentServiceConfig): ModuleWithProviders {
    return {
      ngModule: LibraryModule,
      providers: [
        {
          provide: COMPONENT_SERVICE_CONFIG,
          // Destructring allows the value of the config to be merged
          // this is a shallow operation so keep the objects simple
          useValue: { ...defaultConfig, ...config },
        },
      ],
    };
  }
}

Before we can use this in our application we also need to update the ComponentService itself to use this configuration. Here the Inject decorator will be used to tell the constructor that the ComponentServiceConfig this service needs will be available to inject via the COMPONENT_SERVICE_CONFIG token.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Inject } from '@angular/core';

export class ComponentService {
  constructor(@Inject(COMPONENT_SERVICE_CONFIG) private readonly config: ComponentServiceConfig) {}

  public load(key: string): string | undefined {
    return this.config.storageInterface.getItem(`${this.config.storagePrefix}-${key}`);
  }

  public save(key: string, value: string): void {
    this.config.storageInterface.setItem(`${this.config.storagePrefix}-${key}`, value);
  }
}

As the config is set with private in the constructor, TypeScript knows to set this as a property on the class. This is the same as writing the following code and provides a clean shorthand to set up properties.

1
2
3
4
5
6
7
export class ComponentService {
  private config: ComponentServiceConfig;

  constructor(config: ComponentServiceConfig) {
    this.config = config;
  }
}

Providing the features in the app

Now that we have our configurable module, the Angular application can be provided with a configuration when importing.

As the module now accepts any Storage type, we’ll instead provide localStorage for this application, along side the storagePrefix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot([], routerConfig),
    LibraryModule.forRoot({
      storageInterface: localStorage,
      storagePrefix: 'my-new-app'
    })
  ],
  providers: [...environment.mockProviders],
  bootstrap: [AppComponent]
})
export class AppModule {}

Another example is we could have an NPM library that provides the same interface, such as localStorage-ponyfill , it could be used in your application - or you may want to provide it to a TestBed configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Using it in our application
import { createLocalStorage } from "localstorage-ponyfill";
const fakeLocalStorage = createLocalStorage({ mode : "memory" });

@NgModule({
  imports: [
    ...
    LibraryModule.forRoot({
      storageInterface: fakeLocalStorage,
      storagePrefix: 'my-new-app'
    })
    ...
  ]
})
export class AppModule {}

// Or writing some tests

TestBed.configureTestingModule({
  declarations: [TestHostComponent],
  providers: [
    {
      provide: BB_FORMS_CONTAINER_CONFIG,
      useValue: {
        storageStrategy: fakeLocalStorage,
      },
    },
  ]
});

With this technique, any kind of primitive or complex object, method or class can now be passed into your feature modules - and is a useful way to provide third-party libraries to your modules, configuring not just your services, but also components, pipes and stores.

Any class with a constructor (provided it’s in the same dependency tree) can use Inject with your exported token to access this configuration.

Hopefully you will also find this technique useful to improve the configurable and testable features of your module. Feel free to respond on twitter if you spot any typos or errors.