ng-model
を使うようなdirectiveを作るときのパターンを何個か紹介します。
対象
Angularのversion
1.4系
対象な人
-
ng-model
が使えるdirectiveを作りたい -
NgModelController
について知りたい
対象じゃない人
-
ng-model
自体の使い方を知りたい
NgModelControllerのプロパティ、メソッド一覧
割りと説明なしで使うのでAngularJS: API: ngModel.NgModelControllerを参照しながらみてください。
Validators
form.$error.foo
でエラーを受け取りたいなら、ngModel.$validators.foo
にbooleanを返す関数を書きます。引数はそれぞれngModel.$modelValue
, ngModel.$viewValue
と同じものになります。formの詳しい情報はformとの連携を参照して下さい。
<form name="form">
<input type="text" ng-model="hoge" name="hoge" foo>
{{form.$error.foo}}
</form>
app.directive('foo', () => {
return {
restrict: 'A',
require: 'ngModel',
priority: -1,
link: (scope, element, attr, ngModel) => {
ngModel.$validators.foo = (modelValue, viewValue) => (modelValue | viewValue) === 'foo';
}
};
});
※ $validators.foo
に登録される関数は厳密にbooleanを返すべきです。例えば横着してundefined
はfalsyだろと思ってそのまま返すとformがvalidでもinvalidでもない状態になってしまいます。その状態でformの送信をform.$invalid
を見てガードしていたりするとすり抜けます。
装飾目的で標準のinputをラップする
イケてるUIを要素一つで表現したい等。
<!-- コード上はこう書きたい -->
<awesome-checkbox ng-model="foo" ng-true-value="'TRUE'"></awesome-checkbox>
<!-- こう展開されて欲しい -->
<div class="awesome-checkbox">
<input type="" ng-model="foo" ng-true-value="'TRUE'">
...
</div>
やり方は単純で、compileするときにng-model
など、使用するinput要素の属性にコピーするだけです。
例: ionic/checkbox.js at master · driftyco/ionicより
/**
* @ngdoc directive
* @name ionCheckbox
* @module ionic
* @restrict E
* @codepen hqcju
* @description
* The checkbox is no different than the HTML checkbox input, except it's styled differently.
*
* The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
*
* @usage
* ```html
* <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
* ```
*/
IonicModule
.directive('ionCheckbox', ['$ionicConfig', function($ionicConfig) {
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
template:
'<label class="item item-checkbox">' +
'<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
'<input type="checkbox">' +
'<i class="checkbox-icon"></i>' +
'</div>' +
'<div class="item-content disable-pointer-events" ng-transclude></div>' +
'</label>',
compile: function(element, attr) {
var input = element.find('input');
// compileでng-modelなどのattributeを追加する
forEach({
'name': attr.name,
'ng-value': attr.ngValue,
'ng-model': attr.ngModel,
'ng-checked': attr.ngChecked,
'ng-disabled': attr.ngDisabled,
'ng-true-value': attr.ngTrueValue,
'ng-false-value': attr.ngFalseValue,
'ng-change': attr.ngChange,
'ng-required': attr.ngRequired,
'required': attr.required
}, function(value, name) {
if (isDefined(value)) {
input.attr(name, value);
}
});
var checkboxWrapper = element[0].querySelector('.checkbox');
checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
}
};
}]);
見た目($viewValue)とモデル($modelValue)を分離する
input要素上では文字列で入力するが、コード上はオブジェクト等にしたい等。
実装すべきもの
-
ngModel.$formatters
にngModel.$modelValue
からngModel.$viewValue
へ変換する関数を追加する。 -
ngModel.$parsers
にngModel.$viewValue
からngModel.$modelValue
へ変換する関数を追加する。
あると嬉しいもの
ngModel.$validators.hoge
例:見た目'YYYY/MM/DD'
、値がmoment Plunkerでみる
<input type="text" ng-model="date" moment-input>
app.directive('momentInput', () => {
return {
priority: -1, // ngModelのpriorityが0なので-1以下にする
restrict: 'A',
require: '?ngModel',
controller: function($element) {
var ngModel = $element.controller('ngModel');
if (!ngModel) return;
// $modelValue to $viewValue
ngModel.$formatters.push(value => value && moment(value).format('YYYY/MM/DD'));
// $viewValue to $modelValue
ngModel.$parsers.push(value => value && moment(value, 'YYYY/MM/DD', true));
// ついでにvalidate
ngModel.$validators.date = modelValue => {
if (!modelValue) return true;
return modelValue.isValid();
};
}
}
})
中身が複雑なdirectiveを隠ぺいする
とにかくデータのやりとりはng-model
でやりたい、他のライブラリとかをラップしたい等。インターフェースがAngular標準のdirectiveと同じになるので使い方を共有しやすい。
実装すべきもの
- directive内部で値が更新したときに
ngModel.$setViewValue
でngModelの値を明示的に更新する処理を書く。 -
ngModel.$render
、(ngModel.$formatters
でもいいかも?)に外部から値が変更されたときの処理を書く。
あると嬉しいもの
-
ngModel.$isEmpty
に空判定を書けばrequired
が(length
プロパティを持っている場合はmaxlength
、minlength
も)効く。 ngModel.$validators.hoge
例: チェックボックスが複数あって、値を配列で扱う Plunkerでみる(上のURLと同じものです)
<checkbox-group ng-model="arr" items="items" required minlength="2" maxlength="3"></checkbox-group>
app.directive('checkboxGroup', () => {
return {
scope: {},
priority: -1,
restrict: 'E',
require: '?ngModel',
bindToController: {
items: '='
},
template: `
<div>
<label for="checkbox-{{$id}}" ng-repeat="item in ctrl.items">
<input id="checkbox-{{$id}}" type="checkbox" ng-model="item.selected" ng-change="ctrl.update()">{{item.label}}
</label>
</div>
`,
controller: function($element) {
var ngModel = $element.controller('ngModel');
if (!ngModel) return;
// 空判定
ngModel.$isEmpty = value => {
return !(angular.isArray(value) && value.length);
};
// 外から更新された時の処理
ngModel.$render = () => {
var value = ngModel.$viewValue;
if (!angular.isArray(value) || !angular.isArray(this.items)) return value;
this.items.forEach(item => item.selected = false);
value.forEach(v => this.items.some(item => item.selected = v === item.value));
return value;
};
// 内部で更新された時の処理
this.update = () => {
ngModel.$setViewValue(this.items.filter(item => item.selected).map(item => item.value));
};
},
controllerAs: 'ctrl'
}
})
ng-modelにオブジェクトを設定したときの注意点
ng-model
に設定された値(ng-model="ctrl.foo"
とします)がinvalidとなる場合、ctrl.foo
がundefined
になります。この挙動は割と困る場合が多いかと思われます。ng-model-options
のallowInvalid
をtrue
とすることで防ぐことができます。
<div ng-model-options="{allowInvalid: true}">
<hoge ng-model="ctrl.foo"></hoge>
<div>{{ctrl.foo.firstName}} {{ctrl.foo.lastName}}</div>
</div>
formとの連携
name
属性を付けるだけ。FormController
から辿れるようになります。下の例の場合はhoge.foo
、hoge.bar
。form全体のvalidationはname
がなくても適用されます。ngModel.$validators
をしっかり作っておくと、controllerでのvalidationや、viewにエラーを表示する部分で楽できます。
<form name="hoge">
<checkbox-group name="foo" ng-model="foo" items="items" required></checkbox-group>
<input type="text" name="bar" ng-model="moment" moment-input>
</form>
ng-changeとの連携
ng-model
を使うともれなくng-change
を使えるようになります。ただし、ng-model
にオブジェクトが設定されているときはプロパティだけ変えても何も起こりません(!==
で判定してる)。