ディレクティブ

概要

注意: このガイドは基本的なAngularJSの概要を理解した開発者を対象にしたものです。 もし、あなたが初学者であれば、先にチュートリアルでの学習をお勧めします。 もし、ディレクティブのAPIを探しているのであれば、それは$compileに移されています。

このドキュメントはAngularJSアプリケーションで自身のオリジナルのディレクティブを作成したい場合に、それを実装する方法について説明しています。

ディレクティブとは?

高階層において、ディレクティブはDOM要素(属性、要素名、CSSクラス)上でのマーカーであり、 AngularJSのHTMLコンパイラ($compile)に、そのDOM要素やその子要素に変形や特殊な振る舞いを割り当てるように伝えます。

Angularは、ngBind、ngMolde、ngViewのような組み込みのディレクティブのセットを持ちます。 コントローラーとサービスを作るように、Angularで使用する自身のディレクティブを作成することが可能です。 Angularがアプリケーションを起動する際、HTMLコンパイラはDOM要素内を辿ってディレクティブDOMが無いか探索を行います。

HTMLテンプレートを"compile(コンパイル)"するとは、どういう意味なのでしょうか? AngularJSでは、"コンパイル"とはHTMLにイベントリスナーを割り当て、インタラクティブにすることを意味します。 "コンパイル"という用語を使用している理由は、ディレクティブに割り当てる再帰的なプロセスは、 プログラミング言語のソースコードのコンパイルプロセスを忠実に映し出しているように見えるからです。

ディレクティブを書く前に、AngularのHTMLコンパイラが、 いつ与えられたディレクティブを使用するかを決定するのかを知る必要があります。

下記の例の場合、「<input>要素は、ngModelディレクティブにマッチ(match)している。」と言います。

<input ng-model="foo">

また下記の例も、ngModelにマッチしています。

<input data-ng:model="foo">

Angularはディレクティブにマッチした要素のタグと属性名を正規化します。 通常ディレクティブは、正規化された大文字小文字を区別するキャメルケースの名前(例: ngModel)によって参照されます。 ただし、HTMLでは大文字小文字を区別しないので、通常はダッシュ区切りされたDOM要素上の属性を使用して、 小文字の形式によってDOM内のディレクティブを参照します。(例: ng-model)

下記のようにして正規化のプロセスが行われます。

  1. 要素/属性の前方から、x-data-を取り除きます。
  2. :-_区切りされた名称をキャメルケースに変換します。

下記は、ngBindにマッチする要素の例になります。

<!doctype html>
<html ng-app="docsBindExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl1">
      Hello <input ng-model='name'> <hr/>
      <span ng-bind="name"></span> <br/>
      <span ng:bind="name"></span> <br/>
      <span ng_bind="name"></span> <br/>
      <span data-ng-bind="name"></span> <br/>
      <span x-ng-bind="name"></span> <br/>
    </div>
  </body>
</html>
angular.module('docsBindExample', [])
  .controller('Ctrl1', function Ctrl1($scope) {
    $scope.name = 'Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)';
  });
it('should show off bindings', function() {
  expect(element('div[ng-controller="Ctrl1"] span[ng-bind]').text())
    .toBe('Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)');
});
<!doctype html>
<html ng-app="docsBindExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
<script>angular.module('docsBindExample', [])
  .controller('Ctrl1', function Ctrl1($scope) {
    $scope.name = 'Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)';
  });
</script>
  </head>
  <body>
    <div ng-controller="Ctrl1">
      Hello <input ng-model='name'> <hr/>
      <span ng-bind="name"></span> <br/>
      <span ng:bind="name"></span> <br/>
      <span ng_bind="name"></span> <br/>
      <span data-ng-bind="name"></span> <br/>
      <span x-ng-bind="name"></span> <br/>
    </div>
  </body>
</html>

ベストプラクティス: ダッシュ区切りのフォーマットを使用する方が好ましいです。(例: ng-bind) もし、HTML検証ツールを使用したい場合は、代わりにdata接頭辞バージョンを使用することが出来ます。(例: data-ng-bind) 上記の他の形式は古いバージョンのため残されていますが、それらの使用は避ける事をお勧めします。

