上一章节,给大家回顾了一下AngularJS指令参数的基础使用。如果有纰漏,欢迎大家给我留言,相互探讨探讨。
指令化,其实本质就是代码的通用化与模块化,AngularJS的指令化工作,将逻辑与DOM都结合在一起,能够做到即插即用,与Asp的Component是相似的概念。
要做到模块化,必要的要求就是通用代码与业务代码的解耦。而解耦并不代表完全的隔绝,解耦要做的是,通用模块与业务模块的隔离,同时也保留接口提供两者通讯。
啰嗦,赶紧实例吧!
某天,产品老大压下来需求,要做个学生信息填写卡,说白了就是一个表单编辑器,so easy,前端单身狗拍拍脑袋马上开工。
<body>
<div ng-controller='demoCtrl'>
<div class="panel">
<h3>student card</h3>
<p>
<span>name:</span>
<input type="text" ng-model="stu.name" />
</p>
<p>
<span>sexy :</span>
<select ng-model="stu.sexy">
<option value="1">male</option>
<option value="2">female</option>
</select>
</p>
<p>......</p>
<button class="btn btn-info" type="button" ng-click="saveEditing()">save</button>
</div>
</div>
<script>
var app = angular.module("app", []);
app.controller('demoCtrl', function ($scope) {
$scope.stu = {
name: "mark",
sexy: 1
};
$scope.saveEditing = function () {
//$http.post("...", { stu: $scope.guy });
console.log($scope.guy.name + " is saving!");
console.log($scope.guy);
};
//这里省略信息卡交互逻辑……
});
</script>
</body>
出来结果,还行。
隔了一天,产品老大想了想,要增强体验,在选择性别的时候,名称要根据男女颜色变化;输入名称的时候,要自动匹配近似名称,并且首字母大写。行,改呗。demoCtrl
加上以下逻辑
$scope.nameChange = function () {
//处理名称变更
};
$scope.sexyChange = function () {
//处理性别变更
};
又隔了一天,产品老大终于定稿,这个学生信息填写卡,要应用到demo页、demo1页、demo2页上,而3个页面保存功能指向的后端接口都不一样。
前端单身狗:虽然不至于问候您大爷,但至少要好好考虑如何实现才省功夫吧。难道要把demoCtrl的相关代码都copy到另外两个页面吗?这个时候,只要是写过代码的同志都会say no吧!
赶紧指令化。
隔离
首先,把信息卡的DOM结构独立开来,创建指令student
。指令scope参数要设为{}
,如果设为false
、true
达不到完全隔离的效果哦,不理解原因的童鞋请回顾上一章节。
<div ng-controller='demoCtrl'>
<student></student>
</div>
<div ng-controller='demo1Ctrl'>
<student></student>
</div>
<div ng-controller='demo2Ctrl'>
<student></student>
</div>
<!--信息卡DOM模板->
<script type="text/html" id="t1">
<div class="panel">
<p>
<span>name:</span>
<input type="text" ng-model="stu.name" />
</p>
<p>
<span>sexy :</span>
<select ng-model="stu.sexy">
<option value="1">male</option>
<option value="2">female</option>
</select>
</p>
<button class="btn btn-info" type="button" ng-click="onSave()">save</button>
</div>
</script>
<script>
var app = angular.module("app", []);
app.directive('student', function () {
return {
restrict: 'E',
scope: {},
template: function (elem, attr) {
return document.getElementById('t1').innerHTML;
},
controller:function($scope){
//这里省略信息卡交互逻辑……
}
};
});
</script>
虽然独立了信息卡代码,但有两个问题是显而易见的
隔离以后,指令内部如何获得3个demoCtrl的stu?
保存按钮又是如何调用外部3个demoCtrl不同的save方法?
问题其实指明了解决思路,student需要两个接口与外部通讯:scope.stu
& scope.save()
通讯
为大家介绍3种通讯方案。
scope {}
指令scope {}参数,可以完全隔离作用域,但是也预留了3种绑定策略,实现子域与父域通讯。
为指令建立两个通讯接口,stu
采取=
双向绑定父域对象的策略;而onSave
则采取$
反向调用父域函数策略。
scope: {
stu: '=', //=为双向绑定策略
onSave: '&' //$反向调用父域函数策略
},
优点:简单、简洁
缺点:
通讯接口要求比较多、复杂的情况下,指令scope {}要配置的绑定策略也比较多;
造成指令与指令之间的通讯容易混乱;
指令内部好像没有办法,往
onSave()
函数里面传参,这个不确定,求助大家。
DOM写法:
<student stu="guy" on-save="saveEditing()"></student>
完整例子:
<body>
<div ng-controller='demoCtrl'>
<!--指令scope.stu双向绑定demoCtrl scope.guy-->
<!--指令scope.onSave函数指向demoCtrl scope.saveEditing()-->
<student stu="guy" on-save="saveEditing()"></student>
</div>
<script type="text/html" id="t1">
<div class="panel">
<p>
<span>name:</span>
<input type="text" ng-model="stu.name" />
</p>
<p>
<span>sexy :</span>
<select ng-model="stu.sexy">
<option value="1">male</option>
<option value="2">female</option>
</select>
</p>
<button class="btn btn-info" type="button" ng-click="onSave()">save</button>
</div>
</script>
<script>
var app = angular.module("app", []);
app.controller('demoCtrl', function ($scope) {
$scope.guy = {
name: "mark",
sexy: 1
};
$scope.saveEditing = function () {
//$http.post("...", { stu: $scope.guy });
console.log($scope.guy.name + " is saving!");
console.log($scope.guy);
};
});
app.directive('student', function () {
return {
restrict: 'E',
scope: {
stu: '=', //=为双向绑定策略
onSave: '&' //$反向调用父域函数策略
},
template: function (elem, attr) {
return document.getElementById('t1').innerHTML;
},
controller: function ($scope, $element, $attrs, $transclude) {
//这里省略信息卡交互逻辑……
}
};
});
</script>
</body>
效果:
ngModel
ngModel,这个内置指令相信大家都不会陌生。自定义指令引用其自身的ngModel指令,其原理就是:
父域对象 绑定 ngModel
ngModel 绑定 指令子域对象
优点:可以充分利用ngModel的特性,例如commit、rollback等特性,也可以搭配ng-model-options
进行使用。
缺点:
相对于方案1,父子对象绑定中间还要多一层ngModel的绑定,性能必然降低;
相对于方案1,使用比较麻烦;
自身不能实现反向调用父域函数,需要借助$parse转换表达式方案实现
DOM写法:
<student ng-model="guy" on-save="saveEditing()"></student>
完整例子:
<body>
<div ng-controller='demoCtrl'>
<student ng-model="guy" on-save="saveEditing()"></student>
</div>
<script type="text/html" id="t1">
<div class="panel">
<p>
<span>name:</span>
<input type="text" ng-model="ngModel.$modelValue.name" />
</p>
<p>
<span>sexy :</span>
<select ng-model="ngModel.$modelValue.sexy">
<option value="1">male</option>
<option value="2">female</option>
</select>
</p>
<button class="btn btn-info" type="button" ng-click="onSave()">save</button>
</div>
</script>
<script>
var app = angular.module("app", []);
app.controller('demoCtrl', function ($scope) {
$scope.guy = {
name: "mark",
sexy: 1
};
$scope.saveEditing = function () {
//$http.post("...", { stu: $scope.guy });
console.log($scope.guy.name + " is saving!");
console.log($scope.guy);
};
});
app.directive('student', ["$parse", function ($parse) {
return {
restrict: 'E',
require: ['student', 'ngModel'],
scope: {},
template: function (elem, attr) {
return document.getElementById('t1').innerHTML;
},
link: function (scope, element, attr, ctrls) {
//link阶段,通过require获取指令自身的控制器,及ngModel指令的控制器
var stCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
//并将ngModel指令的控制器,通过自身控制器的init()方法传入到其中
stCtrl.init(ngModelCtrl);
},
controller: function ($scope, $element, $attrs, $transclude) {
//创建controller的对外初始化方法,并将外部ngModel的控制器设置本地作用域对象
this.init = function (ngModelCtrl) {
$scope.ngModel = ngModelCtrl;
};
//获得on-save属性指向的表达式{{saveEditing()}}
var saveInvoker = $parse($attrs.onSave);
$scope.onSave = function () {
//在父域中,执行表达式{{saveEditing()}}——执行父域saveEditing()
saveInvoker($scope.$parent, null);
};
}
};
}]);
</script>
</body>
$parse
在ngModel的解决方案中,已经有过通过$parse获取执行表达式操作父域函数的例子:$parse($attrs.onSave)
。这个方案不仅能够执行父域函数表达式,同时也能够执行对象的get/set表达式。但是本方案本质其实是对于父域$scope.$parent
的直接操作,只是通过$parse服务实现解耦。
关于$parse服务,想了解更多,请移步——
http://segmentfault.com/a/1190000002749571
<body>
<div ng-controller='demoCtrl'>
<student stu="guy" on-save="saveEditing()"></student>
</div>
<script type="text/html" id="t1">
<div class="panel">
<p>
<span>name:</span>
<input type="text" ng-model="stu.name" />
</p>
<p>
<span>sexy :</span>
<select ng-model="stu.sexy">
<option value="1">male</option>
<option value="2">female</option>
</select>
</p>
<button class="btn btn-info" type="button" ng-click="onSave()">save</button>
</div>
</script>
<script>
var app = angular.module("app", []);
app.controller('demoCtrl', function ($scope) {
$scope.guy = {
name: "mark",
sexy: 1
};
$scope.saveEditing = function () {
//$http.post("...", { stu: $scope.guy });
console.log($scope.guy.name + " is saving!");
console.log($scope.guy);
};
});
app.directive('student', ["$parse", function ($parse) {
return {
restrict: 'E',
scope: {},
template: function (elem, attr) {
return document.getElementById('t1').innerHTML;
},
controller: function ($scope, $element, $attrs, $transclude) {
var getClassName,
setClassName,
saveInvoker = angular.noop;
//注意:建议查阅一下$parse内置表达式转换函数的使用方法。
//获得stu属性指向的表达式{{guy}}
getStudent = $parse($attrs.stu);
setStudent = getStudent.assign;
//获得on-save属性指向的表达式{{saveEditing()}}
saveInvoker = $parse($attrs.onSave);
//监听父域的{{guy}}
$scope.$parent.$watch(getStudent, function (stu) {
$scope.stu = stu;
});
$scope.onSave = function () {
//在父域中,执行表达式{{guy}} assign——将本地对象stu设置到父域guy
setStudent($scope.$parent, $scope.stu);
//在父域中,执行表达式{{saveEditing()}}——执行父域saveEditing()
saveInvoker($scope.$parent, null);
};
}
};
}]);
</script>
</body>
效果:
End
说了一大堆,大家还是动手试试哪个方案更合适你的项目,或者有更好的开发思路。
下一章节,为大家介绍一下关于指令的自动化测试。欢迎继续捧场。