ng-repeat是AngularJS中一个非常重要和有意思的directive,常见的用法之一是将某种自定义directive和ng-repeat一起使用,循环地来渲染开发者所需要的组件。比如现在有一个form-text指令,用于快速构建起带自定义数据验证的表单文本框,我们可以用类似下面的代码方便地建立起一个简单的表单:

controller中:

$scope.form = {};
$scope.form.inputs = [{
model: 'name',
required: 'required',
title: '请输入用户名',
hints: '请输入5-15个字符',
regexp: '^.{5,15}$',
classes: ['form-text', 'repeat-widget']
}, {
model: 'phone',
required: 'required',
title: '请输入手机号',
hints: '请输入11位手机号',
regexp: '^1[0-9]{10}$',
classes: ['form-text', 'repeat-widget']
}, {
model: 'email',
required: 'required',
title: '请输入您的邮箱',
hints: '请正确输入您的邮箱地址',
regexp: '^[\\w-.]+@\\w+\\.\\w+$',
classes: ['form-text', 'repeat-widget']
}];

html:

<div class="form-text" ng-repeat="input in form.inputs" ng-model="form[input.model]" required="{{input.required}}" title="{{input.title}}" hints="{{input.hints}}" regexp="{{input.regexp}}" items="input.items"></div>

然而这样的用法有一个缺陷:当表单中含有其他类型的组件时,比如form-radio或form-checkbox(分别用于封装radio或checkbox),如果只是简单地将这些元素放入到inputs数组中,渲染结果可能并非如我们所期望的。

第一个容易想到的地方在于如何解决动态指定指令名称的问题。正如大家所熟悉的,自定义direcitve的restrict通常有三种取值,A(attribute),C(classname)和 E(element)。在ng-repeat中要动态指定元素名或属性名实现起来都较为困难,但是动态指定class名是比较容易的,常用的就有三种方法:既可以使用封装级别较高的ng-class、ng-attr-class指令,又可以使用朴素的class="{{}}"。
根据这样的思路,将上面代码中的class="form-text"换成ng-class="input.classes"是否可以完成这个任务呢?恐怕没有这么容易,虽然这是实现本文描述的业务逻辑的一个必要步骤,但并非最重要的步骤和关键点。

事实上,该业务的关键点在于理解AngularJS自定义指令的compile和link过程,并在恰当的时间点上予以灵活应用。本文将结合笔者的经验,由浅入深地介绍整个实现过程。当然,受限于本人的AngularJS水平,文中必然会出现不少纰漏和不严谨之处,欢迎大家批评指正。

一. 本文中涉及到的自定义directive
正如上文所提及,为了方便解释,我们先来创建了三种带简单验证功能的自定义directive: form-text、form-radio和form-checkbox,分别对应原生的input[type=text]、input[type=radio]和input[type=checkbox]元素。
placeholder对应原生元素的placeholder属性,hints对应错误提示,title对应输入框上方的文本,required表示元素是否为必填项,regexp为验证模式所需的正则表达式,items对应radio和checkbox的选项数组,数组中的每个对象有两个属性:text和value,分别对应显示的label和实际的value。这些命令都被添加到了form.widgets模块中:

(代码较长,为了不影响阅读,默认折叠了)

