angularJS自定義指令各配置項詳解

原創性聲明:本文完全爲筆者原創,請尊重筆者勞動力。轉載務必註明原文地址。

指令詳解

一個指令的定義應當是如下這個樣子:

code:

angular.module('myApp', [])
    .directive('myDirective', function (UserDefinedService) { 
    // 指令定義放在這裏
});

其中,fun中的注入參數爲angular自帶或用戶定義的服務,需要在指令內部中調用。分析其結構:

  • angular.module('myApp', []) :是聲明整個應用對象的。
  • .directive('myDirective', fun(){}) :directive方法接受兩個參數:字符串和函數。

字符串myDirective是用以在視圖中引用特定的指令。而函數則返回一個對象,這個對象中定義了指令的全部行爲,$compile服務利用這個方法返回的對象,在DOM調用指令時來構造指令的行爲。即:

angular.module('myApp', [])
    .directive('myDirective', function (UserDefinedService) { 
        return {

        }
});

當然除了返回一個對象,其實也可以返回一個函數:

angular.module('myApp', [])
    .directive('myDirective', function (UserDefinedService) { 
        return function() { //此時這個函數叫做“鏈接傳遞函數”

        }
});

但是一般地,都採用返回對象的形式,這樣指令的定義可以更豐富。返回函數的情況只有在定義非常簡單的指令時纔可能會使用。

下面看指令中第二個參數——函數——返回對象的詳細配置!!!

code:

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            restrict: String,
            priority: Number,
            terminal: Boolean,
            template: String or Template Function:
                    function(tElement, tAttrs){...},
            templateUrl: String,
            replace: Boolean or String,
            scope: Boolean or Object,
            transclude: Boolean,
            controller: String or
                 function(scope, element, attrs, transclude, otherInjectables){...},
            controllerAs: String,
            require: String,
            link: function(scope, iElement, iAttrs){...},
            compile: //該屬性值返回的是一個對象或函數,如下所示:
                function(tElement, tAttrs, transclude) {
                    return {
                        pre: function(scope, iElement, iAttrs, controller){...},
                        post: function(scope, iElement, iAttrs, controller){...}
                    }

                    //或者
                    return function postLink(...){...}
                }
        }
    });

下面分別對各個配置項進行詳細說明:

1. restrict

  • 非必須
  • 可選值:’EACM’

EACM指的是指令在DOM視圖中的應用形式,如下:

E: (元素)

<my-directive></my-directive>

A: (屬性,默認值)

<div my-directive="expression"></div>

C: (類名)

<div class="my-directive:expression;"></div>

M: (註釋)

<--directive:my-directive expression-->

這些值可以單獨使用,也可以混合使用。其中A是推薦的方式,因爲它的兼容性更好,也更容易擴充。

2. priority

優先級。它的作用是聲明指令的優先級,當多個指令用在同一個DOM元素上時,哪個會先執行呢?就取決於這個參數。如果兩個指令的優先級一樣,那麼聲明在前的會先被調用並執行。

例如 ng-repeat的優先級就是1000,因此,它總是比其他指令更優先執行。

3. terminal

Boolean值,它的作用是告訴angularJS是否停止運行當前元素上比本指令優先級更低的指令,但與當前指令優先級同級的指令仍然會被執行的。如下面的例子:

<div>
    <p my-terminal-test1 my-terminal-test2></p>
</div>
angular.module('angularLearningApp')
    .directive('myTerminalTest1', function() {
        return {
            restrict: 'A',
            priority: 1,
            template: '百度',
            link: function (scope, element, attrs) {
                console.log("myTerminalTest1");
            }
        }
    })
    .directive('myTerminalTest2', function() {
        return {
            restrict: 'A',
            priority: 2,
            terminal: false, //現將terminal設置爲false
            link: function(scope, element, attrs) {
                console.log("myTerminalTest2");
                element[0].textContent += '谷歌';
            }
        }
    });

顯然div中的內容是 百度谷歌 ,如果將terminal設置爲true,則顯示的結果爲 谷歌 ,這是因爲 myTerminalTest1指令的優先級低於 myTerminalTest2,而terminal爲true因此,低於它的指令將不被執行。

4. template

