angular 自定义指令详解
一:指令的创建
创建module:
var module1 = angular.module('module1',[]); angular.bootstrap(document.body,['module1']);
创建controller:
var module1 = angular.module('module1',[]); module1.controller('ctl1', function($scope) { $scope.content = 'i\'m, module 1'; $scope.name = 'module1'; $scope.save = function() { console.log('is function save'); }; }); angular.bootstrap(document.body,['module1']);
创建指令(directive):
// 衔接上面的代码 module1.directive('testDirective', function() { // 将对象return出去 return{ restrict: 'E',// 指令类型 E:element A:attribute M:comment C: class template: '<div>我是指令生成的内容</div>'; replace: true, //使用模板替换原始标记 指令内原本的数据将被清空 } }); angular.bootstrap(document.body,['module1']);
html引入指令:
<body> <div ng-controller="ctl1">{{content}} <test-directive>这是原本的内容</test-directive> </div> </body>
以上代码需要注意一下几点:
1.我们定义的指令名称是testDirective,但是在html中要写成test-directive。
2.我们把指令里面的代码都放在了function中的return里面,其实return出去的内容就是整个指令对象。
3.angular.bootstrap(document.body,['module1']);相当于我们在html中使用ng-app指令。推荐使用bootstarp而不是ng-app;
二、指令的属性
指令的属性如下:
- name
- priority
- terminal
- restrict
- template
- templateUrl
- replace
- transclude
- scope
- controller
- link
- controllerAs
- require
- compile
name:就是指令名,对应上面代码中的testDirective
priority:多个指令设置在同一个元素上的执行优先级,执行顺序从低至高:1>2>3.priority的值为正整数,比如priority: 1
terminal: true/false 如果为true,同一个元素上的其他指令的优先级高于本指令,其他指令将停止执行
restrict:ngular内置的一些指令,比如ng-app,ng-click之类的,都是以html元素的属性(atrrbile)的形式来实现的,我在使用ionic框架的时候,里面有很多自定义的标签、比如:<ion-content>
</ion-content>。这也是一个指令,他是通过html元素(element)来实现的。除了这两个之外,指令还支持class(html标签的class属性)、commment(html中的注释)来实现。
在JS代码中,restrict可以有以下赋值:
restrict: 'AE',// 指令类型 E:element A:attribute M:comment C: class
可以是多个restrict: 'AEMC',也可以是一个restrict: 'E'。在html中对应的写法为:
字母 | 声明风格 | 事例 |
E | 元素 | <my-memu title="products"></my-memu> |
A | 属性 | <div my-memu="products"></div> |
C | 样式 | <div class="my-memu:products"></div> |
M | 注释 | <!-- directive my-memu products --> |
template:
同一个指令中只能template和templateUrl只能选其一。
template为模板内容。即你要在指令所在的容器中插入的html代码。
template属性接收一个字符串,类似这样: template: '<div>我是指令生成的内容</div>';
templateUrl:
为从指定地址获取模板内容。即你要在指令所在的容器中插入的一个.html文件。
有了templateUrl我们就可以将想实现的内容写成一个单独的html模版,在需要的地方插入
angular.module('module1').run(['$templateCache', function ($templateCache) { $templateCache.put('test.html', '<div>This is templateUrl</div>'); }]); angular.module('module1').directive("testDirective", ["$parse", "$http", function ($parse, $http) { return { restrict: "E", templateUrl: "test.html" }; }]);
replace:
是否用模板替换当前元素。true : 将指令标签替换成temple中定义的内容,页面上不会再有<my-directive>标签;false :则append(追加)在当前元素上,即模板的内容包在<my-directive>标签内部。默认false。
var app = angular.module("app", []) .directive("hello", function () { var option = { restrict: "AECM", template: "<h3>Hello, Directive</h3>", replace: true //这里replace为true,所以原来的内容会被template代替 }; return option; })
<html> <head></head> <body> <hello>我是原来的内容</hello><!--消失--> ===> 变成<h3>Hello, Directive</h3> 如果replace为false ===><hello><h3>Hello, Directive</h3></hello> </body> </html>
transculde:
是否使用ng-transculde来包含html中指令包含的原有的内容,接收两个参数true/false
var app = angular.module("app", []) .directive("hello", function () { var option = { restrict: "AECM", template: "<h3>Hello, Directive</h3><span ng-transclude></span>", transculde: true //这里transculde为true,所以原来的内容会被放在有ng-transclude属性的标签内 }; return option; })
<html> <head></head> <body> <hello>我是原来的内容</hello> ===> 变成<hello><h3>Hello, Directive</h3><span ng-transclude>我是原来的内容</span></hello> </body> </html>
scope:
directive 默认能共享父 scope 中定义的属性,例如在模版中直接使用父 scope 中的对象和属性。通常使用这种直接共享的方式可以实现一些简单的 directive 功能。
但是,当你要创建一个可以重复使用的directive的时候,就不能依赖于父scope了,因为在不同的地方使用directive对应的父scope不一样。
所以你需要一个隔离的scope,我们可以向下面这样来定义我们的scope。
module1.directive("testDirective", function () { return { scope: { "":"@", "":"&", "":"=" }, template: 'Say:{{value}}' } });
这样就很方便的将我们directive的上下文scope给定义出来了,但是,如果我想将父scope中的属性传递给directive的scope怎么办呢?
directive 在使用隔离 scope 的时候,提供了三种方法同隔离之外的地方交互:
- @ 指令中定一个变量用来获取父scope中的值,是单项绑定的,指令中的值改变,父scope中的值不变
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Data Value: {{local}}</p> <p>Data Value: {{secondLocal}}</p> </div> </script> <script type="text/javascript"> angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function() { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: { local: "@nameprop", secondLocal:"@secondNameprop" } } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam" }; }); </script>
<body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> // 这里改变下面会跟着改变 </div> <div class="panel-body" scope-demo nameprop="{{data.name}}"></div> // 跟着上面数值动态变化 <div class="panel-body" scope-demo second-nameprop="{{data.name}}"></div> // 跟着上面数值动态变化 </div> </body>
指令中的local通过nameprop获得外部属性data.name,所以在控制器中的data.name变化时,指令中的local也会跟着改变;(但如果是只改变local的值,外界的data.name是不会变的);
- & 调用父级作用域中的方法(function);
<script type="text/ng-template" id="scopeTemplate"> <div class="panel-body"> <p>Name: {{local}}, City: {{cityFn()}}</p> </div> </script> <script type="text/javascript"> angular.module("exampleApp", []) .directive("scopeDemo", function () { return { template: function () { return angular.element( document.querySelector("#scopeTemplate")).html(); }, scope: { local: "=nameprop", cityFn: "&city" } } }) .controller("scopeCtrl", function ($scope) { $scope.data = { name: "Adam", defaultCity: "London" }; $scope.getCity = function (name) { return name == "Adam" ? $scope.data.defaultCity : "Unknown"; } }); </script>
<body ng-controller="scopeCtrl"> <div class="panel panel-default"> <div class="panel-body"> Direct Binding: <input ng-model="data.name" /> </div> <div class="panel-body" scope-demo city="getCity(data.name)" nameprop="data.name"></div> </div> </body>
- = 指令中定一个变量用来获取父scope中的值,是双项绑定的,指令中的值改变,父scope中的值不变
这里所有指令作用域跟外界交互都是通过属性值传入的:<div class="panel-body" scope-demo city="getCity(data.name)"nameprop="data.name"></div>
controller link:
controller: function ($scope, $element, $attrs) {}三个参数
link: function (scope, element, attrs, controller) {}四个参数
第一次尝试:
<custom-directive></custom-directive>
angular .module('app',[]) .directive('customDirective', customDirective); function customDirective() { var directive = { restrict: 'EA', template: '<div>{{vm.test}}</div>', link: function(){}, controller: directiveController }; return directive; } function directiveController() { var vm = this; vm.test = "I'm from Controller"; }
第二次尝试:
angular .module('app',[]) .directive('customDirective', customDirective); function customDirective() { var directive = { restrict: 'EA', template: '<div>{{test}}</div>', link: directiveLink }; return directive; } function directiveLink(scope,elem,attr) { scope.test = "I'm from Link"; }
到这里,我们不仅要开始思索:指令中的controller和link都可以实现同样的效果,那在指令中放这两个属性干嘛?我们的代码到底是放在controller还是link中?
我们首先来看看当二者一起使用时,呈现结果的顺序即在编译前后生成的顺序。
angular .module('app',[]) .directive('customDirective', customDirective); function customDirective() { var directive = { restrict: 'EA', template: '<div>xpy0928{{test}}</div>', link: directiveLink, controller:directiveController }; return directive; } function directiveController($scope){ $scope.test = " from contrller cnblogs"; } function directiveLink(scope,elem,attr) { scope.test = scope.test + ",and from link cnblogs"; }
我们由此得出结论:编译之前执行控制器(controller),编译之后执行链接(link)。
但是我们还未从根本上解决问题,在controller和link应该放哪些代码?我们接下来再看一个例子:
var app = angular.module('app',[]); app.directive('customDirective', customDirective); function customDirective() { var directive = { restrict: 'EA', template: '<child-directive><child-directive>', controller: function($scope, $element) { $element.find('span').text('hello cnblogs!'); } }; return directive; } app.directive("childDirective",childDirective); function childDirective() { var directive = { restrict: 'EA', template: '<h1>hello xpy0928</h1>', replace: true, link: function($scope, $element, attr) { $element.replaceWith(angular.element('<span>' + $element.text() + '</span>')); } } return directive; }
此时结果应该还是hello xpy0928还是hello cnblogs呢?我们看下结果
我们再来将如上进行修改看看:
var app = angular.module('app',[]); app.directive('customDirective', customDirective); function customDirective() { var directive = { restrict: 'EA', template: '<child-directive><child-directive>', link: function(scope, el) { el.find('span').text('hello cnblogs!'); } }; return directive; } app.directive("childDirective",childDirective); function childDirective() { var directive = { restrict: 'EA', template: '<h1>hello xpy0928</h1>', replace: true, link: function($scope, $element, attr) { $element.replaceWith(angular.element('<span>' + $element.text() + '</span>')); } } return directive; }
为什么会出现如此情况?因为在controller函数中此时所有child-directive指令中的link函数还未运行所以此时替换无效
由此我们可以基本得出在controller和link中应该写什么代码的结论:
(1)在controller写业务逻辑(我们明白业务逻辑大部分是放在服务中),这里所说的业务逻辑乃是为呈现视图之前而准备的数据或者是与其他指令进行数据交互而暴露这个api。
(2)在link中主要操作DOM。
controllerAs:
<div ng-app="myApp"> <div ng-controller="firstController"> <div book-list></div> </div> </div>
angular.module('myApp',[]) .directive('bookList',function(){ return { restrict:'ECAM', //此处定义了该指令的controller属性 controller:function($scope){ $scope.books=[ {name:'php'}, {name:'javascript'}, {name:'java'} ]; this.addBook=function(){ //或者 scope.addBook=... alert('test'); } }, controllerAs:'bookListController', //给当前controller起个名称 template:'<ul><li ng-repeat="book in books">{{ book.name }}</li></ul>', replace:true, //link中注入 bookListController ,就可以使用它的方法了 link:function(scope,iElement,iAttrs,bookListController){ iElement.on('click',bookListController.addBook); } } }) .controller('firstController',['$scope',function($scope){ }])
require:
谈require选项之前,应该先说说controller选项,controller选项允许指令对其他指令提供一个类似接口的功能,只要别的指令(甚至是自己)有需要,就可以获取该controller,将其作为一个对象,并取得其中的所有内容。而require就是连接两个指令的锁链,它可以选择性地获取指令中已经定义好的controller,并作为link函数的第四个参数传递进去,link函数的四个参数分别为scope,element,attr和someCtrl,最后一个就是通过require获取的controller的名字,对于controller的名字,可以在指令中用controllerAs选项进行定义,这是发布控制器的关键.
具体如何获取controller呢?require选项的值可以分别用前缀?、^ 和?^进行修饰,也可以不修饰。
如果不进行修饰,比如require:'thisDirective',那么require只会在当前指令中查找控制器
如果想要指向上游的指令,那么就是用^进行修饰,比如require:'^parentDirective',如果没有找到,那就会抛出一个错误。
如果使用?前缀,就意味着如果在当前指令没有找到控制器,就将null作为link的第四个参数;
那么,如果将?和^结合起来,我们就可以既指定上游指令,又可以在找不到时,不抛出严重的错误。
现在问题来了,如果我想指定多于一个指令,那怎么办呢?这时,我们可以将需要的指令放进一个数组中,例如:require:['^?firstDirective','^?secondDirective','thisDirective'],这不正是依赖注入的形式吗?但要注意一点,如果使用这种写法的话,原先指令中发布的控制器的名字,即controllerAs选项,就失去意义了。此时,我们在link中调用这些指令的controller的方法变为:先为上边的数组定义一个名字,接着根据其下标号来指定具体的某个指令。类似于:ctrlList[0]。
假如我们现在需要编写两 个指令,在linking函数中有很多重合的方法,为了避免重复自己(著名的DRY原则),我们可以将这个重复的方法写在第三个指令的 controller中,然后在另外两个需要的指令中require这个拥有controller字段的指令,最后通过linking函数的第四个参数就可以引用这些重合的方法。代码的结构大致如下:
var app = angular.modeule('myapp',[]); app.directive('common',function(){ return { ... controller: function($scope){ this.method1 = function(){ }; this.method2 = function(){ }; }, ... } }); app.directive('d1',function(){ return { ... require: '?^common', link: function(scope,elem,attrs,common){ scope.method1 = common.method1; .. }, ... } });