6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AngularAdvent Calendar 2023

Day 3

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

Posted at

こんにちは! 🌟

Angular Advent Calendar2日目のAngularにおける組み込み制御フローの導入とその背景からバトン🪄を貰った

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

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

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

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

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

// 抽象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),
  ],
});

ポイントは3つ!!!!

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

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

1. @TokenizedType()デコレーター 💅💖

@TokenizedTypeは超単純✨デコレートしたclassにプロパティとしてInjectionTokenを付与しちゃうよ💪🎉!!

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

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


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

2. ProvideComponent()関数😎🌈

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

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()関数を用いて第1引数の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

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

おわりに

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

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

明日は

明日はアドカレ4日目!!

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

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

Thanks!♥

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?