template有兩種形式:

  • String //模板字符串
  • function(tElement, tAttrs){ …; return templateStr;//返回模板字符串}

需要注意的是: template返回的模板中,DOM結構中必須存在一個根節點。在實際的開發中,更常使用的是templateUrl,因爲可以避免字符串拼接,那是可讀性、維護性很差的方式。另外,template中最爲重要的東西是controller與本指令中template變量的數據傳遞。

5. templateUrl

同樣有兩種形式:

  • String // 模板html文件路徑
  • function(tElement, tAttrs){…; return templatePath;//返回模板html路徑}

默認情況下,調用指令會在後臺通過ajax請求html模板文件,有兩個特別需要注意的:

  1. 在本地開發時,需要在後臺運行一個本地服務器,用以從文件系統加載HTML模板,否則會導致Cross Origin Request Script(CORS)錯誤。
  2. 模板加載是異步的,意味着編譯和鏈接要暫停,等待模板加載完成。

通過Ajax異步加載大量的模板將嚴重拖慢一個客戶端應用的速度。爲了避免延遲,可以在部
署應用之前對HTML模板進行緩存。在大多數場景下緩存都是一個非常好的選擇,因爲AngularJS
通過減少請求數量提升了性能。更多關於緩存的內容請查看第28章。

模板加載後, AngularJS會將它默認緩存到$templateCache服務中。在實際生產中,可以提
前將模板緩存到一個定義模板的JavaScript文件中,這樣就不需要通過XHR來加載模板了。更多內容請查看第34章。

6. replace

默認值是false,表示模板的內容將會被插入到視圖中應用指令元素的內部。如果設置爲true,則表示替代,即插入到視圖中時,應用指令的html元素將被刪除,取而代之的是html模板。

7. scope

可選參數

  • boolean 默認是false,即該指令並不會創建新的作用域,改指令內部或外部的作用域是一樣的。當爲true時,會從父作用域繼承並創建一個新的作用域對象,即該指令內部和外部並不是在一個作用域內。
  • Object :設置此屬性也被稱爲“隔離作用域”。
scope爲Boolean時

code:

<div ng-init="someProperty = 'some data'"></div>
<div ng-init="siblingProperty='moredata'">
    Inside Div Two: { { aThirdProperty } }
    <div ng-init="aThirdProperty = 'data for 3rd property'" 
            ng-controller="SomeController">
        Inside Div Three: { { aThirdProperty } }
        <div ng-controller="SecondController">
            Inside Div Four: { { aThirdProperty } }
            <br>
            Outside myDirective: { { myProperty } }
            <div my-directive ng-init="myProperty = 'wow, this is cool'">
                Inside myDirective: { { myProperty } }
            <div>
        </div>
    </div>
</div>
angular.module('myApp', [])
.controller('SomeController', function($scope) {
// 可以留空,但需要被定義
})
.controller('SecondController', function($scope) {
// 同樣可以留空
});

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            //scope: true
        };
    });

首先,將把 scope:true 註釋掉即設置scope爲默認的false。此時的結果是:

view:

<div ng-init="someProperty = 'some data'"></div>
<div ng-init="siblingProperty='moredata'">
    Inside Div Two: {{ aThirdProperty }}
    <div ng-init="aThirdProperty = 'data for 3rd property'" 
        ng-controller="SomeController">
        Inside Div Three:{{ aThirdProperty }}
        <div ng-controller="SecondController">
            Inside Div Four:{{ aThirdProperty }}<br>
            Outside myDirective: {{ myProperty }}
            <div my-directive-scope-test ng-init="myProperty = 'wow, this is cool'">
                Inside myDirective: {{ myProperty }}<div>
        </div>
    </div>
</div>

顯而易見,Outside myDirective和Inside myDirecitve都將是有值的,即使myProperty的值是在指令標籤中定義的,但因爲指令中的配置項scope爲false,該指令並沒有產生一個新的作用域,因此,在這個指令標籤內部和外部都是在一個作用域下,即:SecondController對應的作用域下,所以值都是有的。

但如果將 scope:true 釋放掉,那麼該指令就會產生一個獨立作用域,此作用域繼承父作用域,但是在該作用域中定義的變量myProperty,就無法在該指令外部調用了,因此,結果就是:

Outside myDirective:
    Inside myDirective: wow, this is cool

這就是scope爲boolean值時的作用。

scope爲Object時————隔離作用域

設置scope配置屬性值爲Object時,指令的模板就無法訪問外部作用域了。也因此,不受外部作用域變量的影響,因此,隔離作用域常用來創建可複用的指令組件。

code:

