原文:https://www.nadeau.tv/using-ngmodelcontroller-with-custom-directives/
指令例子:
自定义一个指令,用户可以用输入时间,可以选择时间单位。
My Super App
How often should we emal you? {{email_notify_pref}}
这个指令有两个输入框:
1、一个文本输入框,可以输入数字
2、一个下拉框,可以选择时间单位
模型 model
我们将会把用户输入的数值以秒的形式存储于后端模型(model),当渲染模型(model)时,我们会根据时间单位转化一下显示在输入框中。例如:当我们模型(model)为3600秒时,指令将显示1 hour。
这个指令将会展示Angular存储模型(model)数据时,如何解析($parsers)与格式化($formatters)数据。模型(model)一直以秒的形式存储,当展示到页面上时会被解析成一个数字与一个时间单位。这意味着我们必须处理从“秒”到 “时间单位/数值”,再到“秒”的转换。
一旦明白了解析(parsers)与格式化(formatters)的进本工作流程,我们将会自定义各种复杂的指令。
步骤
开始定义指令
function TimeDurationDirective(){
let tpl=``
return {
restrict:'E',
template: tpl,
require:'ngModel',
replace:true,
link: function(scope, iElement, iAttrs, ngModelCtrl){
//TODO
}
}
};
angualr.module('myModule').directive('timeDuration',TimeDurationDirective);
现在为止,只是创建了一个显示输入框(输入数字)和下拉菜单(选择时间单位)的指令。
使用NgModelController
进一步讨论之前,先来看看link函数的参数
link:function(scope,iElement,iAttrs,ngModelCtrl){
//TODO
}
scope:指令绑定在模板上的scope,
iElement:实际的HTML DOM元素,
iAttrs:原始指令HTML的属性集合,
ngModelCtrl:引入的NgModelController的实例。
现在让看一下指令一共处理几种数据。这里一共有4种不同的值
1、存储在scope上的数据模型(model),例如我们可能在代码中设置这样的值:$scope.email_notify_pref=3600;
2、ngModelCtrl.$modelValue: 数据模型(model)的拷贝;
3、ngModelCtrl.$viewValue: html上的值的拷贝;
4、html上的值。
NgModelController的任务就是反复的处理这四个数值。例如,如果更改表单(#4)里的数值,我们用NgModelController 确保数据模型(#1)更新;相反,当更改数据模型(#1)的值,我们用NgModelController确保UI上的数值也更新。
$formatters管道
第一个问题就是如何将模型(model)上的值转换成页面上的值。在本示例中就意味这将3600s转换成1hour
第一步决定我们页面上将要用的数据(ngModelCtrl.$viewValue)的数据结构。对于我们来说,它是由HTML模板中的表单决定的,一个数字的输入框和一个时间单位的选择框。存储这个的最简单方法是具有两个属性的对象{num:1,unit:'hours'}。ngModelController通过$formatters里的函数对ngModelCtrl.$modelValue进行处理,将最终的返回值赋给ngModelCtrl.$viewValue。
这样的话link函数将会变成下面这样:
link: function(scope, iElement, iAttrs, ngModelCtrl) {
// 时间单位
let multiplierMap = {seconds: 1, minutes: 60, hours: 3600, days: 86400};
let multiplierTypes = ['seconds', 'minutes', 'hours', 'days']
ngModelCtrl.$formatters.push(function(modelValue) {
var unit = 'minutes', num = 0, i, unitName;
modelValue = parseInt(modelValue || 0);
// 计算出model的最大单位时间
//例如,3600是1小时,但1800是30分钟
for (i = multiplierTypes.length-1; i >= 0; i--) {
unitName = multiplierTypes[i];
if (modelValue % multiplierMap[unitName] === 0) {
unit = unitName;
break;
}
}
if (modelValue) {
num = modelValue / multiplierMap[unit]
}
return { unit: unit, num: num };
});
}
现在,管道看起来是这样的:
$scope.email_notify_pref = 3600
↓
ngModelCtrl.$formatters(3600)
↓
$viewValue = { unit: 'hours', num: 1}
根据$viewValue更新UI
将$viewValue的数值渲染到页面上是通过ngModelCtrl.$render函数实现的。
在我们的例子中,我们使用指令scope将值绑定到表单,意味着我们只需更新scope上的值就可以了。
下面是我们的$render方法,将视ngModelCtrl.$viewValue分配给我们在HTML模板中使用的scope。
ngModelCtrl.$render = function() {
scope.unit = ngModelCtrl.$viewValue.unit;
scope.num = ngModelCtrl.$viewValue.num;
};
$parsers管道
类似于$formatters管道将模型(model)的值转换为$viewValue,我们通过$parsers管道将$viewValue转换为$modelValue(最终被分配到模型(model)中)
ngModelCtrl.$parsers.push(function(viewValue) {
var unit = viewValue.unit, num = viewValue.num, multiplier;
// 在上面已经定义了 multiplierMap
multiplier = multiplierMap[unit];
return num * multiplier;
});
让我们来看一下这条管道
$viewValue = { unit: 'hours', num: 1 };
↓
ngModelCtrl.$parsers({unit: 'hours', num: 1})
↓
$modelValue = 3600;
当UI变化时更新$viewValue
最后一个问题是,当UI中的值发生变化时,确保更新$viewValue。当值发生变化时,我们通过执行ngModelCtrl.$setViewValue()来执行此操作。
我们如何知道值什么时候发生变化?这完全取决于我们的指令。这在我们的例子中很简单,因为我们将值绑定到了指令的scope上了,所以我们设个watch就可以了。
scope.$watch('unit + num', function() {
ngModelCtrl.$setViewValue({ unit: scope.unit, num: scope.num });
});
完整的过程
realModel → ngModelCtrl.$formatters(realModel) → $viewModel
↑ ↓
$render()
↓
↑ UI changed
↓
ngModelCtrl.$parsers(newViewModel) ← $setViewModel(newViewModel)
完整指令代码
function TimeDurationDirective() {
let tpl=``;
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
replace: true,
link: function(scope, iElement, iAttrs, ngModelCtrl) {
let multiplierMap = {seconds: 1, minutes: 60, hours: 3600, days: 86400};
let multiplierTypes = ['seconds', 'minutes', 'hours', 'days'] ngModelCtrl.$formatters.push(function(modelValue) {
var unit = 'minutes', num = 0, i, unitName;
modelValue = parseInt(modelValue || 0);
for (i = multiplierTypes.length-1; i >= 0; i--) {
unitName = multiplierTypes[i];
if (modelValue % multiplierMap[unitName] === 0) {
unit = unitName;
break;
}
}
if (modelValue) {
num = modelValue / multiplierMap[unit]
}
return { unit: unit, num: num };
});
ngModelCtrl.$parsers.push(function(viewValue) {
var unit = viewValue.unit, num = viewValue.num, multiplier;
multiplier = multiplierMap[unit];
return num * multiplier;
});
scope.$watch('unit + num', function() {
ngModelCtrl.$setViewValue({ unit: scope.unit, num: scope.num });
});
ngModelCtrl.$render = function() {
if (!$viewValue) $viewValue = { unit: 'hours', num: 1 };
scope.unit = ngModelCtrl.$viewValue.unit;
scope.num = ngModelCtrl.$viewValue.num;
};
} };
};
angular.module('myModule').directive('timeDuration', TimeDurationDirective);