Angular自定义指令Directive详解

时间:2020-02-09 17:02:31       编辑:Echeverra

“控制器应该尽可能保持短小精悍,而在控制器中进行DOM操作和数据操作则是一个不好的实践。设计良好的应用会将复杂的逻辑放到指令和服务中。通过使用指令和服务,我们可以将控制器重构成一个轻量且更易维护的形式” ----《AngularJs权威教程》

1、Angular Directive说明

Directive中文翻译为指令,从《AngularJs权威教程》中对指令的描述,我理解的指令是对控制器的补充,主要功能是对Dom元素和数据的操作(一般指令主Dom操作、服务主数据操作,非强制规定),本文基于Angular1.4.6版本进行讲解和演示。

在Angular中内置了一些指令,如常见的ng-app指令初始化一个 AngularJS 应用程序,ng-model指令把元素值(比如输入域的值)绑定到应用程序。同时Angular也支持创建自定义指令。

2、Angular Directive使用

基础案例展示。

Html代码:

<!DOCTYPE html><html ng-app="myApp"><head>
    <meta charset="utf-8">
    <script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js"></script>
    </head>
    <body ng-controller="myCtrl">
        <!-- 引入指令 -->
        <my-dir></my-dir>
    </body>
   </html>

Js代码:

angular.module('myApp', [])
    .controller("myCtrl", function($scope) {})
    .directive("myDir", function() {  
        return {  
            template: "<h1>这是自定义指令</h1>",
            restrict: "E"  
        }  
    });

在线代码演示

这是一个很简单的指令小案例,指令中的template模板内容“这是自定义指令”通过<my-dir>标签引入到页面中,涉及到指令两个很重要的参数templaterestrict,下面会具体讲解指令的参数。

3、Angular Directive参数

参数类型
restrictString
priorityNumber
terminalBoolean
templateString or Template Function
templateUrlString or Template Function
replaceBoolean or String
transcludeBoolean
scopeBoolean or Object
controllerString or function(scope, element, attrs, transclude, otherInjectables) { ... }
controllerAsString
requireString
linkfunction(scope, iElement, iAttrs) { ... }
compilefunction(tElement, tAttrs, transclude) { 
return { 
pre: function(scope, iElement, iAttrs, controller) { ... }, 
post: function(scope, iElement, iAttrs, controller) { ... } 
} 
 return function postLink(...) { ... } 
}

已上是指令的所有参数以及类型,重点介绍常用参数restrict、scope、template、templateUrl、replace、priority、link

参数1:restrict

restrict用于指定directive的使用形式。如上文例子指令代码中的restrict: "E"即指名使用Element元素形式,同时还有A、C、M共四种形式。默认值为EA

参数值形式示例
E元素 Element<my-dir></my-dir>
A属性 Attribute<div my-dir></div>
C样式 Class<div class="my-dir"></div>
M注释 Comment<!-- directive:my-dir -->

Html代码:

<body>
    <!-- 元素形式引入指令 -->
    <my-dir></my-dir>
    <!-- 属性形式引入指令 -->
    <div my-dir></div>
    <!-- 样式形式引入指令 -->
    <div class="my-dir"></div>
    <!-- 指令形式引入指令 -->
    <!-- directive: my-dir -->
</body>

Js代码:

angular.module('myApp', [])
    .controller("myCtrl", function($scope) {})
    .directive("myDir", function() {  
        return {  
            template: "<h1>这是自定义指令</h1>",
            restrict: "ECMA"
        }  
    });

在线代码演示

指令命名需使用驼峰模式,Html引入指令需使用“-”连接每个单词,如指令myDir在引入指令时改为my-dir写法。E、A、C、M四种形式可组合使用。

说明: 在线代码演示中注释引入<!-- directive: my-dir -->没有成功引入指令内容,这是由于在线codepen不支持,放到自己IDE中是没问题的。

参数2:scope

scope是指令的作用域,每个指令创建后可以继承父作用域(外部controller提供的作用域或根作用域$rootScope),也可以拥有自己独立的作用域,scope参数有三种,分别是:false、true、{},默认为false

scope = false

scope设置为false时,指令模板中可以直接使用父作用域中得变量和函数,举个例子:

Html代码:

