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.