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

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

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アプリケーション開発ガイド

  • 作者: Brad Green,Shyam Seshadri,牧野聡
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2014/04/18
  • メディア: 大型本
  • この商品を含むブログ (2件) を見る

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