Angular學習筆記(5)—指令詳解

指令定義

對于指令,可以把它簡單的理解成在特定DOM元素上運行的函數,指令可以擴展這個元素的功能。
??我們可以自己創(chuàng)造新的指令。directive()這個方法是用來定義指令的:

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

directive()方法可以接受兩個參數:

  • name(字符串)
    指令的名字,用來在視圖中引用特定的指令。要用駝峰式寫法。
  • factory_function (函數)
    這個函數返回一個對象,其中定義了指令的全部行為。$compile服務利用這個方法返回的對象,在DOM調用指令時來構造指令的行為。
angular.application('myApp', [])
    .directive('myDirective', function() {
        return {
            // 通過設置項來定義指令,在這里進行覆寫
        };
    });

我們也可以返回一個函數代替對象來定義指令,但是像上面的例子一樣,通過對象來定義是最佳的方式。當返回一個函數時,這個函數通常被稱作鏈接傳遞函數,利用它我們可以定義指令的鏈接功能。由于返回函數而不是對象會限制定義指令時的自由度,因此只在構造簡單的指令時才比較有用。
??當AngularJS啟動應用時,它會把第一個參數當作一個字符串,并以此字符串為名來注冊第二個參數返回的對象。AngularJS編譯器會解析主HTML的DOM中的元素、屬性、注釋和CSS類名中使用了這個名字的地方,并在這些地方引用對應的指令。當它找到某個已知的指令時,就會在頁面中插入指令所對應的DOM元素。

<div my-directive></div>

指令的工廠函數只會在編譯器第一次匹配到這個指令時調用一次。和controller函數類似,我們通過$injetor.invoke來調用指令的工廠函數。當AngularJS在DOM中遇到具名的指令時,會去匹配已經注冊過的指令,并通過名字在注冊過的對象中查找。此時,就開始了一個指令的生命周期,指令的生命周期開始于$compile方法并結束于link方法。
??定義一個指令時可以使用的全部設置選項:

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

restrict(字符串)

restrict是一個可選參數。它告訴AngularJS這個指令在DOM中可以何種形式被聲明。默認restrict的值是A,即以屬性的形式來進行聲明。
可選值如下:

  • E(元素)
<my-directive></my-directive>
  • A(屬性,默認值)
<div my-directive="expression"></div>
  • C(類名)
<div class="my-directive:expression;"></div>
  • M(注釋)
<--directive:my-directive expression-->

這些選項可以單獨使用,也可以混合在一起使用:

angular.module('myDirective', function(){
    return {
        restrict: 'EA' // 輸入元素或屬性
    };
});

上面的配置可以同時用屬性或元素的方式來聲明指令:

<-- 作為一個屬性 -->
<div my-directive></div>
<-- 或者作為一個元素 -->
<my-directive></my-directive>

屬性是用來聲明指令最常用的方式,因為它能在包括老版本的IE瀏覽器在內的所有瀏覽器中正常工作,并且不需要在文檔頭部注冊新的標簽。

優(yōu)先級priority(數值型)

優(yōu)先級參數可以被設置為一個數值。大多數指令會忽略這個參數,使用默認值0,但也有些場景設置高優(yōu)先級是非常重要的。
??如果一個元素上具有兩個優(yōu)先級相同的指令,聲明在前面的那個會被優(yōu)先調用。具有更高優(yōu)先級的指令總是優(yōu)先運行。
??ngRepeat是所有內置指令中優(yōu)先級最高的,它總是在其他指令之前運行。

terminal(布爾型)

這個參數用來告訴AngularJS停止運行當前元素上比本指令優(yōu)先級低的指令。但同當前指令優(yōu)先級相同的指令還是會被執(zhí)行。
??如果元素上某個指令設置了terminal參數并具有較高的優(yōu)先級,就不要再用其他低優(yōu)先級的指令對其進行修飾了,因為不會被調用。但是具有相同優(yōu)先級的指令還是會被繼續(xù)調用。
??使用了terminal參數的例子是ngViewngIfngIf的優(yōu)先級略高于ngView,如果ngIf的表達式值為true,ngView就可以被正常執(zhí)行,但如果ngIf表達式的值為false,由于ngView的優(yōu)先級較低就不會被執(zhí)行。

template(字符串或函數)

