博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
理解AngularJS生命周期:利用ng-repeat动态解析自定义directive
阅读量:6680 次
发布时间:2019-06-25

本文共 11562 字,大约阅读时间需要 38 分钟。

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:

然而这样的用法有一个缺陷:当表单中含有其他类型的组件时,比如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: ''                + '
' + '
' + '
' + '
{
{hints}}
' + '
', 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: '' + '
' + '
' + '
' + '
' + '
' + '
{
{hints}}
' + '
', 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: '' + '
' + '
' + '
' + '
' + '
' + '
{
{hints}}
' + '
', 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模块。

 

 

三. 利用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:

最终的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

原文: 

转载于:https://www.cnblogs.com/front-end-ralph/p/5078786.html

你可能感兴趣的文章
我封装的全文检索之solr篇
查看>>
NFC的第一次接触
查看>>
RHEL7 Connection closed by foreign host.
查看>>
Nodejs开发框架之Loopback介绍
查看>>
微信小程序下拉刷新使用整理
查看>>
ubuntu12.04禁用客人会话
查看>>
我的友情链接
查看>>
JVM垃圾收集器与内存分配策略
查看>>
分析Linux 文件系统访问控制列表
查看>>
Confluence WIKI 安装、破解及添加汉化包(Windows)
查看>>
一起入门Citrix_XenDesktop7系列 二-- 跟着图片通过XenDesktop7交付(发布)应用与共享桌面...
查看>>
MyBatis学习手记(一)MaBatis入门
查看>>
SCTF-2014 writeup(部分)
查看>>
Elasticsearch 连接查询
查看>>
Retrofit入门
查看>>
设置Exchange 通讯组接收外部组织邮件
查看>>
观点:正在消逝的运维江湖
查看>>
istio 监控,遥测 (理论)
查看>>
Oracle insert 多条记录
查看>>
Python学习-baseNo.2
查看>>