入力コンポーネントを作ってみる(1)
続いて図2に示すようなボタングループのUIで構成するセレクトやラジオボタンと同様の機能を実現してみます。
今回は先ほどの例と違いこれまでよりもずっと複雑になります。具体的には、以下の方法を説明します。
- 親子関係をもったディレクティブの作成方法
- 複数のディレクティブを連携するためのコントローラの作成と利用方法
- ngModelを使った値の取得と、値の設定などの方法
実際に作成するコンポーネントの概要
今回作成するコンポーネントは図2に示すように複数のボタンをラジオボタンのように機能させます。実際にDOM上のHTMLはリスト4のようになります。
<div class="btn-group" role="group" >
<button type="button" class="btn btn-primary">はい</button>
<button type="button" class="btn btn-default">どちらでもない</button>
<button type="button" class="btn btn-default">いいえ</button>
</div>
実際にボタンを押した際にそれぞれのボタンのclass指定をbtn-defaultからbtn-primaryに変えることで、現在押されているボタンがわかるようにします。続いて、このコンポーネントを使用する際にHTMLテンプレートに記述するコードを考えます。ここで注意すべきことは、選択肢をどのように定義するかです。この設計によって作成するディレクティブのコードも変わってきます。1つのディレクティブで、属性で選択肢を管理するのが最も簡単な実装方法ではありますが、今回は、リスト5のようにタグの親子関係は同じように保つよう、buttonGroupディレクティブとbuttonItemディレクティブを作成します。
<button-group ng-model="value"> <!--(1)ng-modelが必ずある -->
<!--(2)選択肢は、button-itemタグとして自由に追加、削除できる -->
<button-item value="yes">はい</button-item>
<button-item value="other">どちらでもない</button-item>
<button-item value="no">いいえ</button-item>
</button-group>
また、見た目以外では、(1)入力コンポーネントとしてのngModelを必須とするようにします。そして、(2)選択肢はbuttonItemディレクティブをbuttonGroupディレクティブの子として追加できるようし、選択された時の値をvalue属性で定義し、表示するボタンのラベル等はタグ内に自由に記述できるようにします。
ディレクティブの構造
今回、図3に示すように少々ディレクティブの構造と関係が複雑になり、また、ディレクティブ同士の連携のためにコントローラも使用します。ただし、単純な親子関係があるようなディレクティブではおおよそこのような構造になると思います。
この構造をもとにディレクティブの定義を行ったのがリスト6になります。
//(1)コンポーネントとして独立したモジュールとして定義する
var module = angular.module('ui.buttonGroup',[]);
module.directive('buttonGroup',[ButtonGroup]);
module.directive('buttonItem',[ButtonItem]);
//(2)buttonGroupディレクティブで使用するコントローラ
function ButtonGroupController($scope){
//(省略)
}
//(3)buttonGroupディレクティブの定義
function ButtonGroup(){
return {
restrict : 'E',
scope : {},
replace : true,
controller : ['$scope',ButtonGroupController],
require : ['buttonGroup','ngModel'], //(4)必要なディレクティブの指定(複数指定)
transclude : true, //(5)ディレクティブ内の記述をテンプレートとして扱うか?
template : '<div class="btn-group" role="group" ng-transclude></div>', //(6)テンプレート本文
compile : function(element,attrs){ //(7)compileメソッドからlinkメソッドを返す
var link = function($scope,$element,$attrs,controllers){
//(省略)
};
return link;
}
}
}
//(8)buttonItemディレクティブの定義
function ButtonItem(){
return {
restrict : 'E',
scope : {},
replace : true,
require : '^^buttonGroup', //(9)必要なディレクティブの指定(1つのみ)
transclude : true,
template: '<button type="button" class="btn" ng-class="{ \'btn-primary\' : active, \'btn-default\' : !active }" ng-transclude></button>',
compile : function(element,attrs){
var link = function($scope,$element,$attrs,buttonGroupController){
//(省略)
};
return link;
}
}
}
(1)ではコンポーネントを1つのモジュールとして定義し、その中で2つのディレクティブを定義しています。再利用性を高くするために別のモジュールとして定義しています。
(2)ではbuttonGroupディレクティブのButtonGroupControllerの定義をします。ここではまず、コントローラとディレクティブの関係に着目して説明しますので中身は省略します。(3)ではbuttonGroupディレクティブの定義、(8)ではbuttonItemディレクティブの定義をしていますが、おおよそ同じような属性の構造になっているのがわかると思います。
続いて(4)と(9)のrequire属性では、作成するディレクティブが他のディレクティブの依存(指定したディレクティブのコントローラ)の指定を行います。この指定に応じて、親子関係がチェックができることとlinkメソッドで取得できるコントローラが変わります。また、require属性では以下のような指定ができます。
| 指定方法 | 説明 |
|---|---|
| ディレクティブ名 | 自分自身のディレクティブと同じ要素に指定したディレクティブが必ず存在すること。 |
| ? + ディレクティブ名 | 自分自身のディレクティブと同じ要素に指定したディレクティブが存在すること。ただし、見つからない場合にはnullをコントローラに指定 |
| ^ + ディレクティブ名 | 指定したディレクティブが自分自身を含めて親ノードをたどって必ず存在すること。 |
| ?^ + ディレクティブ名 | 指定したディレクティブが自分自身を含めて親ノードをたどって存在すること。ただし、見つからない場合にはnullをコントローラに指定 |
| ^^ + ディレクティブ名 | 指定したディレクティブが自分自身を含めず親ノードをたどって必ず存在すること。 |
| ?^^ + ディレクティブ名 | 指定したディレクティブが自分自身を含めず親ノードをたどって存在すること。ただし、見つからない場合にはnullをコントローラに指定 |
(5)ではtransclue属性をtrueに指定しています。こちらはディレクティブ配下のコンテンツをテンプレートとして扱うかの指定でtrueもしくは'element'が指定できます。子にbuttonItemディレクティブがあり、この記述をテンプレートとして実行する必要がありますのでtrueを指定します。'element'はその要素自身もテンプレートとして扱う場合でngRepeatなどで使われています。
また、子をテンプレートとした場合に、実際には親テンプレートのどこにコンテンツを挿入するのかがわからなくなります。そのために、(6)のようにテンプレート側にはng-transclude属性を指定して、その場所を指定する必要があります。この関係を表したものが図4です。
最後に、(7)のcompileメソッドとその中でリターンされているlinkメソッドについて、サンプル上はcompileメソッドでは何もしていませんので特にcompileメソッドを定義しなくてもよいのですが、今回は、説明もかねてcompileメソッドを定義しました。
このcompileメソッドはテンプレートをパースする際に呼ばれるメソッドで、まだ、スコープが取得できないのと、実際にテンプレートにコンテンツが差し込まれる前の時点で処理をしたい場合に実装します。例えば、属性に指定した値に応じて、スコープを使わず自分で値を処理したい場合にもこのcompile処理の中で事前準備をするなどの利用ができます。
また、compileメソッドを定義した場合には、そのメソッド内でlinkメソッドを返す必要があります。筆者の場合には、compileメソッドの実装が必要になるケースを想定して、このような構造をデフォルトとして記述しています。
