属性として与えられたAngular式を、ビューとバインドするとき、通常は、ngModelが使われます。しかし、
ngModelでは一つしか属性をバインドすることができません。これを自前で行うためには、$parseというサービスを使います。このための基本パターンを紹介します。
また、比較のため、ngModelを使って同等のことをするパターンも示します。ちなみに、ngModelを使うとCSSのクラスが自動で設定されたり、formetter, parserで他のディレクティブと連携するといったことが行えますが、これらはngModelの機能なので自前でバインドした場合、これらの機能は使えません。
ngModelを使わない双方向バインド
コード
module.directive('directiveName', ['$parse', function ($parse) {
function testAttrHandler(accessor, element, scope) {
var field = {
event: '{eventName}',
setter: function (element, val) {
//TO DO
},
getter: function (element, val) {
//TO DO
}
};
/* model -> view */
scope.$watch(accessor, function (val) {
field.setter(element, val);
}, true);
/* view -> model */
element.on(field.event, function (event, ui) {
scope.$apply(function () {
accessor.assign(scope, field.getter(element, event, ui));
});
});
}
var handlerMap = {
testAttr: testAttrHandler
};
return {
compile: function (element, attr) {
var accessors = {};
angular.forEach(attr, function (exp, attrName) {
if (!handlerMap[attrName]) {
return;
}
accessors[attrName] = $parse(exp);
});
return function prelink(scope, element, attr) {
angular.forEach(accessors, function (accessor, attrName) {
handlerMap[attrName](accessor, element, scope);
});
};
}
};
}]);
$parseの使い方
$parseにAngular式を与えるとaccsessorを返します。accsessor(scope)で値を取得し、accsessor.assign(scope, value)で値をセットします。
$parseはscopeに依存しないので、prelinkの外側、compileの内側で行うのが良いです。このようにすることで、ng-repeatのように一度のcompileで複数のlinkが行われる時、一度だけ$parseが呼ばれるようになります。
$watchとaccessor
watch関数は、文字列でwatch対象を受け取るのが普通ですが、関数の受け取ることもできます。この関数のインタフェースは、$parseが返すaccessorと互換性があるので、$parseの戻り値をそのまま$watchに渡せば監視ができます。
文字列を与えても内部的には$parseが使われているだけなので、せっかくaccessorを作ったのだから、それを$watchに与える方が、無駄な$parseが発生せず、パフォーマンス的にも優れています。また、コードも見やすくすっきりします。
ngModelを使う場合
ngModelを使う場合は、同等のことをするのに、ngModel.$render, ngModel.$modelValue, ngModel.$setViewValueの3つを使います。
コード
module.directive('directiveName', [function () {
function ngModelHandler(ngModel, element, scope,) {
var field = {
event: '{eventName}',
setter: function (element, val) {
//TO DO
},
getter: function (element, val) {
//TO DO
}
};
/* model -> view */
ngModel.$render = function () {
field.setter(element, ngModel.$modelValue);
};
/* view -> model */
element.on(field.event, function (event, ui) {
ngModel.$setViewValue(field.getter(element, event, ui));
if (!scope.$$phase) {
scope.$apply();
}
});
}
return {
require: '?ngModel',
compile: function (element, attr) {
return function prelink(scope, element, attr, ngModel) {
ngModelHandler(ngModel, element, scope);
};
}
};
}]);
応用
これらをさらに汎用化することで、以下のようなライブラリを作りました。