今回は構造ディレクティブについて
angularではディレクティブというと構造ディレクティブと属性ディレクティブの二つがあります
属性ディレクティブに関してはカスタムディレクティブ作ったりしやすいかなと個人的には思っていて割と理解しやすいかなと思います
構造ディレクティブはあまりカスタムで何か作ることってないだろうなと思っていたのもあってなんとなく*ngFor
だったり*ngIf
だったりを使っている感じでした
構造ディレクティブのカスタムディレクティブ何かサンプル作ってみたいなーと思っていた矢先にたまたま思いつきました
*ngFor
でhashを回したい...
ということで今回は構造ディレクティブでhashを回せるようになるまでどうしようって話です
動作環境
Angular CLI: 1.6.3 Node: 7.9.0 OS: linux x64 Angular: 5.1.2
とりあえずやってみる
hashを投げてみる
やってみたらできた!ってこともあるかもしれないので何はともあれやってみましょう
サンプルコード
app.component.ts
ngOnInit() { this.hash = { hoge: 'foo', fuga: 'bar', piyo: 'baz' }; }
- app.component.html
<div *ngFor="let item of hash"> {{item}} </div>
丁寧にメッセージ付きでエラーが返ってきます
ng:///AppModule/StructualDirectiveComponent.ngfactory.js:17 ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.
まぁそうですよねw
構造ディレクティブについて
ということでカスタムで作ってみるために構造ディレクティブについてドキュメントを見直します
構造ディレクティブに関しては公式に詳しく載っているので読めばいいって話にはなるのですが...
参考
Angular - Structural Directives
https://angular.io/guide/structural-directivesangular.io
* prefix
普段何気なく使っているアスタリスクでしたがちゃんと追ってみると面白いですね
公式からソース持ってきてます
<div *ngIf="hero" class="name">{{hero.name}}</div>
は下記と同じ意味を持ちます(糖衣構文)
<ng-template [ngIf]="hero"> <div class="name">{{hero.name}}</div> </ng-template>
下のコードは[ngIf]ディレクティブに
hero`をバインディングしているように読めますね
ngIfだとふーんといった感じですがngFor
だともう少し複雑です
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}} </div>
が下記の糖衣構文です
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd">({{i}}) {{hero.name}}</div> </ng-template>
forで回す際にループ内で使える変数をletで定義できます
用意されているのは添え字や最初の要素、最後の要素、偶数か、奇数か、差分検知の基準などです
どちらも後者の書き方だと何やってるか、ディレクティブ側がどういうコードになっているか想像しやすくなってますね
構造ディレクティブって書き方特殊だし特別なことをやっているのかなと思われそうですがそんなことはなく普通のディレクティブということがわかると思います
カスタム構造ディレクティブ
Angular - Structural Directives
https://angular.io/guide/structural-directives#write-a-structural-directiveangular.io
公式の例ではappUnless
ディレクティブを作っています
手順は
- @Directiveアノテーションでセレクタを定義
[appUnless]
- constructorでTemplateRef,ViewContainerRefをinject
- DOM操作
- 生成
createEmbeddedView
- 削除
clear()
- 生成
といった流れですね
ただ対象がイテレータだとまた他にも要素がありそうです
ディレクティブ
https://angular.io/api/common/NgForOfangular.io
こちらも見て見ます
DOMの再構築はコストがかかるのでイテレータに変化があったら差分を検知して差分だけDOMを追加、削除、位置が変わった場合は移動させています
また、ngForOfはtrackBy
で変更検知の方法を指定することもできます
なるほど
差分の検出に関しては別途まとめたいと思います
$implicit
$implicit
にはイテレータで回している個々の要素が渡ってきます
今回だとここにハッシュのキーを当てはめればOKそうです
カスタムしてみる
それではカスタム構造ディレクティブを作ってみます
ngFor
のソースをのぞいて、大体の流れはわかりました
もう少しシンプルな実装どこかにないかなと思い探した結果下記が見つかりました
参考
Creating structural directives in Angular – Adrian Fâciu – Medium
上記ではngFor
と同様にループを回す処理のみを実装されています
これを参考にすれば最低限やりたいことはできそうです
実装
結果、一番シンプルな形だと下記のような実装になりました、ほとんどパクリですねw
- hash-keys.directive.ts
import { Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appHashKeys]' }) export class HashKeysDirective implements OnChanges { // let item of itemsで ${selector}Ofというinputが渡ってくる // これがハッシュ @Input() appHashKeysOf: any; constructor( // ディレクティブを定義したコンテナ private viewContainerRef: ViewContainerRef, // appHashKeysを適用した中身のテンプレート private templateRef: TemplateRef<any> ) {} ngOnChanges(changes: SimpleChanges) { // それまで生成されていたDOMを削除 this.viewContainerRef.clear(); // キーを取得 const keys = Object.keys(this.appHashKeysOf); keys.forEach(key => { // キーごとにテンプレートを生成 this.viewContainerRef.createEmbeddedView(this.templateRef, { // let item of itemsのitemの箇所に適用される変数が$implicit // ngForOfのようにindex,first,last,even,oddなど独自のテンプレート変数もここで定義できる $implicit: key }); }); } }
- app.component.html
<div *appHashKeys="let key of hash"> <div>key: {{key}} , value: {{hash[key]}}</div> </div>
- app.component.ts
ngOnInit() { this.hash = { hoge: 'foo', fuga: 'bar', piyo: 'baz' }; }
無事表示までさせることができました
- Inputの変更が伝わるたびにDOMを全削除、全生成しているのでパフォーマンス的にはよろしくない
- Hash以外の値が渡ってきた時のハンドリングが未実装
- keyだけじゃなくvalueもテンプレート変数で扱いたい
- keyのソート順を指定したい
などなど実際に使うとなるとまだ実装しなければならない事が結構ありそうですがカスタム構造ディレクティブを作ってみるという点はクリアできました
まとめ
本読めばわかるにはわかるのですがもう少し深追いしてみると気づきが多くて楽しいですね
実際の使いどころとかイメージが湧きました
所感として、一つのコンポーネントの中でng-template
をいくつか定義して呼び出したりしている場合はその構造をディレクティブにすることができ、その結果コードが分割されてスッキリさせることが出来そうだと思いました
切り出したとしても使い回せるかどうかは微妙な気もしますが
だた今のプロジェクトで実際にあるので書き換えてみたい。。。w
ちなみに構造ディレクティブは他の記事などサンプルである例などと合わせると
- 指定秒数遅らせて表示(delay)
- 再帰的なテンプレート描画
- Unless時に表示
などは構造ディレクティブで実装出来そうですね
勉強になりました