notebook

都内でWEB系エンジニアやってます。

[angular] ディレクティブについて - カスタム構造ディレクティブを作ってみる

今回は構造ディレクティブについて

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()

といった流れですね

ただ対象がイテレータだとまた他にも要素がありそうです

ディレクティブ

Angular - NgForOf

https://angular.io/api/common/NgForOfangular.io

こちらも見て見ます

DOMの再構築はコストがかかるのでイテレータに変化があったら差分を検知して差分だけDOMを追加、削除、位置が変わった場合は移動させています

また、ngForOfはtrackByで変更検知の方法を指定することもできます

なるほど

差分の検出に関しては別途まとめたいと思います

$implicit

$implicitにはイテレータで回している個々の要素が渡ってきます

今回だとここにハッシュのキーを当てはめればOKそうです

カスタムしてみる

それではカスタム構造ディレクティブを作ってみます

ngForのソースをのぞいて、大体の流れはわかりました

もう少しシンプルな実装どこかにないかなと思い探した結果下記が見つかりました

参考

Creating structural directives in Angular – Adrian Fâciu – Medium

medium.com

上記では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'
    };
  }

無事表示までさせることができました

f:id:swfz:20180330030105p:plain

  • Inputの変更が伝わるたびにDOMを全削除、全生成しているのでパフォーマンス的にはよろしくない
  • Hash以外の値が渡ってきた時のハンドリングが未実装
  • keyだけじゃなくvalueもテンプレート変数で扱いたい
  • keyのソート順を指定したい

などなど実際に使うとなるとまだ実装しなければならない事が結構ありそうですがカスタム構造ディレクティブを作ってみるという点はクリアできました

まとめ

本読めばわかるにはわかるのですがもう少し深追いしてみると気づきが多くて楽しいですね

実際の使いどころとかイメージが湧きました

所感として、一つのコンポーネントの中でng-templateをいくつか定義して呼び出したりしている場合はその構造をディレクティブにすることができ、その結果コードが分割されてスッキリさせることが出来そうだと思いました

切り出したとしても使い回せるかどうかは微妙な気もしますが

だた今のプロジェクトで実際にあるので書き換えてみたい。。。w

ちなみに構造ディレクティブは他の記事などサンプルである例などと合わせると

  • 指定秒数遅らせて表示(delay)
  • 再帰的なテンプレート描画
  • Unless時に表示

などは構造ディレクティブで実装出来そうですね

勉強になりました