<div ng-app="MyApp">
    <div class="container" ng-controller="MyController">
        <div class="my-info">我的名字是:<span ng-bind="name"></span>
        <!-- 使用"ng-bind"防止网络状态不佳时出现没有被赋值表达式   -->
            <br/>
            我的年龄是:<span ng-bind="age"></span>
        </div>
        <!-- 使用属性声明指令 -->
        <div class="my-dir" my-dir></div>
    </div></div>

Js代码:

angular.module("MyApp", [])
    .controller("MyController", function ($scope) {        
    // 这里我们在作用域里初始化两个变量
    $scope.name = "echeverra";
    $scope.age = 20;        
    // 创建一个方法,修改我们创建的对象的年龄
    $scope.changeAge = function () {
        $scope.age = 22;
    }
})    
    // 创建我们的指令,指令名字为"myDir"
    .directive("myDir", function () {    
    var obj = {        
        // 指令的声明模式为 "AE" 属性和元素
        restrict: "AE",        
        // 指令继承父作用域的属性和方法
        scope: false,
        replace: true,
        template: "<div class='my-directive'>" +            
            "<h3>下面部分是我们创建的指令生成的</h3>" +            
            "我的名字是:<span ng-bind='name'></span><br/>" +            
            "我的年龄是:<span ng-bind='age'></span>" +            
            "<input type='text' ng-model='name'>"+            
            "</div>"
    }    return obj;
});

在线代码演示

从在线代码演示效果中我们看到创建的指令继承了父作用域controller中的属性name、age,同样也继承了方法changeAge。当改变指令模块中inputname会发现父作用域controller中的name也会发生变化,这说明指令的作用域和父作用域在同一个作用域下。

scope = true

scope参数改为true时,我们再来看:

在线代码演示

同样指令中继承了父作用域的属性和方法,不同的是,当修改指令inputname时,指令的name对应改变,父作用域controller中的name并未发生改变,这说明指令的作用域和父作用域不在同一个作用域下。

scope = {}

scope设置为{}会使指令的作用域变得更加灵活,修改之前的代码:

Html代码:

<div ng-app="MyApp">
    <div class="container" ng-controller="MyController">
        <div class="my-info">
        我的名字是:<span ng-bind="name"></span>
            <br/>我的年龄是:<span ng-bind="age"></span>
            <br/>
        </div>
        <div class="my-dir" my-dir my-name="{{name}}" age="age"  change-my-age="changeAge()"></div>
    </div>
</div>

Js代码:

angular.module("MyApp", [])
    .controller("MyController", function ($scope) {
    $scope.name = "echeverra";
    $scope.age = 20;
    $scope.male = 'man';
    $scope.changeAge = function(){
        $scope.age = 0;
    }
})
    .directive("myDir", function () {    var obj = {
        restrict: "AE",
        scope: {
            name: '@myName',
            age: '=',
            changeAge: '&changeMyAge'
        },
        replace: true,
        template: "<div class='my-dir'>" +            
            "<h3>下面部分是我们创建的指令生成的</h3>" +            
            "我的名字是:<span ng-bind='name'></span><br/>" +            
            "我的性别是:<span ng-bind='male'></span><br/>" +            
            "我的年龄是:<span ng-bind='age'></span><br/>" +            
            "在这里修改名字:<input type='text' ng-model='name'><br/>" +            
            "<button ng-click='changeAge()'>修改年龄</button>" +            
            "</div>"
    }    return obj;
});

在线代码演示

我们使用了隔离的作用域,不代表我们不可以使用父作用域的属性和方法,我们可以通过向scope{}中传入特殊的前缀标识符(即prefix),来进行数据绑定。

我们可以通过前缀标识符@, &, =应用到指令中的属性,如:{name: '@myName', age: '=', changeAge: '&changeMyAge'}age: '='age: '=age'的简写),我们可以在<div class="my-directive" my-directive my-name="{{name}}" age="age" change-my-age="changeAge()"></div>这个元素中,通过使用属性my-name,age,change-my-age来引用这些属性的值。

首先可以确定的是scope={}不会继承父作用域,因为我的性别项male值是空的,在scope中未进行属性male绑定。

那么前缀标识符@, &, =分别有什么作用呢?

@

