Configurable Services in Angular 2

July 28, 2016in Angular
Configurable Services in Angular 2

Angular 2 has greatly simplified services since Angular 1. In Angular 1, there was a service, factory, provider, delegate, value, constant, etc. and it wasn't always clear when to use which.

For most use cases in Angular 2, services have been greatly simplified. All you need to do is:

  • Create a class with an @Injectable decorator
  • Register it as a provider
import { Injectable } from '@angular/core';
import { bootstrap } from '@angular/platform-browser-dynamic';
import { MyApp } from './my-app.component';

@Injectable()
export class MyService {

  greeting:string='Hello';

  greet(name: string) {
    return `${this.greeting} ${name}`;
  }
}

bootstrap(MyApp,[MyService])

Making use of this service in your component is pretty straight forward. Taking a quick look at our MyApp,

import { Component, OnInit } from '@angular/core';
import { MyService } from './my-service';

@Component({
  selector: 'my-app',
  template: `<div>{{greeting}}</div>`
})
export class MyApp implements OnInit {
  value: number;
  constructor(private _myService:MyService) { }

  ngOnInit() {
    this.greeting = this._myService.greet('Evan Schultz');
  }
}

However, what if you wanted to be able to change the greeting from "Hello" to something else and wanted to be able to configure this when registering the provider with your application or component? With the new router in Angular 2, you may have seen something like:

import { provideRouter } from '@angular/router';
import { APP_ROUTES } from './my.routes';

bootstrap(MyApp,[provideRouter(APP_ROUTES)]);

This is a way of providing some router configuration to the router services in Angular 2. So, let's take a look at how we can create a service that takes some configuration.

Let's modify our service so we provide a greeting to it at the bootstrap time of the application.

import { Injectable } from '@angular/core';

@Injectable()
export class MyService {

  constructor(private greeting:string='Hello') { }

  greet(name: string) {
    return `${this.greeting} ${name}`;
  }
}

If we were to leave our code as is, any component that tries to use MyService would complain that there is no provider registered for String. If you view the example and look at the error logs, you will see this issue.

How do we work around this? We need to create a way for users to register the service with this configuration provided.

To do that, we will create a function called provideMyService, that will use return a provider object that Angular 2 will use to create a provider during the bootstrap phase of the application. This object can let you specify a token or class that you want to create a provider for, and instruct Angular 2 on how to form this object. There are a few options - useClass, useValue, useExisting, useFactory, etc. The use case we are interested in here is useFactory.

export function provideMyService(greeting:string) {
  return { provide: MyService, useFactory: () => new MyService(greeting) }
}

Then, to use this in our application we change the bootstrap code slightly:

import { bootstrap } from '@angular/platform-browser-dynamic';
import { MyApp } from './my-app.component.ts';
import { provideMyService } from './my-service.ts';

bootstrap(MyApp, [provideMyService('Good Day')]);

What we are telling Angular 2 now, is whenever we are injecting MyService, is to use a factory function that will return an instance of MyService with the provided configuration.

Now, what happens if MyService depends on another service that we want to inject? That is where the deps property in our provide comes in. Let's make a Greeter service that will turn our greeting into caps.

import { Injectable } from '@angular/core';

@Injectable()
export class Greeter {
   greet(greeting:string) {
     return greeting;
   }
}
@Injectable()
export class LoudGreeter extends Greeter {
  greet(greeting:string) {
    return greeting.toUpperCase()
  }
}

@Injectable()
export class QuietGreeter {
  greet(greeting:string) {
    return greeting.toLowerCase()
  }
}

Then a quick adjustment to our MyService class:

import { Injectable, provide } from '@angular/core';
import { Greeter } from './greeter';
@Injectable()
export class MyService {

  constructor(private greeting:string='Hello', private greeter: Greeter) {

  }

  greet(name:string) {
    return this.greeter.greet(`${this.greeting} ${name}`);
  }
}

As it stands now, without modifying our provideMyService, we would get an error that greeter is undefined, this is because we are manually creating a new instance of MyService, passing in our greeting - but none of our other dependencies.

To fix that, we can make a quick fix to our provideMyService to indicate that Greeter is a dependency.

export function provideMyService(greeting:string) {
  return { provide:MyService,
           useFactory: (greeter:Greeter) => new MyService(greeting, greeter),
           deps: [Greeter]
         }
}

And, in the bootstrapping of our application - we can now register the default greeter, or replace it with a quiet or loud greeter.

// default greeter
bootstrap(App, [provideMyService('Good Day'), Greeter]);

// loud greeter
bootstrap(MyApp, [
  provideMyService('Good Day'),
  { provide: Greeter, useClass: LoudGreeter }
  ]);

// quiet greeter
bootstrap(MyApp, [
  provideMyService('Good Day'),
  { provide: Greeter, useClass: QuietGreeter }
  ]);

While the creation of services in Angular 2 has been simplified, it is still very flexible in how you are able to use it for more advanced use cases.