$compileは、要素名、属性、クラス名、コメントですら、それらを基にしたディレクティブにマッチすることが出来ます。

Angularから提供される全てのディレクティブは、属性名、タグ名、コメント、クラス名にマッチします。 下記は、様々な方法でディレクティブ(このケースではmyDir)をテンプレート無いから参照させるデモになります。

<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>

ベストプラクティス:コメントとクラス名より、タグ名と属性を介してディレクティブを使用するのが好ましいです。 そうすることで通常、ディレクティブが与えられた要素が何なのかを判別し易くなります。

ベストプラクティス:コメントディレクティブは通常、ディレクティブが複数の要素(例: <table>要素内)にまたがるような、 DOM APIの機能を制限される場所で使用されます。 AngularJS 1.2では、ng-repeat-startとng-repeat-end がこの問題のより良い解決方法として導入されました。 開発者は可能であれば、カスタム·コメントディレクティブを介して、これを使用することをお勧めします。

テキストと属性の紐付け

コンパイラ(compiler)はテキストと属性にマッチするコンパイル処理中に、 埋込み式が含まれていないかを確認するのに、$interpolateサービスを使用しマッチします。 これらの式は監視対象として登録され、通常のdigestサイクルの一部として更新されます。 下記は、補完(埋込み式)の例になります。

<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>

ngAttr属性による紐付け

Webブラウザは時折、属性の値が有効なものであるかを気にする事があります。 例えば、下記のテンプレートのようなケースです。

<svg>
  <circle cx="{{cx}}"></circle>
</svg>

Angularがこれを紐付けしてくれる事を期待してしまいますが、 コンソールで次のようなエラーが表示されるはずです。
Invalid value for attribute cx="{{cx}}"
これは、SVGのDOMのAPI制限に引っ掛かってしまうためで、単純にcx="{{cx}}"と書くことは出来ません。

ng-attr-cxであれば、この問題に対処することが出来ます。

もし、バインディングされている属性にngAttr(非正規化でng-attr-とされる)が接頭辞として付けられていた場合、 バインディング中に接頭辞無しとした属性に一致するものが適用されます。 これは他の通常の属性とは異なりブラウザから頻繁に処理される属性への紐付けを可能にしてくれます。 (例: SVG要素のcircle[cx]属性)

例えば、先の例にこの書き方を適用すると、下記のようになります。

<svg>
  <circle ng-attr-cx="{{cx}}"></circle>
</svg>

ディレクティブの適用

それでは、ディレクティブ登録のためのAPIについて見て行きましょう。 多くのコントローラー、ディレクティブのようなものは、モジュールに登録されます。 ディレクティブを登録するには、module.directiveのAPIを使用します。 module.directiveは、ファクトリー関数に続いて、正規化されたディレクティブ名を取得します。 このファクトリー関数は、$compileにディレクティブがマッチした際に振る舞うべき事を伝えるための、 異なるオプションを持つオブジェクトを返すべきです。

ファクトリー関数は、コンパイラがディレクティブにマッチした最初の時にだけ、実行されます。 ここで初期化処理を実行することが出来ます。 この関数は、コントローラーと同様に注入を可能にしてくれる$injector.invokeを使用して実行されます。

ベストプラクティス: 関数を返すことより、定義オブジェクトを使用する方が好ましいです。

これからいくつかのディレクティブの一般的な例を元に、 異なるオプションとコンパイルのプロセスについて深く見て行きましょう。

ベストプラクティス: 今後の標準で実装されるかもしれないものとの衝突を避けるために、 接頭辞を付けて、自身のディレクティブ名にするのが良いでしょう。 例えば、もし<carousel>ディレクティブを作ろうとした場合、 もしかしたら、HTML7で同じ名前の要素が実装されるかもしれません。 2,3文字の接頭辞(例:btfCarousel)が適切だと思われます。 同様に、自身のディレクティブにngの接頭辞、 または将来のAngularのバージョンに含まれそうなワードを含まないでください。

この後の例やデモでは、接頭辞にmy(例: myCustomer)を使用するものとします。

テンプレート拡張ディレクティブ