这是一个单向数据绑定的前缀标识符。在元素中使用属性,如:<div my-dir my-name="{{name}}"></div>,属性的名字要用“-”将两个单词连接,因为是数据的单向绑定所以要通过使用{{}}来绑定数据。在线代码演示结果中修改inputname值可看出,只指令的作用域生效name发生改变,父作用域name未改变,这点和scope参数为true效果是一样的。

=

这是一个双向数据绑定的前缀标识符。在元素中使用属性,如:<div my-dir age="age"></div>,因为是数据的双向绑定,所以使用=前缀实现,而不是{{}}。在线代码演示结果中修改inputname值可看出,指令的作用域和父作用域name均发生改变,这点和scope参数为false效果是一样的。

&

这是一个绑定函数方法的前缀标识符。在元素中使用属性,如:<div my-dir change-my-age="changeAge()"></div>,属性的名字要用“-”将每个单词连接。在线代码演示结果中点击“修改年龄”按钮,指令作用域和父作用域中的年龄都发生改变,这点和前缀标识符=效果相同。

注意: 在新创建指令的作用域对象中,使用属性的名字进行绑定时,要使用驼峰命名标准,如下面的代码:

scope: {            
            // `myName` 就是原来元素中的`my-name`属性
            name: '@myName', 
            age: '=',            
            // `changeMyAge`就是原来元素中的`change-my-age`属性
            changeAge: '&changeMyAge' 
        }

进一步说明,我们创建的指令是如何利用这些前缀标识符来寻找我们想要的属性或函数的呢?

使用@时当指令编译到模板的name,就会到scope中查找是否含有name的键值对(name:'@myName'),找到后发现前缀标识符@就知道这是一个单向数据绑定标识符,之后会去寻找元素上含有这个值的属性(my-name={{name}}),然后在父作用域中查找{{name}}的值,得到之后传递给属性my-name,通过指令参数scope中键值对name:'@myName'最后传给了模板中的name。(此处比较绕,不过真心值得研究一番,有助于深刻理解)。=&@差不多,只不过=进行的是双向的数据绑定,不论模板还是父作用域上的属性的值发生改变都会使另一个值发生改变,而&是绑定函数而已。

总结: scope = falsescope = true初始化都会继承父作用域中的属性和方法,不同的是scope = false和父作用域在同一作用域下,scope = true和父作用域不在同一作用域,而是创建了一个独立的作用域。 scope = {}不会继承父作用域,属性控制更加灵活,单向绑定@,外部scope能够影响内部scope,但反过来不成立,双向绑定=,外部scope和内部scopemodel能够相互改变,函数绑定&把内部scope的函数和外部scope的函数绑定起来。

参数3:template

template参数值可以是一个字符串也可以是一个函数,值为字符串比较好理解,上文一直在演示的就是值为字符串的形式。当值为函数时,可接收两个参数elementattrs。举例:

Html代码:

<!-- 引入指令 -->
<my-dir name="echeverra" age="20" title="指令元素"></my-dir>

Js代码:

angular.module('myApp', [])
    .controller("myCtrl", function($scope) {})
    .directive("myDir", function() {  
        return {  
            template: function(element, attrs) {              
            return '<h2>name:'+attrs.name+'</h2>'+                   
                '<h2>age:'+attrs.age+'</h2>'+                   
                '<h2>title:'+attrs.title+'</h2>'
            },
            restrict: "E"  
        }  
    });

在线代码演示

函数参数element是指使用此指令的元素<my-dir name="echeverra" age="20" title="指令元素"></my-dir>,而attrs是指令元素上所有属性的集合,形如:

{  
    name: 'echeverra',
    age: '20',
    title: '指令元素'
}

参数4:templateUrl

templateUrl和上一个参数template用法基本相同,只不过template值为字符串是一段html模板,templateUrl值为字符串是文件路径。如:

angular.module('myApp', [])
    .controller("myCtrl", function($scope) {})
    .directive("myDir", function() {  
        return {  
            templateUrl: 'index.html';
        }  
    });

具体使用请参考参数template

参数5:replace

replace是一个可选参数,默认值为false,表示指令中模板template内容会当做子元素插入到指令元素中,如果设置为true,表示指令中模板会直接替换掉指令元素。

举例replacefalse:

Html代码:

<div id="dir">
    <!-- 引入指令 -->
    <my-dir></my-dir>
</div>
<button ng-click='getDirCont()'>获取引入指令元素</button>
<div ng-bind="dirCont"></div>