<div ng-controller='SomeController2'>
    Outside myDirective: { { myProperty2 } }
    <div my-directive-scope-test2 ng-init="myProperty2 = 'wow, this is cool!'">
        Inside myDirective: { { myProperty2 } }
    </div>
</div>

angular.module('myApp', [])
    .controller('SomeController2', function($scope) {

    })
    .directive('myDirectiveScopeTest2', function() {
        return {
            restrict: 'A',
            scope: {}, //對象
            priority: 100,
            template: '<div>Inside myDirective: { { myProperty2 } }</div>'
        };
    });

view:

<div ng-controller='SomeController2'>
    Outside myDirective: {{ myProperty2 }}
<div my-directive-scope-obj-test ng-init="myProperty2 = 'wow, this is cool!'">
    Inside myDirective: {{ myProperty2 }}
</div>
</div>

Inside myDirective中將不會出現值,沒錯,因爲scope隔離了模板與外界作用域。

但是Outside myDirective中將存在值,爲什麼呢?難道隔離作用域只是隔離了模板與外界作用域,而當前指令應用的DOM元素中用其他指令定義的變量仍然可以在外面被訪問?

爲此,在此進行對比演示:

code:

<div  ng-controller="ScopeValueCompareController"
                     ng-init="myProperty='wow,this is so cool'">
    Surrounding scope: {{ myProperty }}
    <div my-inherit-scope-directive></div>
    <div my-directive></div>
</div>
angular.module('myApp', [])
    .controller('ScopeValueCompareController', function($scope) {

    })
    .directive('myDirective3', function() {
        return {
            restrict: 'A',
            template: 'Inside myDirective, isolate scope: {{ myProperty }}',
            scope: {}
        };
    })
    .directive('myInheritScopeDirective', function() {
        return {
            restrict: 'A',
            template: 'Inside myDirective, isolate scope: {{ myProperty }}',
            scope: true
        };
    });

view:

<div ng-controller="ScopeValueCompareController"
    ng-init="myProperty3='wow,this is so cool'">
    Surrounding scope: {{ myProperty3 }}
    <div my-directive3></div>
    <div my-inherit-scope-directive></div>
</div>

scope爲{}時,指令內模板作用域被隔離開,所以是沒有值得。scope爲true時,指令內新建了一個作用域,但他繼承父級作用域(這裏是ScopeValueCompareController對應的作用域),因此可以訪問外部變量。

scope爲對象時的綁定策略

scope爲Object時,像上面的空對象的情況肯定是不適用的。angularJS提供了幾種方法,可以將指令內部的隔離作用域和指令外部的作用域進行數據綁定。

  1. @ (or @attr) : 本地作用域屬性。使用 @ 符號將本地作用域與DOM屬性的值進行綁定,指令內部作用域可以訪問並使用外部作用域的變量,常用於DOM中屬性值爲固定參數
  2. = (or =attr) : 雙向綁定。使用=符號將本地作用域中的屬性和DOM屬性的值進行雙向綁定,那麼當DOM屬性值隨時改變時,指令中的值也會改變,同時反過來也是一樣的。常用於DOM中對應屬性值是動態的,如ng-model
  3. & (or &attr) : 父級作用域綁定。主要用於運行其中的函數,也就是說這個值在指令中設置後,會生成一個指向父級作用域的包裝函數。如果要調用帶有參數的父方法,則需要在DOM指令屬性值的函數形參中傳入一個對象,對象的鍵是參數名,值是參數值。

如下面的例子:

code:

<input type="text" ng-model="to"/>
<!-- 調用指令 -->
<div scope-example ng-model="to"  on-send="sendMail(email)"
    from-name="[email protected]">
</div>

自定義指令 scope-example 中如果要訪問此處的數據(模型to、 函數方法sendMail(email)以及字符串"[email protected]")的話,就必須配置scope爲對象,如下:

scope: {
    ngModel: '=', // 將ngModel同指定對象綁定
    onSend: '&', // 將引用傳遞給這個方法
    fromName: '@' // 儲存與fromName相關聯的字符串
}

注意指令中本地變量的命名規則(駝峯法)。如果不想用駝峯法,想自定義隨便取名,也可以指定要綁定的外部DOM變量,如下:

scope: {
    a: '=ngModel', // 將ngModel同指定對象綁定
    b: '&onSend', // 將引用傳遞給這個方法
    c: '@fromName' // 儲存與fromName相關聯的字符串
}

那麼a 的值就是 to的值, b的值就是 sendMail(email) 方法的引用,c 的值就是[email protected]