顧客情報を表示する部分が含まれるテンプレートがあるものとします。 このテンプレートはコード内で何度も繰り返し使用され、 一つの箇所を変更した場合は他の箇所についても変更をしなければいけません。 こういったケースでは、ディレクティブを使用してテンプレートをシンプルにする良い機会だと考えてください。

静的なテンプレートのコンテンツを、作成したディレクティブに置き換えてシンプルにしてみましょう。

<!doctype html>
<html ng-app="docsSimpleDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <div my-customer></div>
    </div>
  </body>
</html>
angular.module('docsSimpleDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.customer = {
      name: 'Naomi',
      address: '1600 Amphitheatre'
    };
  })
  .directive('myCustomer', function() {
    return {
      template: 'Name: {{customer.name}} Address: {{customer.address}}'
    };
  });
<!doctype html>
<html ng-app="docsSimpleDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
<script>angular.module('docsSimpleDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.customer = {
      name: 'Naomi',
      address: '1600 Amphitheatre'
    };
  })
  .directive('myCustomer', function() {
    return {
      template: 'Name: {{customer.name}} Address: {{customer.address}}'
    };
  });
</script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <div my-customer></div>
    </div>
  </body>
</html>

このディレクティブに紐付けを行っていることに注意してください。 $compileでのコンパイルと<div my-customer></div>へのリンク後、 要素の子のディレクティブへのマッチが試みられます。 これは、他のディレクティブのディレクティブを構成することが可能なことを意味します。 下の例で、その方法を確認してみましょう。

上記の例では、テンプレートオプションの値を1行で書いていますが、 テンプレートの内容が増えると目障りになるかもしれません。

ベストプラクティス: テンプレートが非常に小さい場合を除き、通常はその部分をHTMLファイルとして分離し、 templateUrlオプションを使用して読み込むのが良いでしょう。

もし、ngIncludeを使うことに慣れ親しんでいるのであれば、templateUrlはそれと同じように動作することを確認できるはずです。 下記は、代わりにtemplateUrlを使用した同じ例になります。

<!doctype html>
<html ng-app="docsTemplateUrlDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <div my-customer></div>
    </div>
  </body>
</html>
Name: {{customer.name}} Address: {{customer.address}}
angular.module('docsTemplateUrlDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.customer = {
      name: 'Naomi',
      address: '1600 Amphitheatre'
    };
  })
  .directive('myCustomer', function() {
    return {
      templateUrl: 'my-customer.html'
    };
  });

グレート! ただし、もし代わりに<my-customer>のタグ名にマッチする自分達のディレクティブを持ちたいとした場合、何をすれば良いのでしょう? 単に<my-customer>要素をHTML内に配置しても、それは動作しません。

注意: ディレクティブを作成する場合、デフォルトの属性のみに制限されます。 要素名からトリガされるディレクティブを作成するには、restrictオプションを使用する必要があります。

制限オプションは、通常下記のように設定します。

'A'
属性名のみにマッチします。
'E'
要素名のみにマッチします。
'AE'
属性名、要素名のどちらかにマッチします。

それでは、restrict: 'E'を使用したオリジナルのディレクティブに変更してみましょう。

<!doctype html>
<html ng-app="docsRestrictDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-customer></my-customer>
    </div>
  </body>
</html>
Name: {{customer.name}} Address: {{customer.address}}
angular.module('docsRestrictDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.customer = {
      name: 'Naomi',
      address: '1600 Amphitheatre'
    };
  })
  .directive('myCustomer', function() {
    return {
      restrict: 'E',
      templateUrl: 'my-customer.html'
    };
  });

restrictプロパティの詳細については、APIドキュメントを参照してください。

属性と要素のどちらを使用すべきか? テンプレートを管理しているコンポーネントを作成する場合には、'要素'を使用します。 通常、ドメイン特化言語を、テンプレートの一部のために作成する際にこちらのケースが適用されます。 既存の要素に新しい機能を追加・装飾するような場合には、'属性'を使用します。

myCustomerディレクティブ用に'要素'を使用するのは、 customerコンポーネントとしてコアの振る舞いとして定義した"customer"の振る舞いを装飾しないので、 言うまでもなく正しい選択です。