template參數是可選的,必須被設置為以下兩種形式之一:

  • 一段HTML文本;
  • 一個可以接受兩個參數的函數,參數為tElementtAttrs,并返回一個代表模板的字符串。tElementtAttrs中的t代表template,是相對于instance的。

AngularJS會同處理HTML一樣處理模板字符串。模板中可以通過大括號標記來訪問作用域,例如{{ expression }}。
如果模板字符串中含有多個DOM元素,或者只由一個單獨的文本節(jié)點構成,那它必須被包含在一個父元素內。換句話說,必須存在一個根DOM元素:

template: '\
    <div> <-- single root element -->\
    <a >Click me</a>\
    <h1>When using two elements, wrap them in a parent element</h1>\
</div>\

另外,注意每一行末尾的反斜線,這樣AngularJS才能正確解析多行字符串。在實際生產中,更好的選擇是使用templateUrl參數引用外部模板。

templateUrl(字符串或函數)

templateUrl是可選的參數,可以是以下類型:

  • 一個代表外部HTML文件路徑的字符串;
  • 一個可以接受兩個參數的函數,參數為tElementtAttrs,并返回一個外部HTML文件路徑的字符串。

默認情況下,調用指令時會在后臺通過Ajax來請求HTML模板文件。有兩件事情需要知道。

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

模板加載后,AngularJS會將它默認緩存到$templateCache服務中。在實際生產中,可以提前將模板緩存到一個定義模板的JS文件中,這樣就不需要通過XHR來加載模板了。

replace(布爾型)

replace是一個可選參數,如果設置了這個參數,值必須為true,因為默認值為false。默認值意味著模板會被當作子元素插入到調用此指令的元素內部:

<div some-directive></div>
.directive('someDirective', function() {
    return {
        template: '<div>some stuff here<div>'
    };
});
<!--調用指令之后的結果如下(默認replace為false時的情況):-->
<div some-directive>
    <div>some stuff here<div>
</div>

如果replace被設置為了true:

.directive('someDirective', function() {
    return {
        replace: true 
        template: '<div>some stuff here<div>'
    };
});
<!--指令調用后的結果將是:-->
<div>some stuff here<div>

scope參數(布爾型或對象)

scope參數是可選的,可以被設置為true或一個對象。默認值是false。當scope設置為true時,會從父作用域繼承并創(chuàng)建一個新的作用域對象。
??如果一個元素上有多個指令使用了隔離作用域,其中只有一個可以生效。只有指令模板中的根元素可以獲得一個新的作用域。因此,對于這些對象來說scope默認被設置為true。
??內置指令ng-controller的作用,就是從父級作用域繼承并創(chuàng)建一個新的子作用域。它會創(chuàng)建一個新的從父作用域繼承而來的子作用域。
??為了進一步證明作用域的繼承機制是向下而非向上進行的,下面的例子展示的是{{aThirdProperty}}從父作用域繼承而來:

<!--HTML-->
<div ng-app="myApp" 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}}
        </div>
    </div>
</div>
<!--JS-->
angular.module('myApp', [])
    .controller('SomeController', function($scope) {
        // 可以留空,但需要被定義
    })
    .controller('SecondController', function($scope) {
        // 同樣可以留空
    })

如果要創(chuàng)建一個能夠從外部原型繼承作用域的指令,將scope屬性設置為true:

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

下面用指令來改變DOM的作用域:

<body ng-app="myApp">
<div ng-app="myApp" ng-init="someProperty = 'some data'">
<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>
</div>
</body>

隔離作用域

具有隔離作用域的指令最主要的使用場景是創(chuàng)建可復用的組件,組件可以在未知上下文中使用,并且可以避免污染所處的外部作用域或不經意地污染內部作用域。
??創(chuàng)建具有隔離作用域的指令需要將scope屬性設置為一個空對象{}。如果這樣做了,指令的模板就無法訪問外部作用域了:

<div ng-controller='MainController'>
    Outside myDirective: {{myProperty}}
    <div my-directive ng-init="myProperty = 'wow, this is cool'">
        Inside myDirective: {{myProperty}}
    </div>
</div>
angular.module('myApp', [])
    .controller('MainController', function($scope) {
    })
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            scope: {},
            priority: 100,
            template: '<div>Inside myDirective {{myProperty}}</div>'
        };
    });

注意,這里為myDirective設置了一個高優(yōu)先級。由于ngInit指令會以非零的優(yōu)先級運行,這個例子將會優(yōu)先運行ngInit指令,然后才是我們定義的指定,并且這個myProperty$scope對象中是有效的。
??示例代碼的效果與將scope設置為true幾乎是相同的。下面看一下使用繼承作用域的指令的例子,對比一下二者:

