Marginalia

Angular 2のPlatform Provider

Angular 2のDependency Injection(DI)は主にサービスクラスのインスタンスを注入するのに用いられますが、 実は他にも便利な使い方がいくつかあります。 今回はその中から PLATFORM_DIRECTIVESPLATFORM_PIPES の使い方を紹介します。

Angular 2のProviderとmultiオプション

まずはじめに、Angular 2のProviderの仕組みについておさらいしましょう。 もしAngular 2のDIがさっぱりわからない方は、先に Angular2のDIを知る を読むといいかもしれません。

Angular 2のDIは基本的に、 トークン に対して値をセットします。 TypeScriptでは「型ベースのDI」とよく言われますが、これは型(型アノテーションに使われているクラス)がトークンになっています。 このトークンはクラスじゃなくてもよくて、文字列でも何でも、オブジェクトであれば何でも許容されます。

providers: [
    MyClass, // 自動的にMyClassがインスタンス化される
    new Provider(MyClass, {useClass: MyClass}), // 上と同義
    new Provider("myValue", {useValue: "value"}, // 文字列をトークンにする 
]

Angular 2では、トークンを定数として提供して、ユーザーが値を自由にセットできるようにしているものがいくつかあります。 代表的なのはAPP_BASE_HREFです。これはAngular 2のLocationがベースパスとして使うバスを設定するためのトークンです

new Provider(APP_BASE_HREF, {useValue: "/basepath/"})

さらにもう一つ重要なのは、@ComponentbootstrapでProviderが要求される場面では、Providerの配列を渡せるということです。 配列を渡した場合は内部で自動的に展開されるので、複数のProviderが依存しあっている場合に1つの配列にまとめることができます。

const MY_PROVIDERS = [
    MyClassA,
    new Provider(MyClassB, {useValue: new MyClassB("initial")}),
    new Provider(MyClassC, {
        useFactory: (myClassA: MyClassA, myClassB: myClassB) => {
            myClassA.init();
            return new MyClassC(myClassA, myClassB);
        },
        deps: [MyClassA, MyClassB]
    }
]

...

providers: [
     MY_PROVIDERS
]

multi オプション

さて、Angular 2のDIの基礎を振り返ったところで、ここから先の話で必要になるのが multiオプションです。 Providerは、同じトークンに対して2回値をセットすると、先にProvideした方は上書きされてしまいます。

[
    MyClass,
    new Provider(MyClass, {useClass: MockMyClass}) // 上書きする
]

multiオプションは、同じトークンに対して複数のProvideを行うときに、上書きではなく 追加 を行います。 次の例では、複数のクラスをまとめるトークンを作り、multiで追加しています。

[
    new Provider(MY_PROVIDERS, {useClass: MyClassA, multi: true}),
    new Provider(MY_PROVIDERS, {useClass: MyClassB, multi: true}),
    new Provider(MY_PROVIDERS, {useClass: MyClassC, multi: true}),
]

このオプションを使うことで、配列をInjectしたい場合にその要素を動的に追加することができます。

PLATFORM_DIRECTIVES トークン

というわけで、ようやく本題に入れます! PLATFORM_DIRECTIVESトークンは、multiオプションでProviderを追加されることを想定してAngular 2が提供しているものです。 その名の通り、プラットフォーム全体で使えるディレクティブを提供するトークンです。

PLATFORM_DIRECTIVES - ts

例えば、アプリケーション全体で使うモーダルのコンポーネント ModalComponent を作ったとしましょう。

@Component({
    selector: "my-modal",
    template: `...`
})
class ModalComponent {
    ...
}

このコンポーネントを別のコンポーネントから使うには、使う側のコンポーネントの directives にクラスを指定しないといけません。

@Component({
    selector: "my-app",
    template: "<my-model></my-model>",
    directives: [ModalComponent]
})
class AppComponent {
}

このModalComponentを他にも多くのコンポーネントから呼び出す時、毎回directivesを設定するのは面倒ですね? そんな時にPLATFORM_DIRECTIVESの出番です。

PLATFORM_DIRECTIVESModalComponentを追加することで、プラットフォームの共通ディレクティブであるとして自動的に解決してくれるようになります。

new Provider(PLATFORM_DIRECTIVES, {useValue: [ModalComponent], multi: true})

もっとも便利なユースケースは、ROUTER_DIRECTIVESでしょう。 angular2/routerが提供しているrouterLink<router-outlet>をコンポーネントから使うには、directives: [ROUTER_DIRECTIVES] という記述が必要です。 ここでPLATFORM_DIRECTIVESを使うと、アプリケーション全体でどこでも使えるようになります。

new Provider(PLATFORM_DIRECTIVES, {useValue: ROUTER_DIRECTIVES, multi: true})

汎用的なコンポーネントやディレクティブを作った時には、ぜひ活用してみてください。

PLATFORM_PIPES

ここまで理解した方ならもう説明の必要はないでしょう。名前の通り、プラットフォーム全体で使えるパイプを定義できるトークンです。 汎用的なパイプを作った時に活用すると、毎回 pipes: [MyPipe] を書く必要はありません。

PLATFORM_PIPES - ts

new Provider(PLATFORM_PIPES, {useValue: [MyPipe], multi:true});

おまけ: OpaqueToken

もし PLATFORM_DIRECTIVESPLATFORM_PIPESというトークンがどうやって作られいるのか気になった方は、無事にAngular 2中級者への階段を踏み出しています! これらは OpaqueToken というクラスのインスタンスになっています。 このクラスは、Providerのトークンとして使いやすいインスタンスを提供してくれます。

OpaqueToken - ts

var t = new OpaqueToken("value");
var injector = Injector.resolveAndCreate([
  provide(t, {useValue: "bindingValue"})
]);
expect(injector.get(t)).toEqual("bindingValue");

定数としてOpaqueTokenのインスタンスを作っておいてアプリケーションで使うようにすると、柔軟なDIを行うことができるでしょう!

まとめ

PLATFORM_DIRECTIVESPLATFORM_PIPESを使うと、 汎用的なコンポーネントやディレクティブ、パイプを毎回宣言することなく、どこでも使えるようになります。 アプリケーションが大きくなってきたらぜひ活用してみてください。