ディレクティブのスコープの分離

上記のmyCustomerディレクティブは素晴らしいですが、致命的な欠点があります。 与えられたスコープしか、使用することが出来ないことです。

現在の実装では、異なるコントローラー毎にディレクティブを再使用するような形式で作成する必要があります。

<!doctype html>
<html ng-app="docsScopeProblemExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="NaomiCtrl">
      <my-customer></my-customer>
    </div>
    <hr>
    <div ng-controller="IgorCtrl">
      <my-customer></my-customer>
    </div>
  </body>
</html>
Name: {{customer.name}} Address: {{customer.address}}
angular.module('docsScopeProblemExample', [])
  .controller('NaomiCtrl', function($scope) {
    $scope.customer = {
      name: 'Naomi',
      address: '1600 Amphitheatre'
    };
  })
  .controller('IgorCtrl', function($scope) {
    $scope.customer = {
      name: 'Igor',
      address: '123 Somewhere'
    };
  })
  .directive('myCustomer', function() {
    return {
      restrict: 'E',
      templateUrl: 'my-customer.html'
    };
  });

これは、明らかに最適な解決方法とは言えません。

出来ることなら、ディレクティブ内部のスコープを外部のスコープから分離し、 外部スコープを内部スコープにマッピングしたいと考えます。 これは、隔離スコープ(isolate scope)と呼ばれるものを作成することで可能になります。 これをするには、ディレクティブのscopeオプションを使用します。

<!doctype html>
<html ng-app="docsIsolateScopeDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-customer info="naomi"></my-customer>
      <hr>
      <my-customer info="igor"></my-customer>
    </div>
  </body>
</html>
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
angular.module('docsIsolateScopeDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
    $scope.igor = { name: 'Igor', address: '123 Somewhere' };
  })
  .directive('myCustomer', function() {
    return {
      restrict: 'E',
      scope: {
        customerInfo: '=info'
      },
      templateUrl: 'my-customer-iso.html'
    };
  });

index.htmlを確認してください。 1つ目の<my-customer>要素は、info属性がnaomiのものに紐付けられ、コントローラーのスコープにそれが公開されます。 2つ目はigorの情報が紐付けられます。

それでは、scopeオプションを詳しく見てみましょう。

//...
scope: {
  customerInfo: '=info'
},
//...

scopeオプションは、各隔離スコープに紐付けるためのプロパティを含むオブジェクトです。 このケースでは、プロパティを1つだけ持ちます。

  • このプロパティ名(customerInfo)はディレクティブの隔離スコープのプロパティ、customerInfoに対応するものです。
  • このプロパティ値(=info)は、$compileにinfo属性に紐付くように指示します。

注意: これらのディレクティブのオプションスコープ内の=attr属性は、ディレクティブ名と同じように正規化されます。 <div bind-to-this="thing">内の属性を紐付けるには、=bindToThisを指定します。

属性の名前がプロパティ値と同じものとして、ディレクティブのスコープ内に紐付けたい場合、 下記の略記文法を使用することが出来ます。

...
scope: {
  // same as '=customer'
  customer: '='
},
...

更に隔離スコープのもう一つの機能によって、異なるデータをディレクティブ内のスコープを紐付ける事が可能です。

もう一つのプロパティであるvojtaがスコープに追加され、ディレクティブのテンプレートから、 それにアクセスが試みられている事が確認できると思います。

<!doctype html>
<html ng-app="docsIsolationExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-customer info="naomi"></my-customer>
    </div>
  </body>
</html>
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
<hr>
Name: {{vojta.name}} Address: {{vojta.address}}
angular.module('docsIsolationExample', [])
  .controller('Ctrl', function($scope) {
    $scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
    $scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
  })
  .directive('myCustomer', function() {
    return {
      restrict: 'E',
      scope: {
        customerInfo: '=info'
      },
      templateUrl: 'my-customer-plus-vojta.html'
    };
  });

{{vojta.name}}{{vojta.address}}は空で、これらは未定義であることを意味していることに注意してください。 コントローラー内でvojtaを定義したものの、ディレクティブ内で利用することは出来ません。