<div ng-init="myProperty='wow,thisiscool'">
    Surrounding scope: {{ myProperty }}
    <div my-inherit-scope-directive></div>
    <div my-directive></div>
</div>

angular.module('myApp', [])
    .directive('myDirective', 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
        };
    });

綁定策略

使用無數據的隔離作用域并不常見。AngularJS提供了幾種方法能夠將指令內部的隔離作用域,同指令外部的作用域進行數據綁定。
??為了讓新的指令作用域可以訪問當前本地作用域中的變量,需要使用下面三種別名中的一種。
??本地作用域屬性:使用@符號將本地作用域同DOM屬性的值進行綁定。指令內部作用域可以使用外部作用域的變量:

  • @ (or @attr)
    現在,可以在指令中使用綁定的字符串了。
    雙向綁定:通過=可以將本地作用域上的屬性同父級作用域上的屬性進行雙向的數據綁定。就像普通的數據綁定一樣,本地屬性會反映出父數據模型中所發(fā)生的改變。
  • = (or =attr)
    父級作用域綁定 通過&符號可以對父級作用域進行綁定,以便在其中運行函數。意味著對這個值進行設置時會生成一個指向父級作用域的包裝函數。
    要使調用帶有一個參數的父方法,我們需要傳遞一個對象,這個對象的鍵是參數的名稱,值是要傳遞給參數的內容。
  • & (or &attr)

例如,假設我們在開發(fā)一個電子郵件客戶端,并且要創(chuàng)建一個電子郵件的文本輸入框:

<input type="text" ng-model="to"/>
<!-- 調用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)" from-name="ari@fullstack.io" ></div>

在指令中做如下設置以訪問這些內容:

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

transclude

transclude是一個可選的參數。如果設置了,其值必須為true,它的默認值是false。
??嵌入通常用來創(chuàng)建可復用的組件,典型的例子是模態(tài)對話框或導航欄。
??我們可以將整個模板,包括其中的指令通過嵌入全部傳入一個指令中。這樣做可以將任意內容和作用域傳遞給指令。transclude參數就是用來實現這個目的的,指令的內部可以訪問外部指令的作用域,并且模板也可以訪問外部的作用域對象。
??為了將作用域傳遞進去,scope參數的值必須通過{}或true設置成隔離作用域。如果沒有設置scope參數,那么指令內部的作用域將被設置為傳入模板的作用域。
??只有當你希望創(chuàng)建一個可以包含任意內容的指令時,才使用transclude: true
??嵌入允許指令的使用者方便地提供自己的HTML模板,其中可以包含獨特的狀態(tài)和行為,并對指令的各方面進行自定義。
??下面一起來實現個小例子,創(chuàng)建一個可以被自定義的可復用指令。
??例如,假設我們想創(chuàng)建一個包括標題和少量HTML內容的側邊欄,如下所示:

<div sideboxtitle="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>
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>'
        };
    });

這段代碼告訴AngularJS編譯器,將它從DOM元素中獲取的內容放到它發(fā)現ng-transclude指令的地方。
??借助transclusion,我們可以將指令復用到第二個元素上,而無須擔心樣式和布局的一致性問題。
??例如,下面的代碼會產生兩個樣式完全一致的側邊欄。

<div sideboxtitle="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>
<div sideboxtitle="TagCloud">
    <div class="tagcloud">
        <a href="">Graphics</a>
        <a href="">AngularJS</a>
        <a href="">D3</a>
        <a href="">Front-end</a>
        <a href="">Startup</a>
    /div>
</div>

如果指令使用了transclude參數,那么在控制器中就無法正常監(jiān)聽數據模型的變化了。這就是最佳實踐總是建議在鏈接函數里使用$watch服務的原因。

controller(字符串或函數)

controller參數可以是一個字符串或一個函數。當設置為字符串時,會以字符串的值為名字,來查找注冊在應用中的控制器的構造函數:

angular.module('myApp', [])
    .directive('myDirective', function() {
        restrict: 'A', // 始終需要
        controller: 'SomeController'
    })
// 應用中其他的地方,可以是同一個文件或被index.html包含的另一個文件
angular.module('myApp')
    .controller('SomeController', function($scope, $element, $attrs, $transclude) {
        // 控制器邏輯放在這里
    });

