AngularJS 自定義 Directive 及程式碼示例

無上@訣發表於2015-05-20

前面一篇介紹了各種常用的AngularJS內建的Directives以及對應的程式碼例項。這篇我們再看看如何建立自己的Directive吧!

什麼時候需要自定義Directive?

1. 使你的Html更具語義化,不需要深入研究程式碼和邏輯即可知道頁面的大致邏輯。

2. 抽象一個自定義元件,在其他地方進行重用。

看一下如下2個程式碼片段:

示例1:

<body>
    <div>
        <p>This is your class name.</p>
        <div>
            <p>Your teacher:</p>
            <p>Mr. Wang</p>
            <p>35 years old</p>
            <p>English</p>
            <p>Descriptions: 1.85cm tall, with a pair of brown glasses, unmarried, easy going etc.</p>
        </div>
        <div>
            <div>
                <p>Students in the class:</p>
                <div>
                    <p>Jack</p>
                    <p>Male</p>
                    <p>15</p>
                    <p>Description: Smart ...</p>
                </div>
                <div>
                    <p>May</p>
                    <p>Female</p>
                    <p>14</p>
                    <p>Description: Diligent ...</p>
                </div>
                <div>
                    <p>Tom</p>
                    <p>Male</p>
                    <p>15</p>
                    <p>Description: Naughty ...</p>
                </div>
                <div>
                    <p>Alice</p>
                    <p>Female</p>
                    <p>14</p>
                    <p>Description: Smart ...</p>
                </div>
            </div>
        </div>
    </div>
</body>

示例2:

1 <body ng-app>
2     <class-info>
3         <teacher-info></teacher-info>
4         <student-infos></student-infos>
5     </class-info>
6 </body>

示例1中的程式碼你可能要完整的看完才能知道邏輯(當然示例1也不復雜,你可以想象下真實的場景要比這個複雜的多的多),不是說示例2中的程式碼少(邏輯被轉移到其他地方去了),而是在示例2中,光看Html標籤就知道這個頁面是在展示班級資訊,班級資訊中還有班主任的資訊和所有學生的資訊。

另外,示例1中,若一個班級的學生有30個,學生資訊的Html會出現30次,如果將來發生變動,這30出學生資訊的程式碼都需要改動。

製作一個屬於自己的Directive