名前が示すように、ディレクティブの隔離スコープは、 {}ハッシュオブジェクトで明示的にスコープに追加したモデルを除く全てのものを隔離します。 これは、明示的に渡されたモデルの状態が変更からコンポーネントが守られるため、 再利用するコンポーネントを構築する際に便利です。

注意: 通常、スコープは親からプロトタイプ継承します。隔離スコープでは、それを行いません。

ベストプラクティス: アプリケーションを通して再利用したいコンポーネントを作成する際には、隔離スコープをscopeオプションを使用して作成してください。

DOMを操作するディレクティブの作成

この例では、現在の時刻を表示するディレクティブを作成します。 1秒に1回、DOMを更新して現在の時刻を反映します。

ディレクティブでDOMを更新したい場合、通常はlinkオプションを使用します。 linkは、下記の特徴を持つ関数を取得します。

function link(scope, element, attrs) { ... }
scope
Angularのスコープオブジェクトです。
element
このディレクティブにマッチしたjqLiteでラップされた要素です。
attrs
正規化された属性名とそれらに一致する値のオブジェクトです。

リンク関数で1秒に1度、またはユーザーがディレクティブに紐付く時刻フォーマット文字列を変更した際に表示を更新したいと思います。 また、もしディレクティブが削除されたら、メモリーリークを引き起こさないためにtimeoutを削除したいとも考えています。

<!doctype html>
<html ng-app="docsTimeDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl2">
      Date format: <input ng-model="format"> <hr/>
      Current time is: <span my-current-time="format"></span>
    </div>
  </body>
</html>
angular.module('docsTimeDirective', [])
  .controller('Ctrl2', function($scope) {
    $scope.format = 'M/d/yy h:mm:ss a';
  })
  .directive('myCurrentTime', function($timeout, dateFilter) {

    function link(scope, element, attrs) {
      var format,
          timeoutId;

      function updateTime() {
        element.text(dateFilter(new Date(), format));
      }

      scope.$watch(attrs.myCurrentTime, function(value) {
        format = value;
        updateTime();
      });

      function scheduleUpdate() {
        // キャンセルの際のためにtimeoutIdを保存
        timeoutId = $timeout(function() {
          updateTime(); // DOMの更新
          scheduleUpdate(); // 次の更新をスケジューリング
        }, 1000);
      }

      element.on('$destroy', function() {
        $timeout.cancel(timeoutId);
      });

      // UI更新プロセス開始
      scheduleUpdate();
    }

    return {
      link: link
    };
  });
<!doctype html>
<html ng-app="docsTimeDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
<script>angular.module('docsTimeDirective', [])
  .controller('Ctrl2', function($scope) {
    $scope.format = 'M/d/yy h:mm:ss a';
  })
  .directive('myCurrentTime', function($timeout, dateFilter) {

    function link(scope, element, attrs) {
      var format,
          timeoutId;

      function updateTime() {
        element.text(dateFilter(new Date(), format));
      }

      scope.$watch(attrs.myCurrentTime, function(value) {
        format = value;
        updateTime();
      });

      function scheduleUpdate() {
        // キャンセルの際のためにtimeoutIdを保存
        timeoutId = $timeout(function() {
          updateTime(); // DOMの更新
          scheduleUpdate(); // 次の更新をスケジューリング
        }, 1000);
      }

      element.on('$destroy', function() {
        $timeout.cancel(timeoutId);
      });

      // UI更新プロセス開始
      scheduleUpdate();
    }

    return {
      link: link
    };
  });
</script>
  </head>
  <body>
    <div ng-controller="Ctrl2">
      Date format: <input ng-model="format"> <hr/>
      Current time is: <span my-current-time="format"></span>
    </div>
  </body>
</html>

ここで注意すべき点が幾つかあります。 module.controller APIのように、module.directive内の関数の引数は、依存関係を注入します。 この場合であれば、$timeoutとdateFilterをディレクティブのlink関数内部で使用することが出来ます。

element.on('$destroy', ...)をイベントとして登録しています。 何が$destroyイベントを発火するのでしょうか?