可以在指令內部通過匿名構造函數的方式來定義一個內聯的控制器:

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

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

  1. $scope
    與指令元素相關聯的當前作用域。
  2. $element
    當前指令對應的元素。
  3. $attrs
    由當前元素的屬性組成的對象。例如,下面的元素:
    <div id="aDiv"class="box"></div>
    具有如下的屬性對象:
{
    id: "aDiv",
    class: "box"
}
  1. $transclude
    嵌入鏈接函數會與對應的嵌入作用域進行預綁定。
    transclude鏈接函數是實際被執(zhí)行用來克隆元素和操作DOM的函數。

在控制器內部操作DOM是和AngularJS風格相悖的做法,但通過鏈接函數就可以實現這個需求。僅在compile參數中使用transcludeFn是推薦的做法。
??例如,我們想要通過指令來添加一個超鏈接標簽。可以在控制器內的$transclude函數中實現,如下所示:

angular.module('myApp')
    .directive('link', function() {
        return {
            restrict: 'EA',
            transclude: true,
            controller:
            function($scope, $element, $transclude, $log) {
                $transclude(function(clone) {
                    var a = angular.element('<a>');
                    a.attr('href', clone.text());
                    a.text(clone.text());
                    $log.info("Created new a tag in link directive");
                    $element.append(a);
                });
            }
        };
    });

指令的控制器和link函數可以進行互換。控制器主要是用來提供可在指令間復用的行為,但鏈接函數只能在當前內部指令中定義行為,且無法在指令間復用。
??link函數可以將指令互相隔離開來,而controller則定義可復用的行為。
??由于指令可以require其他指令所使用的控制器,因此控制器常被用來放置在多個指令間共享的動作。
??如果我們希望將當前指令的API暴露給其他指令使用,可以使用controller參數,否則可以使用link來構造當前指令元素的功能性。如果我們使用了scope.$watch()或者想要與DOM元素做實時的交互,使用鏈接會是更好的選擇。
??技術上講,$scope會在DOM元素被實際渲染之前傳入到控制器中。在某些情況下,例如使用了嵌入,控制器中的作用域所反映的作用域可能與我們所期望的不一樣,這種情況下,$scope對象無法保證可以被正常更新。
??當想要同當前屏幕上的作用域交互時,可以使用被傳入到link函數中的scope參數。

controllerAs(字符串)

controllerAs參數用來設置控制器的別名,可以以此為名來發(fā)布控制器,并且作用域可以訪問controllerAs。這樣就可以在視圖中引用控制器,甚至無需注入$scope
??例如,創(chuàng)建一個MainController,然后不要注入$scope,如下所示:

angular.module('myApp')
.controller('MainController', function() {
this.name = "Ari";
});

現在,在HTML中無需引用作用域就可以使用MainController

<div ng-app ng-controller="MainController">
<input type="text" ng-model="main.name" />
<span>{{ main.name }}</span>
</div>

這個參數看起來好像沒什么大用,但它給了我們可以在路由和指令中創(chuàng)建匿名控制器的強大能力。這種能力可以將動態(tài)的對象創(chuàng)建成為控制器,并且這個對象是隔離的、易于測試的。
例如,可以在指令中創(chuàng)建匿名控制器,如下所示:

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

require(字符串或數組)

require參數可以被設置為字符串或數組,字符串代表另外一個指令的名字。require會將控制器注入到其值所指定的指令中,并作為當前指令的鏈接函數的第四個參數。
??字符串或數組元素的值是會在當前指令的作用域中使用的指令名稱。
??scope會影響指令作用域的指向,是一個隔離作用域,一個有依賴的作用域或者完全沒有作用域。在任何情況下,AngularJS編譯器在查找子控制器時都會參考當前指令的模板。
??如果不使用^前綴,指令只會在自身的元素上查找控制器。

//...
restrict: 'EA',
require: 'ngModel'
//...

指令定義只會查找定義在指令作當前用域中的ng-model=""

<!-- 指令會在本地作用域查找ng-model -->
<div my-directive ng-model="object"></div>