angular.module('form.widgets', [])
.directive('formText', function () {
return {
restrict: 'CE',
scope: {
placeholder: '@',
hints: '@',
title: '@',
required: '@',
regexp: '@',
type: '@'
},
require: 'ngModel',
template: ''
+ '<div style="margin-bottom:20px;">'
+ '<label>{{title}}</label>'
+ '<input class="form-control" ng-model="value" type="{{type}}"/>'
+ '<div ng-if="failed" style="margin-top:10px;" class="alert alert-danger">{{hints}}</div>'
+ '</div>',
link: function (scope, elem, attrs, ctrl) { var required = scope.required === 'true' || scope.required === 'required';
var regexp = new RegExp(scope.regexp); function validate(value) {
scope.failed = true; if (value === '' && !required) {
scope.failed = false;
} if (regexp.test(value)) {
scope.failed = false;
}
} ctrl.$formatters.push(function (value) {
scope.value = value || '';
}); scope.$watch('value', function (value) {
ctrl.$setViewValue(value);
validate(value);
});
}
};
})
.directive('formRadio', function () {
return {
restrict: 'CE',
scope: {
items: '=',
title: '@',
name: '@',
required: '@',
hints: '@'
},
require: 'ngModel',
template: ''
+ '<div type="radio" style="margin-bottom:20px;">'
+ '<label>{{title}}</label>'
+ '<div>'
+ '<label style="margin-right:20px;" ng-repeat="item in items"><input name="{{name}}" value="{{item.value}}" ng-model="validator.value" type="radio"/> {{item.text}}</label>'
+ '</div>'
+ '<div ng-if="failed" style="margin-top:10px;" class="alert alert-danger">{{hints}}</div>'
+ '</div>',
link: function (scope, elem, attrs, ctrl) { var required = scope.required === 'true' || scope.required === 'required';
var values = scope.items.map(function (item) {
return item.value + '';
}); function validate(value) { value += '';
scope.failed = false; if (required && values.indexOf(value) < 0) {
scope.failed = true;
}
} ctrl.$formatters.push(function (value) {
scope.validator.value = value || '';
}); scope.validator = {}; scope.$watch('validator.value', function (value) {
ctrl.$setViewValue(value);
validate(value);
}); }
};
})
.directive('formCheckbox', function () {
return {
restrict: 'CE',
scope: {
items: '=',
title: '@',
required: '@',
hints: '@'
},
require: 'ngModel',
template: ''
+ '<div type="radio" style="margin-bottom:20px;">'
+ '<label>{{title}}</label>'
+ '<div>'
+ '<label style="margin-right:20px;" ng-repeat="item in items"><input ng-model="validator.value[item.value]" type="checkbox"/> {{item.text}}</label>'
+ '</div>'
+ '<div ng-if="failed" style="margin-top:10px;" class="alert alert-danger">{{hints}}</div>'
+ '</div>',
link: function (scope, elem, attrs, ctrl) { var required = scope.required === 'true' || scope.required === 'required';
var values = scope.items.map(function (item) {
return item.value + '';
}); function validate(value) {
var checked = false;
for (var key in value) {
if (value[key]) {
checked = true;
}
}
scope.failed = required && !checked ? true : false;
} ctrl.$formatters.push(function (value) {
value = value || [];
scope.validator.value = {};
value.forEach(function (item) {
scope.validator.value[item] = true;
});
}); scope.validator = {}; scope.$watch('validator.value', function (value) {
var viewValue = [];
for (var key in value) {
if (value[key]) {
viewValue.push(key);
}
}
ctrl.$setViewValue(viewValue);
validate(value);
}, true); }
};
});

二. 自定义directive的声明式(declarative)使用
该类用法比较简单也比较典型,在这里就不多赘述。唯一需要注意的是,myApp模块依赖于form.widgets模块。

<form-text ng-model="form.name" required="required" title="请输入用户名" hints="请输入5-15个字符" regexp="^.{5,15}$"></form-text>
<form-text ng-model="form.email" required="required" title="请输入您的邮箱" hints="请正确输入您的邮箱地址" regexp="^[\w-.]+@\w+\.\w+$"></form-text>
<form-radio ng-model="form.gender" name="gender" items="form.genders" required="required" title="请选择性别" hints="请选择性别"></form-radio>
<form-checkbox ng-model="form.interest" items="form.interests" required="required" title="请告诉我们您的兴趣爱好" hints="请至少选择一项"></form-checkbox>
<script>
angular.module('myApp', ['form.widgets'])
.controller('myCtrl', function ($scope, $timeout, $compile) { var form = {};
$scope.form = form; form.genders = [{
text: '男',
value: 0
}, {
text: '女',
value: 1
}]; form.interests = [{
text: '电影',
value: 'films'
}, {
text: '音乐',
value: 'music'
}, {
text: '足球',
value: 'soccer'
}, {
text: '健身',
value: 'fitness'
}];
});
</script>