Js代码:

angular.module('myApp', [])

        .controller("myCtrl", function($scope) {
            $scope.getDirCont = function() {
                $scope.dirCont = document.getElementById('dir').innerHTML;
            }
        })
        .directive("myDir", function() {            
            return {
                template:"<h1>这是自定义指令</h1>",
                restrict: "E",
                replace: false,
            }
        });

在线代码演示

点击获取引入指令元素按钮后显示<my-dir><h1>这是自定义指令</h1></my-dir>,可以看到指令元素<my-dir></my-dir>还在,如果将replace设置为true,我们在来看:

在线代码演示

这次获取只显示元素<h1>这是自定义指令</h1>,说明指令元素<my-dir></my-dir>被替换掉了,这就是两者的区别。

参数6:priority

当同一个元素声明两个指令,需要设定执行先后顺序,这时就需要用到参数priority,值为数字,默认值为0,我们来看下不使用priority的情况会有什么麻烦。

Html代码:

<!-- 引入指令 -->
<div ng-init="greeting='Hello '" d1 d2>{{greeting}}!</div>

Js代码:

angular.module('myApp', [])
        .controller("myCtrl", function($scope) {})
        .directive("d1", function() {            
            return {
                link: function (scope) {
                    scope.greeting += 'World ';
                }
            }
        })
        .directive("d2", function() {            
            return {
                link: function (scope) {
                    scope.greeting += 'Angular ';
                }
            }
        });

在线代码演示

输出:Hello Angular World !,可见先执行了ng-init="greeting='Hello '",然后执行了指令d2,最后是d1。实际我们想预期输出的是Hello World Angular !,我们将指令元素中的d1、d2顺序对调,再将指令中创建的d1、d2两个指令顺序对调,再试发现输出的还是Hello Angular World !,这是因为Angular是使用字母顺序来确定链接函数谁先被调用,将d1改为e1就会得到预期的结果,有兴趣的可以尝试一下。如何设定指令执行优先级?这时候priority就派上用场了。

Js代码:

angular.module('myApp', [])
        .controller("myCtrl", function($scope) {})
        .directive("d1", function() {            
            return {
                priority: 1,
                link: function (scope) {
                    scope.greeting += 'World ';
                }
            }
        })
        .directive("d2", function() {            
            return {
              priority: 2,
                link: function (scope) {
                    scope.greeting += 'Angular ';
                }
            }
        });

在线代码演示

输出:Hello World Angular

参数7:link

link函数主要用于操作Dom元素,给Dom元素绑定事件和监听,link参数要求声明一个函数,称之为链式函数。

写法:

link: function(scope, element, attrs) {  
    // 在这里操作DOM
}
  • scope:指令所在的作用域

  • element:指令元素的封装,可以调用angular封装的简装jq方法和属性

  • attr:指令元素的属性的集合

如果指令使用了require选项,那么链接函数会有第四个参数,代表控制器或者所依赖的指令的控制器。

require 'SomeController',
link: function(scope, element, attrs, SomeController) {  
    // 在这里操作DOM,可以访问require指定的控制器
}

下面举例操作Dom元素,改变元素样式。

Html代码:

<!-- 引入指令 -->
<input ng-model="color" placeholder="请输入颜色值"/>
<br/>
<my-dir name="my-dir-name"></my-dir>

Js代码:

angular.module('myApp', [])
        .controller("myCtrl", function($scope) {})
        .directive("myDir", function() {            
            return {
                template:'<h3 style="background-color:{{color}}">code</h3>',
                replace:true,
                link:function(scope, ele, attrs, ctrl) {
                    ele.bind('click', function () {
                        scope.$apply(function () {
                            scope.color = 'red';
                            ele.text(attrs.name);
                        })
                    });
                    ele.bind('mouseover', function () {
                        ele.css({'cursor': 'pointer'})
                    });
                }
            }
        });

在线代码演示

点击code,获取指令元素的属性name值,ele调用text()展示,同时ele绑定bind鼠标悬浮事件,调用css()方法,改变鼠标样式。ele是指令元素的jqLite包装,所以可以基本的jq方法,如例子中的bind()css()等。

结语: 还有一些其他不常见的属性,本文没有介绍。如有错误,欢迎指出,感谢以下博文。