require參數的值可以用下面的前綴進行修飾,這會改變查找控制器時的行為:
?
如果在當前指令中沒有找到所需要的控制器,會將null作為傳給link函數的第四個參數。
^
如果添加了^前綴,指令會在上游的指令鏈中查找require參數所指定的控制器。
?^
將前面兩個選項的行為組合起來,我們可選擇地加載需要的指令并在父指令鏈中進行查找。
沒有前綴
如果沒有前綴,指令將會在自身所提供的控制器中進行查找,如果沒有找到任何控制器(或具有指定名字的指令)就拋出一個錯誤。

AngularJS 的生命周期

在AngularJS應用起動前,它們以HTML文本的形式保存在文本編輯器中。應用啟動后會進行編譯和鏈接,作用域會同HTML進行綁定,應用可以對用戶在HTML中進行的操作進行實時響應。
在這個過程中總共有兩個主要階段。

編譯階段

第一個階段是編譯階段。在編譯階段,AngularJS會遍歷整個HTML文檔并根據JavaScript中的指令定義來處理頁面上聲明的指令。
??每一個指令的模板中都可能含有另外一個指令,另外一個指令也可能會有自己的模板。當AngularJS調用HTML文檔根部的指令時,會遍歷其中所有的模板,模板中也可能包含帶有模板的指令。
??模板樹可能又大又深,但有一點需要注意,盡管元素可以被多個指令所支持或修飾,這些指令本身的模板中也可以包含其他指令,但只有屬于最高優(yōu)先級指令的模板會被解析并添加到模板樹中。這里有一個建議,就是將包含模板的指令和添加行為的指令分離開來。如果一個元素已經有一個含有模板的指令了,永遠不要對其用另一個指令進行修飾。只有具有最高優(yōu)先級的指令中的模板會被編譯。
??一旦對指令和其中的子模板進行遍歷或編譯,編譯后的模板會返回一個叫做模板函數的函數。我們有機會在指令的模板函數被返回前,對編譯后的DOM樹進行修改。
??在這個時間點DOM樹還沒有進行數據綁定,意味著如果此時對DOM樹進行操作只會有很少的性能開銷。基于此點,ng-repeatng-transclude等內置指令會在這個時候,也就是還未與任何作用域數據進行綁定時對DOM進行操作。
ng-repeat為例,它會遍歷指定的數組或對象,在數據綁定之前構建出對應的DOM結構。
??如果我們用ng-repeat來創(chuàng)建無序列表,其中的每一個<li>都會被ng-click指令所修飾,這個過程會使得性能比手動創(chuàng)建列表要快得多,尤其是列表中含有上百個元素時。與克隆<li>元素,再將其與數據進行鏈接,然后對每個元素都循環(huán)進行此操作的過程不同,我們僅需要先將無需列表構造出來,然后將新的DOM(編譯后的DOM)傳遞給指令生命周期中的下一個階段,即鏈接階段。
??一個指令的表現一旦編譯完成,馬上就可以通過編譯函數對其進行訪問,編譯函數的簽名包含有訪問指令聲明所在的元素(tElemente)及該元素其他屬性(tAttrs)的方法。這個編譯函數返回前面提到的模板函數,其中含有完整的解析樹。
??這里的重點是,由于每個指令都可以有自己的模板和編譯函數,每個模板返回的也都是自己的模板函數。鏈條頂部的指令會將內部子指令的模板合并在一起成為一個模板函數并返回,但在樹的內部,只能通過模板函數訪問其所處的分支。
??最后,模板函數被傳遞給編譯后的DOM樹中每個指令定義規(guī)則中指定的鏈接函數。

compile(對象或函數)

compile選項可以返回一個對象或函數。
??compile選項本身并不會被頻繁使用,但是link函數則會被經常使用。本質上,當我們設置了link選項,實際上是創(chuàng)建了一個postLink()鏈接函數,以便compile()函數可以定義鏈接函數。
??通常情況下,如果設置了compile函數,說明我們希望在指令和實時數據被放到DOM中之前進行DOM操作,在這個函數中進行諸如添加和刪除節(jié)點等DOM操作是安全的。
??compilelink選項是互斥的。如果同時設置了這兩個選項,那么會把compile所返回的函數當作鏈接函數,而link選項本身則會被忽略。

// ...
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) {
        // 連接函數
    };
}
//...

如果模板被克隆過,那么模板實例和鏈接實例可能是不同的對象。因此在編譯函數內部,我們只能轉換那些可以被安全操作的克隆DOM節(jié)點。不要進行DOM事件監(jiān)聽器的注冊:這個操作應該在鏈接函數中完成。
??編譯函數負責對模板DOM進行轉換。
??鏈接函數負責將作用域和DOM進行鏈接。在作用域同DOM鏈接之前可以手動操作DOM。在實踐中,編寫自定義指令時這種操作是非常罕見的,但有幾個內置指令提供了這樣的功能。

