在自定义指令中使用NgModelController(Using NgModelController with Custom Directives)

原文地址: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 到指定时间单位的转换,以及逆向的转换.

一旦你了解了parserformatter的基本工作流程,那么几乎可以创建所有类型的指令,即使其中包含着非常复杂的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。在示例中,指的是ngModelcontroller

<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

需要了解的是directiveself-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指令要处理的各种数据解决,主要包括四种:

  1. 在作用域中实际的model的值value。例如,在程序中,我们可能这样设置:$scope.email_notify_pref = 3600;
  2. ngModelCtrl.$modelValue是实际model的一个副本;
  3. ngModelCtrl.$viewValue是在view中使用的数据的副本;
  4. 最终就是在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);  

这就是所有的内容!刚开始的时候可能会有一些迷惑,但是一旦你理解了工作流程以及各组件之间的最优结合方式,你就会发现该系统具有非常高的灵活性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值