AngularJSのdirective間の連携

僕なりによく使うなー、って方法を書いてみる。

親ディレクティブと子ディレクティブ間で通信をしたい場合

tabset要素とtab要素は親と子の関係になると言えると思う。

そしてtabset要素は今選択している要素が何なのかを管理して、tab要素に適切に伝えられる必要がある。

そんな時には、子ディレクティブ(tab)から親ディレクティブ(tabset)のAPIを利用するようにできれば良い。

そんな時に使うと良いのが、directiveのrequireだ。

requireは'^ディレクティブ名'と指定すると、親のコントローラを参照できるようにしてくれる。

^を付けない場合は同じ要素内の属性を探しにいく。

この記事がその辺の挙動がすごく分かりやすい。

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

あまり良い例じゃないかも知れないけど、「選択可能なリスト」でこれを実装してみる。

公式のディレクティブの解説を参考にしました。

AngularJS

(bootstrapを使っています。)

  • main.html
<div class="row">
  <selectable>
    <div class="list-group col-lg-6">
      <selectable-list-item ng-repeat='item in items' item='item'></selectable-list-item>
    </div>
  </selectable>
</div>
  • コントローラ
angular.module('childDirectiveApp')
.controller('MainCtrl', function ($scope) {
  'use strict';
  $scope.items = [
    { title: 'test1', content: 'content1' },
    { title: 'test2', content: 'content2' },
    { title: 'test3', content: 'content3' },
  ];
});
  • ディレクティブ
angular.module('childDirectiveApp')
.directive('selectable', [ function() {
  'use strict';
  return {
    restrict: 'E',
    scope: {},
    controller: [function() {
      var listItems = [];
      this.add = function(listItem) {
        if (listItems.length === 0) { this.select(listItem); }
        listItems.push(listItem);
      };
      this.select = function(listItem) {
        angular.forEach(listItems, function(item) {
          item.selected = false;
        });
        listItem.selected = true;
      };
    }],
  };
}])
.directive('selectableListItem', [ function() {
  'use strict';
  return {
    restrict: 'E',
    template: '<a href="" class="list-group-item" ng-click="select()" ng-class="{active: selected}">{{item.title}}</a>',
    replace: true,
    scope: {item: '='},
    require: '^selectable',
    link: function(scope, element, attrs, cont) {
      cont.add(scope);
      scope.select = function() { cont.select(scope); };
    }
  };
}])
;
  • 表示

f:id:joe-re:20140817001117p:plain

selectabledirectiveが親でselectableListItemが子の関係になっている。

子ディレクティブ同士で通信をしたい場合

例えば、こういうふうにしたい時。

f:id:joe-re:20140817002725p:plain

これならさっきの親子関係を維持したまま、子を一人増やすだけでいける。

  • main.html
<div class="row">
  <selectable>
    <div class="list-group col-lg-6">
      <selectable-list-item ng-repeat='item in items' item='item'></selectable-list-item>
    </div>
    <selected-content class="col-lg-6"></selected-content>
  </selectable>
</div>
  • selected-content.html(テンプレート)
<form class="well">
  <div class="form-group">
    <label for="title">title:</label>
    <input id="title" type="text" class="form-control" placeholder="title" ng-model="item.title">
  </div>
  <div class="form-group">
    <label for="content">content:</label>
    <textarea id="content" class="form-control" placeholder="content" ng-model="item.content"></textarea>
  </div>
  <button class="btn btn-info" ng-click="save()">Save</button>
</form>
  • ディレクティブ selected-contentディレクティブ(子)を作る。
.directive('selectedContent', [ function() {
  'use strict';
  return {
    restrict: 'E',
    templateUrl: 'views/selected-content.html',
    replace: true,
    scope: {},
    require: '^selectable',
    link: function(scope, element, attrs, cont) {
      scope.item = angular.copy(cont.selected().item); //初期選択を反映させるため
      scope.save = function() {
        cont.save(scope.item);
      };
      scope.$on('selectedItemChanged', function(ev,item) {
        scope.item = item;
      });
    }
  };
}])

selectableディレクティブ(親)に機能の追加をする。

.directive('selectable', [ function() {
  'use strict';
  return {
    restrict: 'E',
    scope: {},
    controller: ['$rootScope', function($rootScope) {
      var listItems = [];
      this.add = function(listItem) {
        if (listItems.length === 0) { this.select(listItem); }
        listItems.push(listItem);
      };
      this.select = function(listItem) {
        angular.forEach(listItems, function(item) {
          item.selected = false;
        });
        listItem.selected = true;
        $rootScope.$broadcast('selectedItemChanged', angular.copy(listItem.item));
      };
      this.selected = function() {
        var selectedItem = null;
        angular.forEach(listItems, function(listItem) {
          if (listItem.selected) { selectedItem = listItem; }
        });
        return selectedItem;
      };
      this.save = function(item) {
        this.selected().item = item;
      };
    }],
  };
}])

選択アイテムの変更時は「selectable-list-item(子)→selectable(親)→selected-content(子)」の順で通知するようにしている。

親から子へ通知する時は、scope.$broadcastを利用してselectedItemChangedイベントを発生させるようにした。

scope.$broadcastはDOMツリーの下位方向へイベントの発生を通知することができる。(逆は$emit。)

多分親から子へ通知する方法はまだあるのだろうけど、僕はこの方法をよく使う。

今回はsaveボタンを押した時に保存されるようにしたので、選択したアイテムはangular.copy()を使ってディープコピーしたものを渡している。

selectableディレクティブのselectedは公開APIにする必要があまりないのだけど、selected-contentディレクティブの初期選択が反映されなかったので、そこで使うために仕方なく公開にした。

1番最初にアイテムを追加した時にselectedItemChangedイベントは発行されるのだけど、この時はまだselected-contentディレクティブは動作していないということだろうか。

(解決策ご存知の方は教えてください。。)

書籍

AngularJSアプリケーション開発ガイド

AngularJSアプリケーション開発ガイド

Angularの基本的な部分が、シンプルかつ過不足なくまとまった一冊。隣にあるだけで心強い。