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


controller, require

その1その2その3に引き続き、今回もカスタム directive について。

今回のサンプルコードは、UI Bootstrap の Tabs からの一部抜粋で、controller requireオプションについて見ていく。

tabs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
angular.module('ui.bootstrap.tabs', [])
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
  var ctrl = this,
      tabs = ctrl.tabs = $scope.tabs = [];

  ctrl.select = function(tab) {
    angular.forEach(tabs, function(tab) {
      tab.active = false;
    });
    tab.active = true;
  };
  // ...
}])

.directive('tabset', function() {
  return {
    restrict: 'EA',
    transclude: true,
    replace: true,
    require: '^tabset',
    scope: {},
    controller: 'TabsetController',
    templateUrl: 'template/tabs/tabset.html',
    compile: function(elm, attrs, transclude) {
      return function(scope, element, attrs, tabsetCtrl) {
        // ...
      };
    }
  };
})

.directive('tab', ['$parse', function($parse) {
  return {
    require: '^tabset',
    restrict: 'EA',
    replace: true,
    templateUrl: 'template/tabs/tab.html',
    transclude: true,
    scope: {
      heading: '@',
      onSelect: '&select',
      onDeselect: '&deselect'
    },
    controller: function() {},
    compile: function(elm, attrs, transclude) {
      return function postLink(scope, elm, attrs, tabsetCtrl) {
        // ...
        scope.$watch('active', function(active) {
          setActive(scope.$parent, active);
          if (active) {
            tabsetCtrl.select(scope);
            scope.onSelect();
          } else {
            scope.onDeselect();
          }
        });
        // ...
        scope.select = function() {
          if (!scope.disabled ) {
            scope.active = true;
          }
        };
        tabsetCtrl.addTab(scope);
        scope.$on('$destroy', function() {
          tabsetCtrl.removeTab(scope);
        });
        // ...
      };
    }
  };
}])

controller

directive にもng-controllerで利用するときに定義するのと同じようなcontrollerを記述でき、$scope$httpなどをインジェクト(DI)することもできる。directive の場合でも、controllerでは DOM 操作するコードは記述しないようにし、compileまたは linkのほうに記述する。

なお、directive のcontrollerには、モジュールで定義したcontrollerの名前を記述することができる。

1
2
3
4
5
6
7
.directive('tabset', function() {
  return {
    ...,
    controller: 'TabsetController',
    ...
  };
})

注意すべき点としては、再利用されるコンポーネントとして directive を作成する場合、controllerに付ける名前が重複されにくい名前にしておくこと。

controllerに名前を付けずに、直接 function を記述することもできる。

1
2
3
4
5
6
7
8
9
.directive('tabset', function() {
  return {
    ...,
    controller: ['$scope', function($scope) {
      // ...
    }],
    ...
  };
})

$scopeだけでなく$http$timeoutなどをインジェクト(DI)できる。また、directive のcontrollerでは$element $attrs $transcludeの service をインジェクトできるようになっている。

linkでもcontrollerでも、どちらでも同じような処理を記述することができそうに思う。違う点は、DI を利用できるか否かと、処理のタイミング(controllerが先で、linkが後)。使い分けのヒントとしては、子要素など別の directive から呼び出すのであればcontrollerとして API を公開する感じで実装し、そうでなければlinkで実装するという感じで。

require

ネストされた directive から親の directive のcontrollerで定義された API を呼び出すにはrequireが必要となる。

上のコード例では、require: '^tabset'の記述があり、これによってtabset directive のcontrollerであるtabsetCtrlを参照して API を利用できるようになる。

1
2
3
4
5
6
7
compile: function(elm, attrs, transclude) {
  return function postLink(scope, elm, attrs, tabsetCtrl) {
    tabsetCtrl.select(scope);
    tabsetCtrl.addTab(scope);
    tabsetCtrl.removeTab(scope);
  };
}

なお、^を付けない場合、親階層ではなく directive を指定した要素のcontrollerを探すこととなる。このケースでよく使うのはrequire: 'ngModel'で、directive と同じ要素にng-model="..."の記述があることを前提として実装できることになる。

同じ要素にng-model属性の記述が無い場合、こんなエラーになる。

1
Error: [$compile:ctreq] Controller 'ngModel', required by directive 'input', can't be found!

このエラーを発生させる必要が無いなら、require: '?ngModel'のように?を付けて記述する。

これでもうカスタム directive を書ける

この4回目でcontrollerrequireを使って、複数の directive でコンポーネントを構成することについての理解も進んだので、どんどん directive を活用していこう!