示例3:

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.info = {
                    yourname: 'Jack',
                    template: 'template.html'
                };
            }]);

            // 自定義Element的Directive
            app.directive("studentInfo", function () {
                return {
                    // A 代表 Attribute
                    // C 代表 Class
                    // E 代表 Element
                    // ACE 表示同時建立 A、C、E 三種
                    restrict: 'ACE',
                    // templateUrl 指向獨立的Html檔案,AngularJS會用Html檔案中的內容替換studentInfo物件
                    templateUrl: 'template.html'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <student-info></student-info>
        <br />
        <data-student-info></data-student-info>
        <br />

        <div student-info></div>
        <br />
        <div data_student-info></div>
        <br />

        <div class="student-info"></div>
        <br />
        <div class="data-student-info"></div>
        <br />
    </div>
</body>
</html>

template.html:

1 <div>
2     <p>This is a custom template.</p>
3     <p>Your name: {{info.yourname}}</p>
4 </div>

注意:你可能還見過restrict:’M',或者Directive的命名以pre_suf、pre:suf這樣的程式碼書寫方式,這些都已經“過時”了,最潮的restrict僅使用ACE三種,命名方式使用pre-suf。

另外,你可能疑惑,為什麼加上”data-”字首的為什麼也能被解析?實際上AngularJS在處理Directive時,首先會忽略Directive命名中的”data-”或者”x-”字首,因此無論你加上”data-”還是”x-”,AngularJS還是能正確解析的,不過”x-”也是一種過時的寫法,我們可以忽略。

好了,是不是很容易?屬於我們自己的Directive就這樣建立成功了,接著讓我們更深入一些,看一下Directive的scope屬性。首先看一下以下3段程式碼:

示例4(student-info直接使用了包含它的Controller的Scope中的變數jack和alice):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.jack = {
                    name: 'Jack',
                    sex: 'Male'
                },
                $scope.alice = {
                    name: 'Alice',
                    sex: 'Female'
                }
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    template: '<div><p>Student name: {{jack.name}}</p><p>Student sex: {{jack.sex}}</p></div><br /><div><p>Student name: {{alice.name}}</p><p>Student sex: {{alice.sex}}</p></div>'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <student-info></student-info>
    </div>
</body>
</html>

示例5(和示例1類似,直接使用包含student-info的Controller中的變數students,在template中使用ng-repeat展示學生資訊):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.students = [
                    {
                        name: 'Jack',
                        sex: 'Male'
                    },
                    {
                        name: 'Alice',
                        sex: 'Female'
                    }
                ];
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    template: '<div ng-repeat="stu in students"><p>Student name:{{stu.name}}</p><p>Student sex:{{stu.sex}}</p></div>'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <student-info></student-info>
    </div>
</body>
</html>

示例6(定義兩個不同的Controller:jackController和aliceController,使student-info處於2個不同的controller中):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('jackController', ['$scope', function ($scope) {
                $scope.student =
                    {
                        name: 'Jack',
                        sex: 'Male'
                    }
            }]);

            app.controller('aliceController', ['$scope', function ($scope) {
                $scope.student =
                    {
                        name: 'Alice',
                        sex: 'Female'
                    }
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    template: '<div><p>Student name:{{student.name}}</p><p>Student sex:{{student.sex}}</p></div>'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="jackController as jackCtrl">
        <student-info></student-info>
    </div>
    <br />
    <div ng-controller="aliceController as aliceCtrl">
        <student-info></student-info>
    </div>
</body>
</html>

上述三種方式,都能達到我們所需的目的:自定義一個名為student-info的Directive,展示Controller中的學生資訊。但仔細分析上述3種不同的程式碼,能發現它們各自有不同的問題:

1. 示例4中,student-info的template中的所有表示式嚴重依賴Controller中的變數定義,導致student-info無法抽象成一個公共的學生資訊展示模組。

2. 示例5中,雖然使用ng-repeat封裝了程式碼,但是還是存在依賴Controller中students變數的問題,示例5僅比示例4稍微好點。

3. 示例6中,定義了不同的Controller來隔離作用域,但N個學生需要定義N個作用域,並且定義Controller時,還是必須定義一個名為student的變數,否則程式碼無法正確執行,因此還是存在耦合性。

好吧,讓我們看看AngularJS為我們提供的優雅的解決方案-Isolate scope:

示例7(通過使用=attr將Isolate scope中的屬性賦值給Directive的名為’attr’的Attribute):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.jack = {
                    name: 'Jack',
                    sex: 'Male'
                },
                $scope.alice = {
                    name: 'Alice',
                    sex: 'Female'
                }
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    // 定義student-info的Isolate scope
                    scope: {
                        // 作用域內定義一個變數:newNameInScope
                        // 值對應到Directive中的info屬性
                        newNameInScope: '=info'
                    },
                    // template 不再依賴外部, 僅依賴內部的newNameInScope變數
                    template: '<div><p>Student name: {{newNameInScope.name}}</p><p>Student sex: {{newNameInScope.sex}}</p></div>'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <!--將myController中的jack屬性傳遞給info-->
        <student-info info="jack"></student-info>
        <br />
        <!--將myController中的alice屬性傳遞給info-->
        <student-info info="alice"></student-info>
    </div>
</body>
</html>

不同之處已經在註釋中說明,示例7已經完全將student-info與外界隔離,不在存在耦合性,真正達到了我們自定義Directive的目的2(見本文”什麼時候需要自定義Directive”部分)。

讓我們再對示例7進行一些調整:

示例8:

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.jack = {
                    name: 'Jack',
                    sex: 'Male'
                },
                $scope.alice = {
                    name: 'Alice',
                    sex: 'Female'
                }
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    scope: {
                        newNameInScope: '=info'
                    },
                    // 這裡的alice將不能獲取Controller中的變數alice的資訊
                    template: '<div><p>Student name: {{newNameInScope.name}}</p><p>Student sex: {{newNameInScope.sex}}</p><br /><p>Deskmate name: {{alice.name}}</p><p>Deskmate sex: {{alice.sex}}</p></div>'
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <student-info info="jack"></student-info>
    </div>
</body>
</html>

這個就是所謂的封閉(Isolate),對比一下示例4,當建立student-info時指定了scope屬性後,不在scope中指定的變數,在student-info中將無法被識別,做到了“封閉”。這樣,當你定義一個公共模組時,不會因為在不同的Controller中使用而產生意想不到的問題。因此當你需要定義一個具有隔離性的Directive時,即使不需要傳遞Controller中的變數,也務必加上scope屬性。

不過我們只能將一個字串或者一個物件傳入Isolate scope中,試想若遇到某些特殊情況,需要直接包含指定的Html片段時怎麼辦?AngularJS也是有這樣的功能的。

示例9:

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('ngCustomDirectiveTest', []);
            app.controller('myController', ['$scope', function ($scope) {
                $scope.jack = {
                    name: 'Jack',
                    sex: 'Male'
                },
                $scope.alice = {
                    name: 'Alice',
                    sex: 'Female'
                }
            }]);

            app.directive("studentInfo", function () {
                return {
                    restrict: 'E',
                    // 指定transclude屬性為true
                    transclude: true
                };
            });
        })();
    </script>
</head>
<body ng-app="ngCustomDirectiveTest">
    <div ng-controller="myController as myCtrl">
        <!--指明student-info將會使用transclude模式-->
        <student-info ng-transclude>
            <!-- student-info的內容由使用者自己指定,並且內容中能訪問student-info的scope以外的變數 -->
            <p>Student name: {{jack.name}}</p>
            <p>Student sex: {{jack.sex}}</p>
            <br />
            <p>Deskmate name: {{alice.name}}</p>
            <p>Deskmate sex: {{alice.sex}}
        </student-info>
    </div>
</body>
</html>

其他自定義Directive的示例

示例10(自定義Directive操作DOM,官方文件中的demo):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('docsTimeDirective', []);

            app.controller('Controller', ['$scope', function ($scope) {
                $scope.format = 'M/d/yy h:mm:ss a';
            }])

            app.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);
                    });

                    timeoutId = $interval(function () {
                        updateTime();
                    }, 1000);
                }

                return {
                    link: link
                };
            }]);
        })();
    </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>
