こんにちは! 🌟
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つ!!!!
-
@TokenizedType()
デコレーター💄
- ComponentTypeにInjectionTokenをつけとく魔法のデコレーター💫✨ -
ProvideComponent()
関数- ComponentTypeからInjectionTokenを取り出していい感じにProviderを設定しちゃう関数🔮✨
-
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を実装しよう!
@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スゴイ!!この場を借りて感謝と共有!!🙏🌟