這就是三種綁定策略的不同以及各自的適用場景!

8. transclude

可選參數。Boolean值,默認值爲false。定義爲true時,它會將整個DOM嵌入到指令內部定義的模板中,包括DOM中的其他指令。

只有當你希望創建一個可以包含任意內容的指令時, 才使用transclude: true。

爲了將作用域傳遞進去, scope參數的值必須通過{}或true設置成隔離作用域。如果沒有設
置scope參數,那麼指令內部的作用域將被設置爲傳入模板的作用域。嵌入允許指令的使用者方便地提供自己的HTML模板,其中可以包含獨特的狀態和行爲,並對指令的各方面進行自定義。看一個簡單的例子,一個包括標題和少量html內容的側邊欄。

code:

<div sidebox title="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>

爲這個側邊欄創建一個簡單的指令,設置transclude爲true:

angular.module('myApp', [])
    .directive('sidebox', function() {
        return {
            restrict: 'EA',
            scope: {
                title: '@'
            },
            transclude: true,
            template: '<div class="sidebox">\
                <div class="content">\
                <h2 class="header">{ { title } }</h2>\
                <span class="content" ng-transclude>\
                </span>\
                </div>\
                </div>'
        };
    });

view:

<div sidebox title="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>

此時,在瀏覽器中生成的DOM結構爲:

<div sidebox title="Links">     <!-- a:原來DOM中應用sidebox指令的標籤 -->

    <div class="sidebox">       <!-- c:sidebox指令中定義的模板 -->
        <div class="content">   <!-- c -->
            <h2 class="header">{ { title } }</h2>   <!-- c -->
            <span class="content" ng-transclude>    <!-- c -->

                <ul>            <!-- b 的子標籤內容 -->
                    <li>First link</li>     <!-- b -->
                    <li>Second link</li>    <!-- b -->
                </ul>

            </span>             <!-- c -->
        </div>                  <!-- c -->
    </div>                      <!-- c -->

</div>                          <!-- a -->

顯然,transclude設爲true後,angularJS將該指令應用的DOM元素(a)的內部所有元素(b)都嵌入到了指令模板中聲明ng-transclude的元素(c)內,並全部套入到a中,再被渲染出來。

transclude和ng-transclude是聯合使用的。

9.controller

controller可以是字符串或函數:

  1. String : 以該字符串爲值去整個項目中查找同名註冊的controller
  2. function: 匿名構造函數定義的內聯controller

code:

angular.module('myApp',[])
    .directive('myDirective', function() {
        restrict: 'A',
        controller:
            function($scope, $element, $attrs, $transclude) {
                // 控制器邏輯放在這裏
            }
    });

我們可以將任意可以被注入的AngularJS服務傳遞給控制器。例如,如果我們想要將$log服
務傳入控制器,只需簡單地將它注入到控制器中,便可以在指令中使用它了。上面的例子,有:

  • $scope :與指令元素相關聯的當前作用域
  • $element: 指令元素,即當前指令應用的DOM元素
  • attrs:<divid="aDiv"class="box"></div> attrs值爲:{id: "aDiv", class: "box"}
  • $transclude: transclude鏈接函數是實際被執行用來克隆元素和操作DOM的函數。

指令的控制器和link函數可以進行互換。控制器主要是用來提供可在指令間複用的行爲,但
鏈接函數只能在當前內部指令中定義行爲,且無法在指令間複用。由於指令可以require其他指令所使用的控制器,因此控制器常被用來放置在多個指令間共享的動作。

10.controllerAs

字符串。這個參數用以設置控制器的別名,以此名發佈控制器,並且作用域可以訪問controllerAs。

code:

angular.module('myApp')
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            template: '<h4>{{ myController.msg }}</h4>',
            controllerAs: 'myController',
            controller: function() {
                this.msg = "Hello World"
            }
        }
    });

11. require

字符串或數組。可選值。當值爲字符串時,它應當是另一個指令的名字。require是將其值所指定的指令中的控制器注入到當前指令中,並作爲當前指令的link函數的第四個參數。而這個被注入進來的控制器(位於指令鏈接的父指令中)會首先被當前指令查找,查找當然是根據require的值決定的,不過給這個值予以不同的前綴,會影響它的查找行爲:

  • ? 尋找require值對應的指令中的控制器,如果在指令鏈的父指令(即require的值所對應的指令)中沒有找到需要的控制器,則當前指令中的link函數的第四個參數將會是null。
  • ^ 如果在指令鏈的父指令中沒有找到需要的控制器,則會進一步往指令鏈上游尋找需要的控制器。
  • ?^ 教程的解釋是:我們可選擇地加載需要的指令並在父指令鏈中進行查找。
  • 沒有前綴的情況: 如果沒有前綴,則指令就會在自身所提供的控制器中進行查找,如果沒有找到控制器(或者沒有找到require的值所對應的指令),就會拋出一個錯誤。

