原文地址:https://www.nadeau.tv/using-ngmodelcontroller-with-custom-directives/
在自定义指令中使用NgModelController
在AngularJS中创建directive(指令)是相对容易的。但是大多数指令都需要和标识它们状态的model(模型)进行通信。对于这些情况,可以自定义model处理句柄,也可以使用AngularJS内置的NgModelController
——与之类似的ng-model
用来处理input
(输入框)和select
(下拉列表)。
directive 示例:<time-duration />
通过一个简单的示例,让我们一起构建一个指令,它允许用户输入不同单位的时间区间。
<h3>My Super App</h3>
How often should we email you?
<time-duration ng-mode="email_notify_pref" />
我们的指令将会展示输入域:
1.input
,用于用户输入数字
2.selection
,用户选择时间单位
model
后台的model会将用户的输入以second(秒)为单位进行存储。当渲染model时,我们将会以最大的时间单位展示它的值。例如,如果model的值是 3600 second ,则显示为 1 hour 。
通过这个示例,我们可以探究Angular如何存储model的值,以及parser(解析器)和formatter(格式器)是如何运作的。示例中,真实的model值将一直保持以 second 为单位的数值,但是在屏幕中展示的值被分解为时间单位的选项和对应的数值。这意味着,我们必须处理从 second 到指定时间单位的转换,以及逆向的转换.
一旦你了解了parser和formatter的基本工作流程,那么几乎可以创建所有类型的指令,即使其中包含着非常复杂的model。
setup
首先来定义指令:
function TimeDurationDirective() {
var tpl = "<div> \
<input type='text' ng-model='num' size='80' /> \
<select ng-model='unit'> \
<option value='secs'>Seconds</option> \
<option value='mins'>Minutes</option> \
<option value='hours'>Hours</option> \
<option value='days'>Days</option> \
</select> \
</div>";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
replace: true,
link: function(scope, iElement, iAttrs, ngModelCtrl) {
//TODO
}
};
};
angular.module('myModule').directive('timeDuration', TimeDurationDirective);
到目前为止,这个简单的指令仅仅实现了渲染HTML模板的作用,包括展示一个input
(用于用户输入数值)和一个select
(用于用户选择时间单位)。
require: ‘ngModel’
这个指令比较特殊的就是require
属性,它告诉Angular我们的指令需要其他指令的controller。在示例中,指的是ngModel
的controller:
<time-duration ng-model="email_notify_pref" />
当需要controller并且通过require
指明后,该controller将作为link
函数的最后一个参数(如上代码中的ngModelCtrl
)。此时就将我们的timeDuration
指令和ngModel
指令联系在一起了。
需要注意的是,require
有两个特殊的语法规则,需要根据构建指令的不同按需选择:
需要父级controller:require: '^someController'
如果不是明确地需要管理指令的的controller,可以使用脱字符号(^
),告诉Angular向上追溯DOM树,直到寻找到指定的controller。适用于创建结构相关的多个directive联合协作的情况。例如:
<my-directive ng-model="myModel">
<other-directive></other-directive>
</my-directive>
可以在otherDirective
中使用require: '^ngModel'
,此时它会自动提取父级元素的model controller。
非必须使用的controller: require: '?optionalController'
使用问号( ? )告诉Angular可以使用一个非必须的controller。Angular会自行寻找指明的controller,如果未找到,会返回null
。
结合使用: require: '?^optionalParentController'
最后,可以将两者结合,得到一个非必须的父级controller。
什么是directive? 什么是controller?
你可能会对这几个问题有疑问:什么是ngModel
?什么是ng-model
?什么是NgModelController
?
ngModel
是指令的名字,ng-model
是指令在HTML中的引用。而NgModelController
是指令的controller。
需要了解的是directive是self-contained(自包含的)。为了使directive能够抛离UI独立完成一些有意思的事情(例如在应用中影响 state(状态)),它需要通过controller进行通信。controller就像是这些自包含的directive的双工通信通道。
大部分的directive自身拥有一个或者使用其他指令的controller。如果没有controller,那么指令将因此成为一个单纯的展示工具,而无法影响state的改变以及和其他directive进行交互。
在示例中,我们使用内建的的NgModelController
去处理setting/saving(设置/存储) model的值。ng-model
指令自身几乎没有什么功能,它的存在只是为了NgModelController
。
当使用require: 'ngModel'
,实际是在说,“请给我ng-model
指令的controller”。
使用NgModelController
我们已经简单的介绍了NgModelController
,下面让我们更近一步地剖析它的使用。
在此之前,重新回顾下,目前为止我们的link
函数:
link: function(scope, iElement, iAttrs, ngModelCtrl) {
//TODO
}
scope
是绑定在HTML模板上的作用域,iElement
是真实的HTML DOM元素,iAttrs
是HTML元素上添加的属性,ngModelCtrl
则是被需要的NgModelController
的实例。
现在来讨论下timeDuration
指令要处理的各种数据解决,主要包括四种:
- 在作用域中实际的model的值value。例如,在程序中,我们可能这样设置:
$scope.email_notify_pref = 3600;
ngModelCtrl.$modelValue
是实际model的一个副本;ngModelCtrl.$viewValue
是在view中使用的数据的副本;- 最终就是在view中实际展示的数据。例如,在HTML表单(form)中的值,或者更像是一个元素的
innerHTML
,也有可能是在指令作用域中配置的数据。
NgModelControllers
的工作就是在处理值在这四个阶段的双向传播。例如,如果改变form(#4)中的值,则可以使用NgModelControllers
以保证真实的model(#1)的值得到更新。类似的,如果改变model(#1)中的值,则可以使用NgModelControllers
更新UI中展示的值(例如,checkbox
是选中还是非选中状态等等)。
$formatters
管道
首先需要了解的是,如何将real model(真实的值)转换为在view中使用的值。在代码示例中,意味着将 3600 second 转换为 1 hour 。
第一步是决定在view(ngModelCtrl.$viewValue
) 中所使用的数据结构。示例中,数据结构就像是HTML模板中定义的那样。有两个域,input
输入框用于输入数字,select
下拉选择框用于选择时间单位。使用有两个属性的对象存储是最简单的了。所以,在你的脑海中应该会想到$viewValue
看起来是这样的(这并非是实际编写代码,只是在脑海中呈现的结构而已):
$viewValue = { num: 1, unit: "hours" };
那么,如何进行实际操作呢?NgModelController
通过将真实的model的值传递给$formatters
的数组来运作,$formatters
是一组将model的值value转换成view展示value的函数。最终$viewValue
被设置的值就是$formatters
的返回值(大都数情况下,可能仅拥有一个formatter)。
我们的link
函数变化如下:
link: function(scope, iElement, iAttrs, ngModelCtrl) {
// Units of time
multiplierMap = {seconds: 1, minutes: 60, hours: 3600, days: 86400};
multiplierTypes = ['seconds', 'minutes', 'hours', 'days']
ngModelCtrl.$formatters.push(function(modelValue) {
var unit = 'minutes', num = 0, i, unitName;
modelValue = parseInt(modelValue || 0);
// Figure out the largest unit of time the model value
// fits into. For example, 3600 is 1 hour, but 1800 is 30 minutes.
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
};
});
}
虽然看起来有些冗长,但上述代码实现的功能是将秒级的时间数值转换为最大的时间单位数值,例如将 3600 second 装换为 1 hour 。在当中展示了如何将一个函数添加到$formatters
的管道中。
ngModelCtrl.$formatters.push(function() {...});
之后在formatter函数中,返回了最终要设置到$viewValue
中的值:
return {
unit: unit,
num: num
};
所以到目前为止,管道流程如下所示:
$scope.email_notify_pref = 3600
↓
ngModelCtrl.$formatters(3600)
↓
$viewValue = { unit: 'hours', num: 1}
根据$viewValue更新UI
现在我们需要通过ngModelCtrl.$render
实现在浏览器屏幕(browser screen)中渲染$viewValue
的值。
在示例中,我们在模板(template)中使用指令作用域(directive scope)将数据绑定到form表单中。这意味着我们仅需要更新作用域的值。但是如果你并没有使用作用域,则需要使用其他的一些DOM更新操作。例如,如果你想要创建封装jQuery插件的指令,那么就需要在render
方法中调用jQuery的函数。
在此,我们实际只需要简单的将view的值赋给在模板中使用的scope
的变量即可。
ngModelCtrl.$render = function() {
scope.unit = ngModelCtrl.$viewValue.unit;
scope.num = ngModelCtrl.$viewValue.num;
};
$parsers管道
类似于使用$formatters
管道将model的值配置到$viewValue
的过程,$parsers
管道负责将$viewValue
配置到$modelValue
中(最终被配置到实际的model)。
我们只需要将我们自定义函数push
到管道中即可:
ngModelCtrl.$parsers.push(function(viewValue) {
var unit = viewValue.unit, num = viewValue.num, multiplier;
// Remember multiplierMap was defined above
// in the formatters snippet
multiplier = multiplierMap[unit];
return num * multiplier;
});
可视化的管道流程如下:
$viewValue.email_notify_pref = { unit: 'hours', num: 1 };
↓
ngModelCtrl.$parsers({unit: 'hours', num: 1})
↓
$modelValue = 3600;
在UI变化时更新$viewValue
最后需要做的是在UI发生变化时,执行ngModelCtrl.$setViewValue()
,以保证数据同步到$viewValue
。
那么我们如何知道value发生变化呢?这完全依赖于你的指令。在示例中,因为将变量直接绑定在指令的scope
作用域下,所以可以通过添加watch
监听获悉value的变化:
scope.$watch('unit + num', function() {
ngModelCtrl.$setViewValue({ unit: scope.unit, num: scope.num });
});
当然你也可以用其他你喜欢的形式。例如,使用jQuery,希望监听在一些select下拉框发出的change事件,则可以写成如下形式:
$(iElement).find('select').on('change', function() {
ngModelCtrl.$setViewValue(...);
});
整体流程图
<realModel> → ngModelCtrl.$formatters(realModel) → $viewModel
↓
↑ $render()
↓
↑ UI changed
↓
ngModelCtrl.$parsers(newViewModel) ← $setViewModel(newViewModel)
完整的指令
将所有代码片组合到一起,完整的指令如下:
function TimeDurationDirective() {
var tpl = "<div> \
<input type='text' ng-model='num' size='80' /> \
<select ng-model='unit'> \
<option value='secs'>Seconds</option> \
<option value='mins'>Minutes</option> \
<option value='hours'>Hours</option> \
<option value='days'>Days</option> \
</select> \
</div>";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
replace: true,
link: function(scope, iElement, iAttrs, ngModelCtrl) {
// Units of time
multiplierMap = {seconds: 1, minutes: 60, hours: 3600, days: 86400};
multiplierTypes = ['seconds', 'minutes', 'hours', 'days']
ngModelCtrl.$formatters.push(function(modelValue) {
var unit = 'minutes', num = 0, i, unitName;
modelValue = parseInt(modelValue || 0);
// Figure out the largest unit of time the model value
// fits into. For example, 3600 is 1 hour, but 1800 is 30 minutes.
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);
这就是所有的内容!刚开始的时候可能会有一些迷惑,但是一旦你理解了工作流程以及各组件之间的最优结合方式,你就会发现该系统具有非常高的灵活性。