在angular中,angular.module返回一个module对象,module对象拥有directive方法,directive方法可以用来创建自定义指令,自定义指令可以使html标签语义化。

angular-directive

先看一个实例:

<html>

   <head>
      <meta charset="utf-8">
      <title>自定义指令</title>
      <script src="angular.js"></script>
   </head>

   <body ng-app='app'>
      <hello></hello>
      <script>
      var app = angular.module('app', []);
      app.directive('hello', function() {
          return {
              restrict: 'E',
              template: '<h1>Hello World!</h1>',
              replace: true
          };
      });
      </script>
   </body>
   
</html>

restrict: 'E' 用来指定hello指令只能通过元素名方式调用。当然 restrict 还有其他的可选值:

字母代表方式示例
E元素名<hello></hello>
A属性<div hello></div>
C类名<div class=“hello”></div>
M注释<!– directive:hello –>

restrict 默认值为 EA, 即可以通过元素名和属性名来调用指令。

replace: true 用来在html中替换掉调用指令的标签。

模板扩展

默认情况下 directive 直接从父 scope 中共享属性,来看一个实例:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='app'>
    <div ng-controller="Controller">
        <div my-customer></div>
    </div>
    <script>
    var app = angular.module('app', []);
    app.controller('Controller', ['$scope', function($scope) {
        $scope.customer = {
            name: 'Naomi',
            address: '1600 Amphitheatre'
        };
    }]).directive('myCustomer', function() {
        return {
            template: 'Name: {{customer.name}} Address: {{customer.address}}'
        };
    });
    </script>
</body>

</html>

如果你不太喜欢把template写到javascript中,可以使用templateUrl实现相同的效果,如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='app'>
    <div ng-controller="Controller">
        <div my-customer></div>
    </div>
    <script>
    var app = angular.module('app', []);
    app.controller('Controller', ['$scope', function($scope) {
        $scope.customer = {
            name: 'Naomi',
            address: '1600 Amphitheatre'
        };
    }]).directive('myCustomer', function() {
        return {
            templateUrl: 'my-customer.html'
        };
    });
    </script>
</body>

</html>
Name: {{customer.name}} Address: {{customer.address}}

隔离指令的Scope

在上面的例子中,myCustomer指令已经可以很好的运行了,但是在给定的scope中myCustomer只能只能使用一次。为了达到多次使用指令接收不同的scope你很容易想到下面的方法:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsScopeProblemExample'>
    <div ng-controller="NaomiController">
        <my-customer></my-customer>
    </div>
    <hr>
    <div ng-controller="IgorController">
        <my-customer></my-customer>
    </div>
    <script>
    angular.module('docsScopeProblemExample', [])
        .controller('NaomiController', ['$scope', function($scope) {
            $scope.customer = {
                name: 'Naomi',
                address: '1600 Amphitheatre'
            };
        }])
        .controller('IgorController', ['$scope', function($scope) {
            $scope.customer = {
                name: 'Igor',
                address: '123 Somewhere'
            };
        }])
        .directive('myCustomer', function() {
            return {
                restrict: 'E',
                template: 'Name: {{customer.name}} Address: {{customer.address}}'
            };
        });
    </script>
</body>

</html>

假如我们需要将指令内部的scope与指令外部的scope隔离,并且可以按需映射指令外部的scope到指令内部,此时我们可以借助指令的scope属性来实现,如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsIsolateScopeDirective'>
    <div ng-controller="Controller">
        <my-customer info="naomi"></my-customer>
        <hr>
        <my-customer info="igor"></my-customer>
    </div>
    <script>
    angular.module('docsIsolateScopeDirective', [])
        .controller('Controller', ['$scope', function($scope) {
            $scope.naomi = {
                name: 'Naomi',
                address: '1600 Amphitheatre'
            };
            $scope.igor = {
                name: 'Igor',
                address: '123 Somewhere'
            };
        }])
        .directive('myCustomer', function() {
            return {
                restrict: 'E',
                scope: {
                    customerInfo: '=info'
                },
                template: 'Name: {{customerInfo.name}} Address: {{customerInfo.address}}'
            };
        });
    </script>
</body>

</html>