鏈接

link函數創(chuàng)建可以操作DOM的指令。
??鏈接函數是可選的。如果定義了編譯函數,它會返回鏈接函數,因此當兩個函數都定義了時,編譯函數會重載鏈接函數。如果我們的指令很簡單,并且不需要額外的設置,可以從工廠函數(回調函數)返回一個函數來代替對象。如果這樣做了,這個函數就是鏈接函數。
??下面兩種定義指令的方式在功能上是完全一樣的:

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            pre: function(tElement, tAttrs, transclude) {
                // 在子元素被鏈接之前執(zhí)行
                // 在這里進行Don轉換不安全
                // 之后調用'lihk'函數將無法定位要鏈接的元素
            },
            post: function(scope, iElement, iAttrs, controller) {
                // 在子元素被鏈接之后執(zhí)行
                // 如果在這里省略掉編譯選項
                //在這里執(zhí)行DOM轉換和鏈接函數一樣安全嗎
            }
        };
    });
angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            link: function(scope, ele, attrs) {
                return {
                    pre: function(tElement, tAttrs, transclude) {
                        // 在子元素被鏈接之前執(zhí)行
                        // 在這里進行Don轉換不安全
                        // 之后調用'lihk'h函數將無法定位要鏈接的元素
                    },
                    post: function(scope, iElement, iAttrs, controller) {
                        // 在子元素被鏈接之后執(zhí)行
                        // 如果在這里省略掉編譯選項
                        //在這里執(zhí)行DOM轉換和鏈接函數一樣安全嗎
                    }
                }
            }
        }
    });

當定義了編譯函數來取代鏈接函數時,鏈接函數是我們能提供給返回對象的第二個方法,也就是postLink函數。本質上講,這個事實正說明了鏈接函數的作用。它會在模板編譯并同作用域進行鏈接后被調用,因此它負責設置事件監(jiān)聽器,監(jiān)視數據變化和實時的操作DOM。
??link函數對綁定了實時數據的DOM具有控制能力,因此需要考慮性能問題。
鏈接函數的簽名如下:

link: function(scope, element, attrs) {
// 在這里操作DOM
}

如果指令定義中有require選項,函數簽名中會有第四個參數,代表控制器或者所依賴的指令的控制器。

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

如果require選項提供了一個指令數組,第四個參數會是一個由每個指令所對應的控制器組成的數組。
下面看一下鏈接函數中的參數:

  • scope
    指令用來在其內部注冊監(jiān)聽器的作用域。
  • iElement
    iElement參數代表實例元素,指使用此指令的元素。在postLink函數中我們應該只操作此元素的子元素,因為子元素已經被鏈接過了。
  • iAttrs
    iAttrs參數代表實例屬性,是一個由定義在元素上的屬性組成的標準化列表,可以在所有指令的鏈接函數間共享。會以JavaScript對象的形式進行傳遞。
  • controller
    controller參數指向require選項定義的控制器。如果沒有設置require選項,那么controller參數的值為undefined
    控制器在所有的指令間共享,因此指令可以將控制器當作通信通道(公共API)。如果設置了多個require,那么這個參數會是一個由控制器實例組成的數組,而不只是一個單獨的控制器。

ngModel

ngModel是一個用法特殊的指令,它提供更底層的API來處理控制器內的數據。當我們在指令中使用ngModel時能夠訪問一個特殊的API,這個API用來處理數據綁定、驗證、CSS更新等不實際操作DOM的事情。
??ngModel控制器會隨ngModel被一直注入到指令中,其中包含了一些方法。為了訪問ngModelController必須使用require設置:

angular.module('myApp')
    .directive('myDirective',function(){
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                // 現在我們的指令中已經有ngModelController的一個實例
            }
        };
    });

