【Angular】ComponentでインジェクションするServiceを切り替えられるようにしてみる

Angularを触っていて、同じコンポーネントでサービスを切り替えたい時ってありますよね？
一般ユーザー向けと管理者用で画面は一緒だけど、中身は変えたいとか。

サービスに`isAdmin`とか持たせちゃいます？
色んな関数で`if(isAdmin)`とかやっちゃいます？

実はサービスを切り替えられるんです。

# Angularプロジェクトを作成する

```
ng new service-test --routing
```

# 画面の追加

## コンポーネントの追加

- サービスからデータを取得して、表示するようにします

```src/app/page1/page1.component.ts
import { Component } from '@angular/core';
import { Page1Service } from './page1.service';

@Component({
    selector: 'app-page1',
    template: `
        <h1>type: {{type}}</h1>
        <p>{{data}}</p>
    `,
})
export class Page1Component {
    constructor(private service: Page1Service) { }

    get type() { return this.service.type; }
    get data() { return this.service.getData(); }
}
```

## サービスの追加

- 一般ユーザー向けサービスを作成します

```src/app/page1/page1.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class Page1Service {
    type = 'general';

    getData() {
        return 'これは一般ユーザー向けです';
    }
}
```

- 次に管理者用サービスを作成します
    - Page1Serviceを継承して作成します（継承しなくても変数名やメソッド名が一致していれば問題ないと思います）
    - abstractクラス作るのが一番安全だと思います。

```src/app/page1/page1-for-admin.service.ts
import { Injectable } from '@angular/core';
import { Page1Service } from './page1.service';

@Injectable()
export class Page1ForAdminService extends Page1Service {
    type = 'admin';

    getData() {
        return 'これは管理者ユーザー向けです';
    }
}
```

## モジュールの定義

ここが今回の肝です！！！

- providersでuseFactoryを使って、指定のサービスを別のサービスに差し替えます
    - これによりComponentでインジェクションするServiceが差し変わります

```src/app/page1/page1.module.ts
import { NgModule } from '@angular/core';
import { Page1Component } from './page1.component';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { Page1Service } from './page1.service';
import { Page1ForAdminService } from './page1-for-admin.service';
import { Page1RoutingModule } from './page1-routing.module';

@NgModule({
    declarations: [Page1Component],
    imports: [Page1RoutingModule],
    providers: [
        {
            // 置換対象のサービス
            provide: Page1Service,
            // 置換
            useFactory: (route: ActivatedRoute) => {
                const getData = (snapshot: ActivatedRouteSnapshot) => snapshot.firstChild ? getData(snapshot.firstChild) : snapshot.data;
                const isAdmin = getData(route.snapshot)?.isAdmin;
                if (isAdmin) {
                    return new Page1ForAdminService();
                }
                return new Page1Service();
            },
            // useFactoryでインジェクションするサービス
            deps: [ActivatedRoute],
        }
    ]
})
export class Page1Module { }
```

isAdminは後述するルーティングファイルで設定するdata内から取得します


# ルーティングの設定

- Page1Module内のルーティングを定義します

```src/app/page1/page1-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Page1Component } from './page1.component';

const routes: Routes = [
    // 一般ユーザー向け
    { path: '', component: Page1Component },
    // 管理者向け
    { path: 'admin', component: Page1Component, data: { isAdmin: true } },
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class Page1RoutingModule { }
```
⇒管理者向けの方はdataにisAdminを設定します


- `/page1`アクセス時にPage1Moduleを遅延ロードするようにします

```src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: 'page1', loadChildren: () => import('./page1/page1.module').then(m => m.Page1Module) },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```



# 動作確認

- [http://localhost:4200/page1](http://localhost:4200/page1)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/9134c1bf-2dae-c336-0704-38692b3e0065.png)

- [http://localhost:4200/page1/admin](http://localhost:4200/page1/admin)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325108/e0106b08-7d3d-8206-f248-6af5b611ddfd.png)



# 最後に

こんな感じで簡単？にコンポーネントを共通化しつつ、サービスだけ差し替えることができます。
こうすることで、権限ごとにif文が増えたりとかしなくなるので、割とサービスがシンプルになります。

ファイル数は増えるし、Moduleが複雑になるというデメリットもあるので、使いどころの見極めは必要ですね。



# 参考

[ファクトリープロバイダー: useFactory](https://angular.jp/guide/dependency-injection-in-action#%E3%83%95%E3%82%A1%E3%82%AF%E3%83%88%E3%83%AA%E3%83%BC%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80%E3%83%BC-usefactory)