directive 在使用隔离 scope 的时候,提供了三种方法同隔离之外的地方交互。这三种分别是

  • @ 绑定一个局部 scope 属性到当前 dom 节点的属性值。结果总是一个字符串,因为 dom 属性是字符串。单向绑定,外部scope能够影响内部scope,但反过来不成立。
  • & 提供一种方式执行一个表达式在父 scope 的上下文中。把内部scope的函数的返回值和外部scope的任何属性绑定起来。
  • = 通过 directive 的 attr 属性的值在局部 scope 的属性和父 scope 属性名之间建立双向绑定。双向绑定,外部scope和内部scope的model能够相互改变。

在指令中操作DOM

如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsTimeDirective'>
    <div ng-controller="Controller">
        Date format:
        <input ng-model="format">
        <hr/> Current time is: <span my-current-time="format"></span>
    </div>
    <script>
    angular.module('docsTimeDirective', [])
        .controller('Controller', ['$scope', function($scope) {
            $scope.format = 'M/d/yy h:mm:ss a';
        }])
        .directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {

            function link(scope, element, attrs) {
                var format,
                    timeoutId;

                function updateTime() {
                    element.text(dateFilter(new Date(), format));
                }

                scope.$watch(attrs.myCurrentTime, function(value) {
                    format = value;
                    updateTime();
                });

                element.on('$destroy', function() {
                    $interval.cancel(timeoutId);
                });

                // start the UI update process; save the timeoutId for canceling
                timeoutId = $interval(function() {
                    updateTime(); // update DOM
                }, 1000);
            }

            return {
                link: link
            };
        }]);
    </script>
</body>

</html>

创建一个包装其他元素的指令

如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsTransclusionDirective'>
    <div ng-controller="Controller">
        <my-dialog>Check out the contents, {{name}}!</my-dialog>
    </div>
    <script>
    angular.module('docsTransclusionDirective', [])
        .controller('Controller', ['$scope', function($scope) {
            $scope.name = 'Tobias';
        }])
        .directive('myDialog', function() {
            return {
                restrict: 'E',
                transclude: true,
                scope: {},
                template: '<div class="alert" ng-transclude></div>'
            };
        });
    </script>
</body>

</html>

transclude会使指令内容访问指令外部的scope,如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsTransclusionExample'>
    <div ng-controller="Controller">
        <my-dialog>Check out the contents, {{name}}!</my-dialog>
    </div>
    <script>
    angular.module('docsTransclusionExample', [])
        .controller('Controller', ['$scope', function($scope) {
            $scope.name = 'Tobias';
        }])
        .directive('myDialog', function() {
            return {
                restrict: 'E',
                transclude: true,
                scope: {},
                template: '<div class="alert" ng-transclude></div>',
                link: function(scope) {
                    scope.name = 'Jeff';
                }
            };
        });
    </script>
</body>

</html>

现在为dialog添加一个按钮,并在指令内部实现对dialog的行为操作,如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsIsoFnBindExample'>
    <div ng-controller="Controller">
        {{message}}
        <my-dialog ng-hide="dialogIsHidden" on-close="hideDialog(message)">
            Check out the contents, {{name}}!
        </my-dialog>
    </div>
    <script>
    angular.module('docsIsoFnBindExample', [])
        .controller('Controller', ['$scope', '$timeout', function($scope, $timeout) {
            $scope.name = 'Tobias';
            $scope.message = '';
            $scope.hideDialog = function(message) {
                $scope.message = message;
                $scope.dialogIsHidden = true;
                $timeout(function() {
                    $scope.message = '';
                    $scope.dialogIsHidden = false;
                }, 2000);
            };
        }])
        .directive('myDialog', function() {
            return {
                restrict: 'E',
                transclude: true,
                scope: {
                    'close': '&onClose'
                },
                template: '<div class="alert"><a href class="close" ng-click="close({message: \'closing for now\'})">&times;</a><div ng-transclude></div></div>'
            };
        });
    </script>
</body>

</html>

scope中的&attr用来在指令中暴漏一个API绑定行为

创建指令并为其添加事件监听