如果不設置require選項,ngModelController就不會被注入到指令中。
??注意,這個指令沒有隔離作用域。如果給這個指令設置隔離作用域,將導致內部ngModel無法更新外部ngModel的對應值:AngularJS會在本地作用域以外查詢值。
??為了設置作用域中的視圖值,需要調用ngModel.$setViewValue()函數。ngModel.$setViewValue()函數可以接受一個參數。
value(字符串):value參數是我們想要賦值給ngModel實例的實際值。這個方法會更新控制器上本地的$viewValue,然后將值傳遞給每一個$parser函數。
??當值被解析,且$parser流水線中所有的函數都調用完成后,值會被賦給$modelValue屬性,并且傳遞給指令中ng-model屬性提供的表達式。
??最后,所有步驟都完成后,$viewChangeListeners中所有的監(jiān)聽器都會被調用。
??注意,單獨調用$setViewValue()不會喚起一個新的digest循環(huán),因此如果想更新指令,需要在設置$viewValue后手動觸發(fā)digest
??$setViewValue()方法適合于在自定義指令中監(jiān)聽自定義事件,我們會希望在回調時設置$viewValue并執(zhí)行digest循環(huán)。

angular.module('myApp')
    .directive('myDirective', function() {
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                $(function() {
                    ele.datepicker({
                        onSelect: function(date) {
                            // 設置視圖和調用apply
                            scope.$apply(function() {
                                ngModel.$setViewValue(date);
                            });
                        }
                    });
                });
            }
        };
    });

自定義渲染

在控制器中定義$render方法可以定義視圖具體的渲染方式。這個方法會在$parser流水線完成后被調用。
由于這個方法會破壞AngularJS的標準工作方式,因此一定要謹慎使用:

angular.module('myApp')
    .directive('myDirective', function() {
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                ngModel.$render = function() {
                    element.html(ngModel.$viewValue() || 'None');
                };
            }
        };
    });

屬性

ngModelController中有幾個屬性可以用來檢查甚至修改視圖。

  1. $viewValue
    $viewValue屬性保存著更新視圖所需的實際字符串。
  2. $modelValue
    $modelValue由數據模型持有。$modelValue$viewValue可能是不同的,取決于$parser流水線是否對其進行了操作。
  3. $parsers
    $parsers的值是一個由函數組成的數組,其中的函數會以流水線的形式被逐一調用。ngModel從DOM中讀取的值會被傳入$parsers中的函數,并依次被其中的解析器處理。
  4. $formatters
    $formatters的值是一個由函數組成的數組,其中的函數會以流水線的形式在數據模型的值發(fā)生變化時被逐一調用。它和$parser流水線互不影響,用來對值進行格式化和轉換,以便在綁定了這個值的控件中顯示。
  5. $viewChangeListeners
    $viewChangeListeners的值是一個由函數組成的數組,其中的函數會以流水線的形式在視圖中的值發(fā)生變化時被逐一調用。通過$viewChangeListeners,可以在無需使用$watch的情況下實現類似的行為。由于返回值會被忽略,因此這些函數不需要返回值。
  6. $error
    $error對象中保存著沒有通過驗證的驗證器名稱以及對應的錯誤信息。
  7. $pristine
    $pristine的值是布爾型的,可以告訴我們用戶是否對控件進行了修改。
  8. $dirty
    $dirty的值和$pristine相反,可以告訴我們用戶是否和控件進行過交互。
  9. $valid
    $valid值可以告訴我們當前的控件中是否有錯誤。當有錯誤時值為false,沒有錯誤時值為true。
  10. $invalid
    $invalid值可以告訴我們當前控件中是否存在至少一個錯誤,它的值和$valid相反。

自定義驗證

要驗證username在數據庫中是否合法,可以實現一個指令,用來在表單發(fā)生變化時發(fā)送Ajax請求:

angular.module('validationExample', [])
    .directive('ensureUnique',function($http) {
        return {
            require: 'ngModel',
            link: function(scope, ele, attrs, c) {
                scope.$watch(attrs.ngModel, function() {
                    $http({
                        method: 'POST',
                        url: '/api/check/' + attrs.ensureUnique,
                        data: {field: attrs.ensureUnique, valud:scope.ngModel}
                    }).success(function(data,status,headers,cfg) {
                        c.$setValidity('unique', data.isUnique);
                    }).error(function(data,status,headers,cfg) {
                        c.$setValidity('unique', false);
                    });
                });
            }
        };
    });

<input type="text"
    placeholder="Desired username"
    name="username"
    ng-model="signup.username"
    ng-minlength="3"
    ng-maxlength="20"
    ensure-unique="username" required />

在這個自定義驗證中,每當ngModel中對應的字段發(fā)生變化就會向服務器發(fā)送請求,以檢查用戶名是否是唯一的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容