AngularJS Directive なんてこわくない(その2)


compile と link

前回に引き続き、今回もカスタム directive について。

2回目の今回は、compilelinkの使い分けについて。

compile のサンプルコード

まずはcompileを利用するサンプルコードを、『Mastering Web Application Development with AngularJS』から抜粋して見ていく。

このコードでは、Bootstrap の理由を前提として、以下に示すように Bootstrap でのボタンデザインに必要な CSS class の値を追加する処理を、compileで実装している。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
myModule.directive('button', function() {
  return {
    restrict: 'E',
    compile: function(element, attrs) {
      element.addClass('btn');
      if (attrs.type === 'submit') {
        element.addClass('btn-primary');
      }
      if (attrs.size) {
        element.addClass('btn-' + attrs.size);
      }
    }
  };
});
  • <button>の class 属性にbtnを追加
  • <button type="submit">のように type="submit"がある場合には、class 属性にbtn-primaryを追加
  • <button size="...">のように size 属性がある場合には、btn-largeなど、btn- と size 属性で指定した文字列を組み合わせた文字列を class 属性に追加

コードからわかるように、compile オプションの function 第1引数にあるelementは、jQuery(jqLite)オブジェクトになっているので、直接 jQuery の API を利用できる。つまり$(element)や、$(button)などとしてやる必要はない。

第2引数のattrsではattrs.sizeのようにドット区切りで属性値にアクセスできるので、HTML テンプレート側の値を簡単に参照することができる。

compile の流れ

AngularJS が HTML テンプレートをコンパイルするために、標準の directives とカスタム定義した directives のすべてを、DOM の要素・属性・コメント・CSS class から探し出す。各 directive のcompilefunction を呼び出し、compilefunction から返されるlinkfunction を後で呼び出すために集めていく流れとなる。

なお、directives をコンパイルしていく処理の順序はpriorityの大きい順となるが、これについてはまた別の機会に先送り。

重要なポイントとしては、compileの処理は scope ができる前の処理であり、compilefunction では scope を利用できない。

すべてのコンパイル処理が終わった後、生成した scope を付けてlinkfunction を呼び出す流れとなり、この時点でlinkfunction の scope を利用した DOM との間での双方向バインドが効くことになる。

compile と link の使い分け

ng-repeatの内側にある directive というケースなど、同じ directive が繰り返し使われることになるような場合では、compileの function はng-repeatの繰り返しに関係なく一度だけ呼び出されるのに対し、linkの function はイテレーションのたびに呼び出されることになる。

これは、scope のデータや双方向バインドに依存ぜずに処理できる上記のサンプルコードのようなケースであれば、compileを使うことで同じ処理を繰り返す無駄を省くように最適化できることになる。

compilelinkの両方を指定した場合にはlinkが無視される仕様になっているため、両方を利用した実装をしたい場合にはcompilefunction からlinkfunction を return するよう実装することになる。

link のサンプルコード

ここから、最もよく利用することになるlinkの書き方について見ていく。

1
2
3
4
5
6
7
8
9
10
11
12
myModule.directive('myDirective', function () {
  return {
    link: function (scope, element) {
      scope.$watch('xxxVar', function () {
        // ...
      });
      scope.$on('xxxEvent', function () {
        // ...
      });
    }
  };
});

このように、linkでは、scopeにあるモデルの変更を検知して処理することや、イベントに応じた処理を記述して、DOM や controller とのやり取りを記述していく。

いろいろある link の書き方

Directives ではlinkの書き方にバリエーションがあるので、ざっと眺めておく。

1
2
3
4
5
myModule.directive('returnLinkFunction', function () {
  return function(scope, element, attrs) {
    // ...
  };
});

まず、単にfunctionを return するだけという書き方ができて、これはlinkプロパティだけを持つオブジェクトを返しているのと同じことになる。link以外のプロパティを指定する必要が無ければ、こう書くことでシンプルなコードにできる。

1
2
3
4
5
6
7
myModule.directive('usingLinkOption', function () {
  return {
    link: function(scope, element, attrs) {
      // ...
    }
  };
});

これはオブジェクト(DDO: Directive Definition Object)を返す書き方で、compileプロパティを使わない場合の書き方。

1
2
3
4
5
6
7
8
9
10
myModule.directive('usingCompileOption', function () {
  return {
    compile: function(element, attrs) {
      // ...
      return function(scope, element, attrs) {
        // ...
      };
    }
  };
});

最後に、compileプロパティを使う場合の書き方で、compilefunction で return しているfunctionlinkfunction として扱われることになる。

3回目へ向けて

この2回目で compile と link の使い分けができるようになったので、ここまでの知識でとりあえず directive を使っていろいろ実装していけるんじゃないかと思う。

けれども、replace transclude scope controller requireといった大事なプロパティを抑えてないので、ここで終わったら directive こわいままになっちゃうので、3回目に続く。