AungularJSには、AngularJSが発行する幾つかの特別なイベントが存在します。 AngularのコンパイラによってコンパイルされたDOMノードが削除された際に、 $destroyイベントを発行します。 同様に、AngularJSのスコープが削除されると、 $destroyイベントがリスニングスコープにブロードキャストされます。

このイベントをリッスンすることにより、メモリーリークを引き起こすイベントリスナーを削除することが出来ます。 リスナーはスコープへ登録され、それらが削除されると要素が自動的にクリーンアップされますが、 もし、サービス上または削除されないDOMノード上にリスナーを登録した場合、 自分自身でそれをクリーンアップするか、さもなくばメモリーリークの危険を抱えることになります。

ベストプラクティス: ディレクティブは、自身が作成された後にクリーンアップを行うようにすべきです。 element.on('$destroy', ...)またはscope.$on('$destroy', ...)を使用して、 ディレクティブが削除された際にクリーンアップ関数を実行することが出来ます。

他の要素をラップするディレクティブの作成

隔離スコープを使用して、ディレクティブへモデルを渡す方法について見てきましたが、 場合によっては文字列やオブジェクトではなくテンプレート全体を渡せることが望ましいケースがあります。 では、"ダイアログボックス(dialog box)"コンポーネントを作成したいとしましょう。 ダイアログボックスは任意のコンテンツをラップすることが出来るべきです。

これを行うために、transcludeオプションを使用する必要があります。

<!doctype html>
<html ng-app="docsTransclusionDirective">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-dialog>Check out the contents, {{name}}!</my-dialog>
    </div>
  </body>
</html>
<div class="alert" ng-transclude>
</div>
angular.module('docsTransclusionDirective', [])
  .controller('Ctrl', function($scope) {
    $scope.name = 'Tobias';
  })
  .directive('myDialog', function() {
    return {
      restrict: 'E',
      transclude: true,
      templateUrl: 'my-dialog.html'
    };
  });

実際、このtranscludeは何をするのでしょうか? transcludeは、このオプションがディレクティブの内部では無く、 外部のスコープにアクセスする事によって、ディレクティブのコンテンツを作成します。

これを説明するために、下記の例を用意しました。 script.js内のリンク関数にJeffという名前を再定義していることに注目してください。 この場合、{{name}}の紐付けは何で解決されると思いますか?

<!doctype html>
<html ng-app="docsTransclusionExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-dialog>Check out the contents, {{name}}!</my-dialog>
    </div>
  </body>
</html>
<div class="alert" ng-transclude>
</div>
angular.module('docsTransclusionExample', [])
  .controller('Ctrl', function($scope) {
    $scope.name = 'Tobias';
  })
  .directive('myDialog', function() {
    return {
      restrict: 'E',
      transclude: true,
      scope: {},
      templateUrl: 'my-dialog.html',
      link: function (scope, element) {
        scope.name = 'Jeff';
      }
    };
  });

普通、この{{name}}はJeffになると予測すると思います。 しかしながら、この例ではTobiasがそのまま紐付けられているのが確認できます。

transcludeオプションは、スコープが入れ子にされる過程を変更します。 transclude指定されたディレクティブのコンテンツは、ディレクティブの内部をスコープとするのでは無く、 外部をスコープするようにします。 そのため、外部のスコープを通してコンテンツが渡されます。

もし、ディレクティブが自身のスコープを作らなかった場合、 scope.name = 'Jeff';は外部スコープを参照し、Jeffの出力を確認できるはずです。

この振る舞いは、ディレクティブが何らかのコンテンツをラップする事を出来るようにしてくれるため、 個々に使用したい各モデル毎に渡す必要が無くなります。 もし、使用したい各モデル毎に渡さなければいけないとしたら、 任意のコンテンツを持つことが出来なくなりますよね?

ベストプラクティス: 任意のコンテンツをラップするディレクティブを作りたい場合は、transcludeをtrueにして使用します。

次に、このダイアログボックスにボタンを追加したくなりました。 また、任意でそのディレクティブに独自の振る舞いの紐付けも行いたいとします。