如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='dragModule'>
    <span my-draggable>Drag Me</span>
    <script>
    angular.module('dragModule', [])
        .directive('myDraggable', ['$document', function($document) {
            return {
                link: function(scope, element, attr) {
                    var startX = 0,
                        startY = 0,
                        x = 0,
                        y = 0;

                    element.css({
                        position: 'relative',
                        border: '1px solid red',
                        backgroundColor: 'lightgrey',
                        cursor: 'pointer'
                    });

                    element.on('mousedown', function(event) {
                        // Prevent default dragging of selected content
                        event.preventDefault();
                        startX = event.pageX - x;
                        startY = event.pageY - y;
                        $document.on('mousemove', mousemove);
                        $document.on('mouseup', mouseup);
                    });

                    function mousemove(event) {
                        y = event.pageY - startY;
                        x = event.pageX - startX;
                        element.css({
                            top: y + 'px',
                            left: x + 'px'
                        });
                    }

                    function mouseup() {
                        $document.off('mousemove', mousemove);
                        $document.off('mouseup', mouseup);
                    }
                }
            };
        }]);
    </script>
</body>

</html>

指令通信

如下所示:

<html>

<head>
    <meta charset="utf-8">
    <title>自定义指令</title>
    <script src="angular.js"></script>
</head>

<body ng-app='docsTabsExample'>
    <my-tabs>
        <my-pane title="Hello">
            <p>Lorem ipsum dolor sit amet</p>
        </my-pane>
        <my-pane title="World">
            <em>Mauris elementum elementum enim at suscipit.</em>
            <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
        </my-pane>
    </my-tabs>
    <script>
    angular.module('docsTabsExample', [])
        .directive('myTabs', function() {
            return {
                restrict: 'E',
                transclude: true,
                scope: {},
                controller: ['$scope', function MyTabsController($scope) {
                    var panes = $scope.panes = [];

                    $scope.select = function(pane) {
                        angular.forEach(panes, function(pane) {
                            pane.selected = false;
                        });
                        pane.selected = true;
                    };

                    this.addPane = function(pane) {
                        if (panes.length === 0) {
                            $scope.select(pane);
                        }
                        panes.push(pane);
                    };
                }],
                templateUrl: 'my-tabs.html'
            };
        })
        .directive('myPane', function() {
            return {
                require: '^^myTabs',
                restrict: 'E',
                transclude: true,
                scope: {
                    title: '@'
                },
                link: function(scope, element, attrs, tabsCtrl) {
                    tabsCtrl.addPane(scope);
                },
                templateUrl: 'my-pane.html'
            };
        });
    </script>
</body>
<div class="tabbable">
    <ul class="nav nav-tabs">
        <li ng-repeat="pane in panes" ng-class="{active:pane.selected}">
            <a href="" ng-click="select(pane)">{{pane.title}}</a>
        </li>
    </ul>
    <div class="tab-content" ng-transclude></div>
</div>
<div class="tab-pane" ng-show="selected">
    <h4>{{title}}</h4>
    <div ng-transclude></div>
</div>

myPane指令拥有一个值为^^myTabsrequire选项,当指令拥有此选项时,$complie将会抛出一个错误除非发现指定的控制器。^^前缀告诉指令在当前元素父元素寻找控制器。

一个^前缀告诉指令在当前元素或当前元素父元素寻找控制器;没有前缀,告诉指令仅在当前元素寻找控制器。

myPane指令中,可以发现link函数最后一个参数tabsCtrl。当指令依赖一个控制器时,指令的link函数会把控制器作为第四个参数传递过去。因此myPane能够调用myTabsaddPane方法。

如果需要依赖多个控制器,指令中的require选项值以数组的形式来表示,传递给link函数相对应的参数也是一个数组,如下所示:

angular.module('docsTabsExample', [])
    .directive('myPane', function() {
        return {
            require: ['^^myTabs', 'ngModel'],
            restrict: 'E',
            transclude: true,
            scope: {
                title: '@'
            },
            link: function(scope, element, attrs, controllers) {
                var tabsCtrl = controllers[0],
                    modelCtrl = controllers[1];
                tabsCtrl.addPane(scope);
            },
            templateUrl: 'my-pane.html'
        };
    });