クリーンな美少女エンジニアはComponentも注入💉したい！！

こんにちは！ 🌟

[Angular Advent Calendar](https://qiita.com/advent-calendar/2023/angular)２日目の[Angularにおける組み込み制御フローの導入とその背景](https://zenn.dev/carimatics/articles/angular_2023_builtin_control_flow)からバトン🪄を貰った

超絶美少女エンジニア のんたん(@nontangent)だよ〜✨✨
  

長く生きてると、コンポーネントをDIしたい場面って、けっこう増えてくるよね〜？ 😄

そしたらプレゼンテーション層ももっとクリーンなアーキテクチャになる気がするー🧹🧹🧹

そこで、アドカレ3日目はコンポーネントをInjectableにしちゃう方法を教えちゃうよ！ 🚀

いろいろ、試行錯誤した結果、最終的にはこんな感じ！！ 😣


```ts
// 抽象Component(=入出力のみ定義されたComponent=Directive≒ComponentStore)
@TokenizedType() // #1
@Directive({
  selector: 'app-example',
  standalone: true,
})
export class ExampleComponentStore extends InjectableComponent { // #3
  @Input() name = '';
}

// Componentの実装(=抽象Compoenntにtemplateを付与したもの)
@Component({
  template: `ExampleComponentImpl is injected by {{ store.name }}!`,
  hostDirectives: [{ directive: ExampleComponentStore, inputs: ['name'] }],
})
export class ExampleComponentImpl {
  protected store = inject(ExampleComponentStore);
}

// Componentの実装の別バージョン
@Component({
  template: `ExampleComponentImplV2 is injected by {{ store.name }}!`,
  hostDirectives: [{ directive: ExampleComponentStore, inputs: ['name'] }],
})
export class ExampleComponentImplV2 {
  protected store = inject(ExampleComponentStore);
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ExampleComponentStore],
  template: `
    <app-example injectable
      [name]="name"
    ></app-example>
  `,
})
export class AppComponent {
  name = 'App';
}

bootstrapApplication(AppComponent, {
  providers: [
    provideComponent(ExampleComponentStore, () => ExampleComponentImpl),　 // #2
    // MEMO(@nontangent): コメントインするとV2コンポーネントがDIされる。
    // provideComponent(ExampleComponentStore, () => ExampleComponentImplV2),
  ],
});

```

ポイントは３つ！！！！
1. `@TokenizedType()`デコレーター💄
    -　ComponentTypeにInjectionTokenをつけとく魔法のデコレーター💫✨
1. `ProvideComponent()`関数
    - ComponentTypeからInjectionTokenを取り出していい感じにProviderを設定しちゃう関数🔮✨
1. `InjectableComponent`クラス
    - InjectionTokenから取得したComponentTypeを用いてComponentを作成する抽象クラスよ💁‍♀️💖

それじゃあ中身を実装👩‍💻していくよ❗🙌


## 1. `@TokenizedType()`デコレーター 💅💖
`@TokenizedType`は超単純✨デコレートしたclassにプロパティとしてInjectionTokenを付与しちゃうよ💪🎉！！

```ts
export function TokenizedType() {
  return function (target: any) {
    target['Δtype'] = new InjectionToken(target);
  };
}

```


ついでに、`getToken()`関数も定義して、付与したclassからInjectionTokenを簡単に取得できるようにしておこう！👍💕

```ts

export function getToken(constructor: any) {
  return constructor['Δtype'];
}
```


## 2. `ProvideComponent()`関数😎🌈
開発者はInjectionTokenに興味がないから、`ProvideComponent(抽象のComponentType, () => 実装のComponentType)`で直感的にプロバイダーを設定できるようにするよ🔥

```ts
export function provideComponent<ABS = any, IMPL = any>(
  abstract: Type<ABS>,
  typeOrFactory: Type<IMPL> | TypeFactory<IMPL>
) {
  async function loadComponentType(): Promise<Type<IMPL>> {
    if (typeof typeOrFactory === 'function' && !typeOrFactory.prototype) {
      return await (typeOrFactory as TypeFactory<IMPL>)();
    } else {
      return typeOrFactory as Type<IMPL>;
    }
  }
  return { provide: getToken(abstract), useValue: loadComponentType };
}
```


`import()`対応のために若干複雑になってるけど、やってることは`getToken()`関数を用いて第１引数のCopmonentTypeに付与されたInjectionTokenを取得して、第2引数のloaderが取得できるようにしているだけ〜！😘🌟


### 3. `InjectableComponent`クラス🌟🎀
最後は抽象ComponentTypeが継承するInjectableComponentを実装しよう！

```.ts
@Directive({ standalone: true })
export abstract class InjectableComponent<T = any> {
  readonly #outlet = inject(ViewContainerRef);
  readonly #injector = inject(Injector);
  readonly #destroy$ = inject(DestroyRef);
  readonly #el = inject(ElementRef);
  #component: ComponentRef<T> | null = null;
  #componentMirror: ComponentMirror<T> | null = null;

  @Input({ transform: (value: any) => (value === '' ? true : value) })
  private injectable = false;

  ngOnInit() {
    if (this.injectable) {
      this.#injector
        .get<TypeFactoryAsync<T>>(getToken(this.constructor))()
        .then((type) => {
          this.#component = this.#outlet.createComponent(type);
          this.#componentMirror = reflectComponentType(type);
          this.#bindInputs();
          this.#bindOutputs();
          this.#setAttribute();
        });
    }
  }
  ...
}
```

`ngOnInit()`の内部では、①継承先のクラスに付与されたInjectionTokenを取得 -> ②注入された実装のComponentTypeを取得 -> ③実装のComponentTypeからComponentを作成 -> ④DirectiveについていたInputやOutput、Attriuteを接続って流れの処理をしているよ❗

継承(HostDirectivesも含む)先でもこの処理が無限ループしないように`injectable`属性がついてるときだけ実行してる (´꒳｀*)💖

これで抽象Componentに実装が注入されてtemplateが表示されるはず♫


## 百聞は一見にしかず！
実際の動きを触ってみよう 👉 [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-ejs8gi?file=src%2Finjectable-component.ts)


より詳細なソースコードをチェックできるから、ぜひチェックしてみてね！💻🔍

## おわりに
アトミックデザインみたいな多層化したデザインシステムでも、これがあれば階層の深〜いコンポーネントをピンポイントにDIして、可搬性と拡張性がアガる⤴はず！🚀

私はこれを用いた抽象デザインシステムの構築を目論んでるよ〜👀

## 明日は
明日はアドカレ４日目！！

@rysiva がなんか書くってさ！！楽しみ〜〜

=> https://qiita.com/advent-calendar/2023/angular


## Thanks！♥
Componentの注入、全然やり方が思いつかなくて、１年前くらいにlacoさんに相談したら一瞬で解決してくれてシビレタ⚡⚡⚡GDEスゴイ！！この場を借りて感謝と共有！！🙏🌟