<!doctype html>
<html ng-app="docsIsoFnBindExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div ng-controller="Ctrl">
      <my-dialog ng-hide="dialogIsHidden" on-close="hideDialog()">
        Check out the contents, {{name}}!
      </my-dialog>
    </div>
  </body>
</html>
<div class="alert">
  <a href class="close" ng-click="close()">&times;</a>
  <div ng-transclude></div>
</div>
angular.module('docsIsoFnBindExample', [])
  .controller('Ctrl', function($scope, $timeout) {
    $scope.name = 'Tobias';
    $scope.hideDialog = function () {
      $scope.dialogIsHidden = true;
      $timeout(function () {
        $scope.dialogIsHidden = false;
      }, 2000);
    };
  })
  .directive('myDialog', function() {
    return {
      restrict: 'E',
      transclude: true,
      scope: {
        'close': '&onClose'
      },
      templateUrl: 'my-dialog-close.html'
    };
  });

ディレクティブのスコープから、それを呼び出すことによって渡される関数を実行したいと考えますが、 それが登録されているスコープのコンテキスト内で実行されます。

先程、スコープ内オプションの=propの使用方法を見てきましたが、 上記の例では代わりに&propを使用しています。 &の紐付けは、隔離スコープがその関数を実行できるよに隔離スコープに公開しますが、 関数の元のスコープも保持されます。 ユーザーがダイアログ内のxをクリックすると、Ctrlのclose関数が実行されます。

ベストプラクティス: scopeオプションの&propは、ディレクティブに振る舞いを紐付けるためにAPIを公開したい際に使用します。

イベントリスナーを登録するディレクティブの作成

先程、リンク関数を使用してDOM要素が操作されるディレクティブを作成しました。 この例を元に、その要素のイベントに反応するディレクティブを作成してみましょう。

例えば、ユーザーに要素をドラッグさせるディレクティブを作成したい場合、どうすれば良いでしょうか?

<!doctype html>
<html ng-app="dragModule">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <span my-draggable>Drag ME</span>
  </body>
</html>
angular.module('dragModule', []).
  directive('myDraggable', function($document) {
    return function(scope, element, attr) {
      var startX = 0, startY = 0, x = 0, y = 0;

      element.css({
       position: 'relative',
       border: '1px solid red',
       backgroundColor: 'lightgrey',
       cursor: 'pointer'
      });

      element.on('mousedown', function(event) {
        //選択したコンテンツのデフォルトのドラッグをキャンセル
        event.preventDefault();
        startX = event.pageX - x;
        startY = event.pageY - y;
        $document.on('mousemove', mousemove);
        $document.on('mouseup', mouseup);
      });

      function mousemove(event) {
        y = event.pageY - startY;
        x = event.pageX - startX;
        element.css({
          top: y + 'px',
          left:  x + 'px'
        });
      }

      function mouseup() {
        $document.unbind('mousemove', mousemove);
        $document.unbind('mouseup', mouseup);
      }
    }
  });
<!doctype html>
<html ng-app="dragModule">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
<script>angular.module('dragModule', []).
  directive('myDraggable', function($document) {
    return function(scope, element, attr) {
      var startX = 0, startY = 0, x = 0, y = 0;

      element.css({
       position: 'relative',
       border: '1px solid red',
       backgroundColor: 'lightgrey',
       cursor: 'pointer'
      });

      element.on('mousedown', function(event) {
        //選択したコンテンツのデフォルトのドラッグをキャンセル
        event.preventDefault();
        startX = event.pageX - x;
        startY = event.pageY - y;
        $document.on('mousemove', mousemove);
        $document.on('mouseup', mouseup);
      });

      function mousemove(event) {
        y = event.pageY - startY;
        x = event.pageX - startX;
        element.css({
          top: y + 'px',
          left:  x + 'px'
        });
      }

      function mouseup() {
        $document.unbind('mousemove', mousemove);
        $document.unbind('mouseup', mouseup);
      }
    }
  });
</script>
  </head>
  <body>
    <span my-draggable>Drag ME</span>
  </body>
</html>

対話式のディレクティブの作成

ここで紹介する機能を使用して、テンプレート内でディレクティブを構成することが可能です。

また、ディレクティブを組み合わせたコンポーネントの構築が必要になることがあると思います。

