Quantcast
Channel: Articles by thoughtram
Viewing all articles
Browse latest Browse all 68

Multi Providers in Angular 2

$
0
0

The new dependency injection system in Angular 2 comes with a feature called “Multi Providers” that basically enable us, the consumer of the platform, to hook into certain operations and plug in custom functionality we might need in our application use case. We’re going to discuss what they look like, how they work and how Angular itself takes advantage of them to keep the platform flexible and extendible.

Recap: What is a provider?

If you’ve read our article on Dependency Injection in Angular2 you can probably skip this section, as you’re familiar with the provider terminology, how they work, and how they relate to actual dependencies being injected. If you haven’t read about providers yet, here’s a quick recap.

A provider is an instruction that describes how an object for a certain token is created.

Quick example: In an Angular 2 component we might have a DataService dependency, which we can ask for like this:

import {DataService} from './dataService';

@Component(...)
class AppComponent {

  constructor(dataService: DataService) {
    // dataService instanceof DataService === true
  }
}

We import the type of the dependency we’re asking for, and annotate our dependency argument with it in our component’s constructor. Angular knows how to create and inject an object of type DataService, if we configure a provider for it. This can happen either in the bootstrapping process of our app, or in the component itself (both ways have different implications on the dependency’s life cycle and availability).

// at bootstrap
bootstrap(AppComponent, [
  provide(DataService, {useClass: DataService})
]);

// or in component
@Component({
  ...
  providers: [
    provide(DataService, {useClass: DataService})
  ]
})
class AppComponent { }

In fact, there’s a shorthand syntax we can use if the instruction is useClass and the value of it the same as the token, which is the case in this particular provider:

// bootstrap
bootstrap(AppComponent, [DataService]);

// component
@Component({
  ...
  providers: [DataService]
})
class AppComponent { }

Now, whenever we ask for a dependency of type DataService, Angular knows how to create an object for it.

Understanding Multi Providers

With multi providers, we can basically provide multiple dependencies for a single token. Let’s see what that looks like. The following code manually creates an injector with multi providers:

const SOME_TOKEN: OpaqueToken = new OpaqueToken('SomeToken');

var injector = Injector.resolveAndCreate([
  provide(SOME_TOKEN, {useValue: 'dependency one', multi: true}),
  provide(SOME_TOKEN, {useValue: 'dependency two', multi: true})
]);

var dependencies = injector.get(SOME_TOKEN);
// dependencies == ['dependency one', 'dependency two']

Note: We usually don’t create injectors manually when building Angular 2 applications since the platform takes care of that for us. This is really just for demonstration purposes.

A token can be either a string or a type. We use a string, because we don’t want to create classes to represent a string value in DI. However, to provide better error messages in case something goes wrong, we can create our string token using OpaqueToken. We dont’ have to worry about this too much now. The interesting part is where we’re registering our providers using the multi: true option.

Using multi: true tells Angular that the provider is a multi provider. As mentioned earlier, with multi providers, we can provide multiple values for a single token in DI. That’s exactly what we’re doing. We have two providers, both have the same token but they provide different values. If we ask for a dependency for that token, what we get is a list of all registered and provided values.

Okay understood, but why?

Alright, fine. We can provide multiple values for a single token. But why in hell would we do this? Where is this useful? Good question!

Usually, when we register multiple providers with the same token, the last one wins. For example, if we take a look at the following code, only TurboEngine gets injected because it’s provider has been registered at last:

class Engine { }
class TurboEngine { }

var injector = Injector.resolveAndCreate([
  provide(Engine, {useClass: Engine}),
  provide(Engine, {useClass: TurboEngine})
]);

var engine = injector.get(Engine);
// engine instanceof TurboEngine

This means, with multi providers we can basically extend the thing that is being injected for a particular token. Angular uses this mechanism to provide pluggable hooks.

In Angular 2 we always had to explicitly define directives that are used in our component’s template likes this:

@Component({
  selector: 'app',
  directives: [NgFor], // also CORE_DIRECTIVES
  template: `
    <ul>
      <li *ngFor="#item of items">
        {{item.name}}
      </li>
    </ul>
  `
})
class App { }

This is not needed anymore since alpha.46. All directives provided by the platform (now called PLATFORM_DIRECTIVES) are available in our component’s template right away. In addition to that, it turns out that PLATFORM_DIRECTIVES is a token that configures a multi provider. And since we’ve learned what multi providers are, we know that we can use that token to extend what is being injected for it with our own custom values and dependencies!

In other words, if we have a couple of directives that should automatically be available in our entire application without anyone having to define them in component decorations, we can do that by taking advantage of multi providers and extending what is being injected for PLATFORM_DIRECTIVES. All we have to do is to add multi providers for our directives like this:

@Directive(...)
class Draggable { }

@Directive(...)
class Morphable { }

@Component(...)
class RootCmp { }

// at bootstrap
bootstrap(RooCmp, [
  provide(PLATFORM_DIRECTIVES, {useValue: Draggable, multi: true}),
  provide(PLATFORM_DIRECTIVES, {useValue: Morphable, multi: true})
]);

Multi providers also can’t be mixed with normal providers. This makes sense since we either extend or override a provider for a token.

Other Multi Providers

The Angular platform comes with a couple more multi providers that we can extend with our custom code. At the time of writing these were

  • PLATFORM_PIPES - Basically same as PLATFORM_DIRECTIVES just for pipes
  • NG_VALIDATORS - Interface that can be implemented by classes that can act as validators
  • PLATFORM_INITIALIZER - Can be used to perform initialization work
  • EVENT_MANAGER_PLUGINS - Plugins to extend the event system with custom events and behaviour (this might be refactored with a different system soon)

Conclusion

Multi providers are a very nice feature to implement pluggable interface that can be extended from the outside world. The only “downside” I can see is that multi providers only as powerful as what the platform provides. Platform pipes, directives etc. are implemented right in the platform, which is the only reason we can take advantage of those particular multi providers. There’s no way we can introduce our own custom multi providers (with a specific token) that influences what the platform does, but maybe this is also not needed. Time will tell.


Viewing all articles
Browse latest Browse all 68

Trending Articles