</body>
</html>

如果想要使Directive改變DOM,一般會用到link引數,其原型為:function link(scope, element, attrs) {…}:

  • scope: 與當前元素結合的scope
  • elment:當前元素
  • $attrs:當前元素的屬性物件

示例11(通過使用&attr開放Directive,將自定義的方法繫結到Directive上):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('isoFnBindTest', []);

            app.controller('myController', ['$scope', function ($scope) {
                $scope.name = '';
                $scope.message = '';
                $scope.isHide = true;
                $scope.sayHello = function (message, name) {
                    $scope.isHide = false;
                    $scope.name = name;
                    $scope.message = message;
                    alert($scope.message + ',' + $scope.name);
                };
            }]);

            app.directive('myGreeting', function () {
                return {
                    restrict: 'E',
                    transclude: true,
                    scope: {
                        // Step 2: greet方法繫結到onGreet屬性(對應Html中的on-greet),並將greet的輸入引數傳給onGreet
                        'greet': '&onGreet'
                    },
                    templateUrl: 'my-greeting.html'
                };
            });
        })();
    </script>
</head>
<body ng-app="isoFnBindTest">
    <div ng-controller="myController">
        <!-- Step 3: on-greet指向了myController中的sayHello方法,此時on-greet中能直接訪問到greet的輸入引數-->
        <my-greeting on-greet="sayHello(message, name)">
            <div ng-hide="isHide">
                {{message}}, {{name}}!
            </div>
        </my-greeting>
    </div>
</body>
</html>

my-greeting.html:

1 <div>
2   <!-- Step1: 一旦觸發click, 將呼叫Isolate scope中的greet方法-->
3   <button ng-click="greet({message: 'Hello', name: 'Tom'})">Click me!</button>
4   <div ng-transclude></div>
5 </div>

示例12(Directive偵聽事件,官方Demo):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('dragModule', []);

            app.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>
</head>
<body ng-app="dragModule">
    <span my-draggable>Drag ME</span>
</body>
</html>

示例13(Directive之間的相互作用,官方Demo):

<!DOCTYPE>
<html>
<head>
    <script src="/Scripts/angular.js"></script>
    <script type="text/javascript">
        (function () {
            var app = angular.module('docsTabsExample', []);

            app.directive('myTabs', function () {
                return {
                    restrict: 'E',
                    transclude: true,
                    scope: {},
                    controller: function ($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'
                };
            });

            app.directive('myPane', function () {
                return {
                    // 指定必須有myTabs物件,若物件不存在則會報錯,見下面的圖1
                    require: '^myTabs',  // ^ 表示將在父級的範圍內查詢該物件, 沒有 ^ 表示在Directive內查詢該物件, 若範圍指定錯誤無法找到myTabs,js則會報錯
                    restrict: 'E',
                    transclude: true,
                    scope: {
                        title: '@'
                    },
                    link: function (scope, element, attrs, tabsCtrl) {
                        tabsCtrl.addPane(scope);
                    },
                    templateUrl: 'my-pane.html'
                };
            });
        })();
    </script>
</head>
<body ng-app="docsTabsExample">
    <my-tabs>
      <my-pane title="Hello">
        <h4>Hello</h4>
        <p>Lorem ipsum dolor sit amet</p>
      </my-pane>
      <my-pane title="World">
        <h4>World</h4>
        <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>
</body>
</html>

my-tabs.html:

 <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>

my-pane.html:

<div class="tab-pane" ng-show="selected" ng-transclude>
</div>

參考資料

AngularJS官方文件:https://docs.angularjs.org/guide/directive

CodeSchool快速入門視訊:http://campus.codeschool.com/courses/shaping-up-with-angular-js/intro

相關文章