ここでは、アクティブなタブに対応してコンテナのコンテンツを表示する、タブ機能が必要になったと仮定して説明していきます。

<!doctype html>
<html ng-app="docsTabsExample">
  <head>
    <script src="http://code.angularjs.org/1.2.1/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <my-tabs>
      <my-pane title="Hello">
        <h5 id="creating-custom-directives_source_hello">Hello</h5>
        <p>Lorem ipsum dolor sit amet</p>
      </my-pane>
      <my-pane title="World">
        <h5 id="creating-custom-directives_source_world">World</h5>
        <em>Mauris elementum elementum enim at suscipit.</em>
        <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
      </my-pane>
    </my-tabs>
  </body>
</html>
<div class="tabbable">
  <ul class="nav nav-tabs">
    <li ng-repeat="pane in panes" ng-class="{active:pane.selected}">
      <a href="" ng-click="select(pane)">{{pane.title}}</a>
    </li>
  </ul>
  <div class="tab-content" ng-transclude></div>
</div>
<div class="tab-pane" ng-show="selected" ng-transclude>
</div>
angular.module('docsTabsExample', [])
  .directive('myTabs', function() {
    return {
      restrict: 'E',
      transclude: true,
      scope: {},
      controller: function($scope) {
        var panes = $scope.panes = [];

        $scope.select = function(pane) {
          angular.forEach(panes, function(pane) {
            pane.selected = false;
          });
          pane.selected = true;
        };

        this.addPane = function(pane) {
          if (panes.length == 0) {
            $scope.select(pane);
          }
          panes.push(pane);
        };
      },
      templateUrl: 'my-tabs.html'
    };
  })
  .directive('myPane', function() {
    return {
      require: '^myTabs',
      restrict: 'E',
      transclude: true,
      scope: {
        title: '@'
      },
      link: function(scope, element, attrs, tabsCtrl) {
        tabsCtrl.addPane(scope);
      },
      templateUrl: 'my-pane.html'
    };
  });

myPaneディレクティブは、値が^myTabsrequireオプションを持ちます。 ディレクティブがこのオプションを使用する際に、$compileは指定されたコントローラーが見つからないと、エラーをスローします。 ^接頭辞は、このディレクティブは親のコントローラーを検索するという意味になります。 (^接頭辞が無いと、ディレクティブは自身の要素上からコントローラーを探そうとしてしまいます。)

では、このmyTabsコントローラーはどこから来ているのでしょうか? ディレクティブは、当然ですがcontrollerという名前のオプションを使用して、コントローラーを指定することが出来ます。 ご覧のとおりmyTabsディレクティブは、このオプションを使用しています。 ngControllerのように、このオプションはディレクティブのテンプレートにコントローラーを割り当てます。

myPaneの定義に戻りましょう。 リンク関数の最後の引数であるtabsCtrlに注目してください。 ディレクティブがコントローラーを必要とする場合、このリンク関数は4つ目の引数としてコントローラーを受け取ります。 この利点により、myPaneはmyTabsのaddPane関数を呼び出すことが出来ます。

経験豊富な方であれば、linkとcontroller間の違いについて疑問を抱くかもしれません。 基本的な違いとして、controllerはAPIを公開し、link関数はrequireを使用してコントローラーと相互作用することが出来ます。

ベストプラクティス: 他のディレクティブにAPIを公開したい場合はコントローラーを使用します。 そうでなければ、linkを使用します。

要約

ここまでで、ディレクティブのための主要な使用方法について見てきました。 これらの各サンプルは、自身のディレクティブを作成する際の良いお手本になると考えています。

もしかしたら、コンパイル処理をより詳しく知りたいと興味をお持ちになっているかもしれません。 その場合は、コンパイルガイドをご覧ください。

$compile APIページで、リファレンス用としてディレクティブのオプション一覧を確認することが出来ます。

 Back to top

© 2017 Google
Licensed under the Creative Commons Attribution License 3.0.

このページは、ページトップのリンク先のAngularJS公式ドキュメント内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • AngularJSの更新頻度が高いため、元のコンテンツと比べてドキュメントの情報が古くなっている可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。