code:

<hello>
    <div>hello</div>
    <beautiful good>
        beautiful
    </beautiful>
</hello>
angular.module("myApp", [])
    .directive("hello",function(){
        return {
            restrict : "E",
            controller : function($scope){
                $scope.name = "張三";
                this.information = {
                    name : $scope.name,
                    age : 25,
                    job : "程序員"
                }
            },
            link : function(scope){

            }
        }
    })
    .directive("beautiful",function(){
        return {
            restrict : "E",
            require : "?good",
            controller : function(){
                this.name = "beautiful";
            },
            link : function (scope,element,attrs,good) {
                console.log(good.name)
            }
        }
    }).
    directive("good",function(){
        return {
            restrict : "A",
            require : "?^hello",
            controller : function(){
                this.name = "good";
            },
            link : function (scope,element,attrs,hello) {
                console.log(hello.information)
            }
        }
    });

12.compile

該屬性的屬性值是一個函數內部返回一個對象,或者函數。理解compile和link函數是angularJS需要討論的高級話題之一,對於瞭解angularJS是如何工作的是至關重要的。

本質上,當我們設置了link選項,實際上是創建了一個postLink()鏈接函數,以便compile()函數可以定義函數。

通常情況下,如果我們設置了complie()函數,說明我們希望在指令和實時數據被放到DOM中之前對DOM進行操作,在這個函數中進行諸如,添加和刪除節點等DOM操作是安全的。

特別注意:compile函數和link函數是互斥的。即,如果同時設置了這兩個配置項,那麼angularJS會選擇compile函數的返回函數作爲link函數,而本身link函數的配置會被完全忽略。

  • 編譯函數compile內部通常用來轉換可以被安全操作的DOM節點,不要對DOM進行事件監聽註冊。
  • 鏈接函數link負責將DOM和作用域進行鏈接。

如下一個例子:

compile: function(tEle, tAttrs, transcludeFn) {
    var tplEl = angular.element('<div>' +
            '<h2></h2>' +
            '</div>');
    var h2 = tplEl.find('h2');
    h2.attr('type', tAttrs.type);
    h2.attr('ng-model', tAttrs.ngModel);
    h2.val("hello");
    tEle.replaceWith(tplEl);
    return function(scope, ele, attrs) {
        // 連接函數
    };
}

13 link函數

link函數用來創建可以操作DOM的屬性。當定義了編譯函數來取代鏈接函數時,鏈接函數是我們能提供給返回對象的第二個方法,也就是postLink函數。本質上講,這個事實正說明了鏈接函數的作用。它會在模板編譯並同作用域進行鏈接後被調用,因此它負責設置事件監聽器,監視數據變化和實時的操作DOM。

鏈接函數一共有四個參數:

  • scope : 指令用來在其內部註冊監聽器的作用域
  • iElement: 代表實例元素,即使用此指令的元素。在postLink函數中,我們應該只操作這個元素和其子元素,因爲這些元素已經被鏈接過了。
  • iAttrs: 代表實例屬性,一個由定義在元素上的屬性組成的標準化列表,可以在所有指令的鏈接函數間共享。會以javascript對象的形式進行傳遞。
  • controller: 這個參數只有在當前指令存在require選項時纔會有,否則就是undefined。如果require的值是另一個指令A,那麼controller的值就是這個指令A中的controller;如果require的值是另一個單獨的controller,那麼當前controller的值就是這個controller;如果require指向多個控制器,那麼當前controller就是一個由這些多個控制器組成的數組。

link函數是指令中最爲常用的一個配置項。它和controller函數最大的區別就是功能性區分,前者是用以操作DOM,後者用以指令間傳遞。

自定義指令的配置項中complie、link、controller等還有很深的水,需要進一步去探究。關於指令的link中如何訪問到視圖中的ng-model的值,其中也存在很多問題。

補充: 更多內容,可以在簡書上關注我(東方一號藍)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章