三. 利用ng-repeat循环声明单一类型的自定义directive
这种用法就是文首提到的用法。代码之前已经贴过了,在这里就不重复了。第一感可能会认为这种方案之所以可用,是因为ng-repeat的优先级非常低(ngRepeat指令的优先级为1000,参见文档https://docs.angularjs.org/api/ng/directive/ngRepeat)。是否的确是这个原因,第四种用法中会有所涉及,大家可以自行判断。

四. ng-repeat动态解析自定义directive
终于到了本文的核心部分, 首先我们要回答一个问题:
既然ng-repeat的优先级低,而ng-class的优先级高(默认优先级,0),ng-class解析完成后新的classname,比如form-text,已经被添加上(姑且这么认为,事实上ng-class对classname的修改并不是发生在link阶段),和第三种用法类似,既然如此,为什么基于classname的directive无法被识别?
因为太晚啦!因为太晚啦!因为太晚啦!(重要的事情说三遍)
在对于某段特定的HTML片段进行$compile时,该过程只会执行一次;$complie结束时,返回的link函数中已经包含了之后要调用的各directive的link方法的信息(这句话中的两个link含义不同,第一个link指AngularJS编译HTML的link阶段,第二个link指某一指令的link方法)。也就是说,虽然ng-class的优先级较高,在ng-class的link阶段已经将诸如form-text一类的classname添加到了DOM元素上(再强调一次,事实上classname在这一阶段并没有改变,但是为了强调生命周期的概念,这里姑且认为classname已经被改变),但是由于此时$compile阶段已经结束,由$compile返回的link函数中并不带有form-text的link方法,自然也未对其进行编译,因而无法渲染出我们想要的效果。
说到这里,我们至少确定了一点:由于ng-class的渲染发生在$compile阶段之后的link阶段,因此无法利用ng-class(ng-attr-class、class={{}}的原因类似,都和生命周期相关,但不完全一样)动态地改变classname并完成渲染。
原因找到了,让我们暂时先抛开ng-repeat,来简化一下这个问题,因为下面这个问题解决了,需求也就完成了,如何渲染:
<div ng-class="'form-text'" ng-model="form.name" required="required" title="请输入用户名" hints="请输入5-15个字符" regexp="^.{5,15}$"></div>
既然无法利用上一次的编译周期,那么手动启动一次难道还不行吗?答案是肯定的。而且AngularJS并没有隐藏$compile API,我们很容易通过依赖注入获取这一强大的功能。但关键是如何才能在上一个编译结束之后"立即"手动启动一次编译?这里思路不只一种,但利用setTimeout(或者$timeout)向event queue中添加一个异步回调函数应该是比较直接的做法。
问题到这里,解决方案也就比较明显了。为了query方便,让我们为刚刚的div添加一个class="repeat-widget"
然后在controller中加上如下一段代码:

$timeout(function () {
var widgets = document.querySelectorAll('.repeat-widget');
Array.prototype.slice.call(widgets).forEach(function (widget) {
var link = $compile(widget);
link($scope);
});
});

这段代码利用$compile编译已经有了form-text这个classname的div,编译完成后再将其link到当前$scope上,大功告成!
等等,本文的主题不是说要在ng-repeat的基础上实现吗?如果单单一个widget的声明还要写的这么复杂,那并没有什么实际意义啊。
要把这个方案移植到ng-repeat上,其实已经非常容易了,只有两个小问题还需要解决一下:
1. ng-repeat生成的子元素每一个都会带上ng-repeat属性,再次$compile又会repeat一次,形成我们不想要的双重循环,如何处理?
2. 需要link的不再是page级别的$scope,而是ng-repeat在循环中产生各个子scope,如何处理?
第一个问题很简单,removeAttribute即可。
第二个问题,我们可以利用angular.element(node).scope()来获取子scope。
请看下面的代码:

$timeout(function () {
var widgets = document.querySelectorAll('.repeat-widget');
Array.prototype.slice.call(widgets).forEach(function (widget) {
// 移除ng-repeat,防止被再次编译
widget.removeAttribute('ng-repeat');
// 获取子scope
var scope = angular.element(widget).scope();
var link = $compile(widget);
link(scope);
});
});

当然,如果每次利用ng-repeat动态地编译directive都需要这样一段代码的话,那也太不优雅了。别忘了我们是在AngularJS的世界中,把这个逻辑封装成一个更强大的directive才是这个方案的理想归宿。有兴趣的同学可以自行完成这一步。

本分享到此就告一段落了,如果本文能够或多或少地帮助大家加深对AngularJS中compile阶段和link阶段的理解,那就再好不过了。

最终的html:

<div ng-class="input.classes" ng-repeat="input in form.inputs" ng-model="form[input.model]" required="{{input.required}}" title="{{input.title}}" hints="{{input.hints}}" regexp="{{input.regexp}}" items="input.items" name="{{input.name}}"></div>

最终的controller:

angular.module('myApp', ['form.widgets'])
.controller('myCtrl', function ($scope, $timeout, $compile) { var form = {};
$scope.form = form; form.genders = [{
text: '男',
value: 0
}, {
text: '女',
value: 1
}]; form.interests = [{
text: '电影',
value: 'films'
}, {
text: '音乐',
value: 'music'
}, {
text: '足球',
value: 'soccer'
}, {
text: '健身',
value: 'fitness'
}]; var inputs = [{
model: 'name',
required: 'required',
title: '请输入用户名',
hints: '请输入5-15个字符',
regexp: '^.{5,15}$',
classes: ['form-text', 'repeat-widget']
}, {
model: 'phone',
required: 'required',
title: '请输入手机号',
hints: '请输入11位手机号',
regexp: '^1[0-9]{10}$',
classes: ['form-text', 'repeat-widget']
}, {
model: 'email',
required: 'required',
title: '请输入您的邮箱',
hints: '请正确输入您的邮箱地址',
regexp: '^[\\w-.]+@\\w+\\.\\w+$',
classes: ['form-text', 'repeat-widget']
}, {
model: 'gender',
required: 'required',
title: '请选择性别',
items: form.genders,
name: 'gender',
hints: '请选择性别',
classes: ['form-radio', 'repeat-widget']
}, {
model: 'interest',
required: 'required',
title: '请告诉我们您的兴趣爱好',
items: form.interests,
hints: '请至少选择一项',
classes: ['form-checkbox', 'repeat-widget']
}]; form.inputs = inputs; $timeout(function () {
var widgets = document.querySelectorAll('.repeat-widget');
Array.prototype.slice.call(widgets).forEach(function (widget) {
widget.removeAttribute('ng-repeat');
var scope = angular.element(widget).scope();
var link = $compile(widget);
link(scope);
});
});
});

作者:ralph_zhu

时间:2015-12-26 20:10

原文:http://www.cnblogs.com/front-end-ralph/p/5078786.html

理解AngularJS生命周期:利用ng-repeat动态解析自定义directive的更多相关文章

  1. iOS对UIViewController生命周期和属性方法的解析

    目录[-] iOS对UIViewController生命周期和属性方法的解析 一.引言 二.UIViewController的生命周期 三.从storyBoard加载UIViewController实 ...

  2. 【iOS开发】iOS对UIViewController生命周期和属性方法的解析

    iOS对UIViewController生命周期和属性方法的解析 一.引言 作为MVC设计模式中的C,Controller一直扮演着项目开发中最重要的角色,它是视图和数据的桥梁,通过它的管理,将数据有 ...

  3. 理解Fragment生命周期

    官网帮助文档链接:  http://developer.android.com/guide/components/fragments.html 主要看两张图,和跑代码 一,Fragment的生命周 二 ...

  4. android官方Api 理解Activity生命周期的回调机制(适合有基础的人看)

    原文地址:http://www.android-doc.com/training/basics/activity-lifecycle/starting.html#lifecycle-states 此处 ...

  5. Maven学习总结(16)——深入理解maven生命周期和插件

    在项目里用了快一年的maven了,最近突然发现maven项目在eclipse中build时非常慢,因为经常用clean install命令来build项目,也没有管那么多,但最近实在受不了乌龟一样的b ...

  6. 理解React生命周期的好例子

    class App extends React.Component { static propTypes = { }; static defaultProps = { }; constructor(p ...

  7. 深刻的理解Fragment生命周期 都在做什么,fragment生命周期

    先上一个生命周期的图片吧  下面挨个的说一下我平时 都怎么使用 这些 回调函数的 流程: onAttach() 作用:fragment已经关联到activity, 这个是 回调函数 @Override ...

  8. 10秒钟理解react生命周期

    慎点!这是一篇很水很水的文章, 抄自react中文文档, 本文详细介绍了react生命周期函数执行顺序, 以及各生命周期函数的含义和具体作用. 不同阶段生命周期函数执行顺序 挂载(Mounting) ...

  9. 深入源码理解SpringBean生命周期

    概述 本文描述下Spring的实例化.初始化.销毁,整个SpringBean生命周期,聊一聊BeanPostProcessor的回调时机.Aware方法的回调时机.初始化方法的回调及其顺序.销毁方法的 ...

随机推荐

  1. JAVA_OPTS

    JAVA_OPTS ,顾名思义,是用来设置JVM相关运行参数的变量. JVM:JAVA_OPTS="-server -Xms2048m -Xmx2048m -Xss512k" -s ...

  2. Jetty 发布web服务

    Jetty provides a Web server and javax.servlet container, plus support for HTTP/2, WebSocket, OSGi, J ...

  3. jQuery简单入门(四)

    4.表单应用 表单是HTML的重要组成部分,在采集.提交用户输入的信息和显示列表数据等需求中有重要作用 表单应用 一个简单的表单HTML示例: <form action=”url” method ...

  4. gulp系列:简单实践

    coffescript测试源码   gulp = require('gulp') #删除 1.清空目录 常用插件 gulp-clean .del (nodejs模块) del = require('d ...

  5. 摆脱npm的网络问题: 淘宝npm镜像

    在使用npm install的时候, 经常会因为网络问题, 各种安装不顺利, 一个字'烦躁'. 自从遇上淘宝npm之后,执行cnpm install之后, 怎一个'快'字了得. 闲话不多说, 直接上干 ...

  6. Cookie在IE缓存问题深度研究

    最近在发布net到生产环境的时候,测试发现了问题,IE的登录无效. 同样的版本在QA环境没有遇到问题. 代码一样,chrome,firefox 都可以.就是IE不行,调试发现 登录完成,读取cooki ...

  7. dpdk在虚拟机上出错处理

    目录 1. 所用系统与软件版本 2. 虚拟机配置 3. Ubuntu 12.04上的配置 3.1 准备 3.2 通过setup脚本进行配置 3.3 通过命令配置 4. CentOS 7.0上的配置 4 ...

  8. Java【小考】

    课上, 老师出了一个题: 考察:1.类的定义 2.类的属性 3.类的方法.重载.构造方法.代码块 题目是这样的: 设计 一个 类:Tree 要求: 1.包含main方法 2.属性:静态: String ...

  9. WebRequest 访问 https

    参考代码: 1: [TestMethod] 2: public void TestHttps() 3: { 4: var req =(HttpWebRequest) System.Net.WebReq ...

  10. for循环的省略

    for (初始化语句; 条件语句; 控制语句){ 被执行的代码块 }; 例如: for (var i=0;i<cars.length;i++) { document.write(cars[i] ...