指令(Directives)是所有AngularJS應用最重要的部分。儘管AngularJS已經提供了非常豐富的指令,但還是經常需要建立應用特定的指令。這篇教程會為你講述如何自定義指令,以及介紹如何在實際專案中使用。在這篇文章的最後(第二部分),我會指導你如何使用Angular指令來建立一個簡單的記事本應用。
概述
一個指令用來引入新的HTML語法。指令是DOM元素上的標記,使元素擁有特定的行為。舉例來說,靜態的HTML不知道如何來建立和展現一個日期選擇器控制元件。讓HTML能識別這個語法,我們需要使用指令。指令通過某種方法來建立一個能夠支援日期選擇的元素。我們會循序漸進地介紹這是如何實現的。 如果你寫過AngularJS的應用,那麼你一定已經使用過指令,不管你有沒有意識到。你肯定已經用過簡單的指令,比如 ng-mode, ng-repeat, ng-show等。這些指令都賦予DOM元素特定的行為。例如,ng-repeat 重複特定的元素,ng-show 有條件地顯示一個元素。如果你想讓一個元素支援拖拽,你也需要建立一個指令來實現它。指令背後基本的想法很簡單。它通過對元素繫結事件監聽或者改變DOM而使HTML擁有真實的互動性。
jQuery視角
想象一下使用jQuery如何建立一個日期選擇器。首先,我們在HTML中新增一個普通的輸入框,然後通過jQuery呼叫 $(element).dataPicker() 來將它轉變成一個日期選擇器。但是,仔細想一下。當一個設計人員過來檢查HTML標記的時候,他/她能否立刻猜到這個欄位實際上表示的內容?這只是一個簡單的輸入框,或者一個日期選擇器?你需要檢視jQuery程式碼來確定這些。而Angular的方法是使用一個指令來擴充套件HTML。所以,一個日期選擇器的指令可以是下面的形式:
1 |
<input type="text" /> |
或者是這樣:
1 |
<input type="text" /> |
這種建立UI組建的方式更加直接和清晰。你可以輕易地通過檢視元素就明白這到底是什麼。
建立自定義指令:
一個Angular指令可以有以下的四種表現形式: 1. 一個新的HTML元素(<data-picker></data-picker>) 2. 元素的屬性(<input type=”text” data-picker/>) 3. CSS class(<input type=”text” class=”data-picker”/>) 4. 註釋(<!–directive:data-picker –>) 當然,我們可以控制我們的指令在HTML中的表現形式。下面我們來看一下AngularJS中的一個典型的指令的寫法。指令註冊的方式與 controller 一樣,但是它返回的是一個擁有指令配置屬性的簡單物件(指令定義物件) 。下面的程式碼是一個簡單的 Hello World 指令。
1 2 3 4 5 6 7 8 9 |
var app = angular.module('myapp', []); app.directive('helloWorld', function() { return { restrict: 'AE', replace: 'true', template: '<h3>Hello World!!</h3>' }; }); |
在上面的程式碼中,app.directive()方法在模組中註冊了一個新的指令。這個方法的第一個引數是這個指令的名字。第二個引數是一個返回指令定義物件的函式。如果你的指令依賴於其他的物件或者服務,比如 $rootScope, $http, 或者$compile,他們可以在這個時間被注入。這個指令在HTML中以一個元素使用,如下:
1 2 3 |
<hello-world/> //OR <hello:world/> |
或者,以一個屬性的方式使用:
1 2 3 |
<div hello-world></div> //OR <div hello:world/> |
如果你想要符合HTML5的規範,你可以在元素前面新增 x- 或者 data-的字首。所以下面的標記也會匹配 helloWorld 指令:
1 2 3 |
<div data-hello-world></div> //OR <div x-hello-world></div> |
注意: 在匹配指令的時候,Angular會在元素或者屬性的名字中剔除 x- 或者 data- 字首。 然後將 – 或者 : 連線的字串轉換成駝峰(camelCase)表現形式,然後再與註冊過的指令進行匹配。這是為什麼,我們在HTML中以 hello-world 的方式使用 helloWorld 指令。其實,這跟HTML對標籤和屬性不區分大小寫有關。 儘管上面的指令僅僅實現了靜態文字的顯示,但是這裡還是有一些有趣的點值得我們去挖掘。我們在指令定義過程中使用了三個屬性來配置指令。我們來一一介紹他們的作用。
- restrict – 這個屬性用來指定指令在HTML中如何使用(還記得之前說的,指令的四種表示方式嗎)。在上面的例子中,我們使用了 ‘AE’。所以這個指令可以被當作新的HTML元素或者屬性來使用。如果要允許指令被當作class來使用,我們將 restrict 設定成 ‘AEC’。
- template – 這個屬性規定了指令被Angular編譯和連結(link)後生成的HTML標記。這個屬性值不一定要是簡單的字串。template 可以非常複雜,而且經常包含其他的指令,以及表示式({{ }})等。更多的情況下你可能會見到 templateUrl, 而不是 template。所以,理想情況下,你應該將模板放到一個特定的HTML檔案中,然後將 templateUrl 屬性指向它。
- replace – 這個屬性指明生成的HTML內容是否會替換掉定義此指令的HTML元素。在我們的例子中,我們用 <hello-world></hello-world>的方式使用我們的指令,並且將 replace 設定成 true。所以,在指令被編譯之後,生成的模板內容替換掉了 <hello-world></hello-world>。最終的輸出是 <h3>Hello World!!</h3>。如果你將 replace 設定成 false,也就是預設值,那麼生成的模板會被插入到定義指令的元素中。
開啟這個 plunker,在”Hello World!!”右鍵檢查元素內容,來更形象地明白這些。
Link函式和Scope
指令生成出的模板其實沒有太多意義,除非它在特定的scope下編譯。預設情況下,指令並不會建立新的子scope。更多的,它使用父scope。也就是說,如果指令存在於一個controller下,它就會使用這個controller的scope。 如何運用scope,我們要用到一個叫做 link 的函式。它由指令定義物件中的link屬性配置。讓我們來改變一下我們的 helloWorld 指令,當使用者在一個輸入框中輸入一種顏色的名稱時,Hello World 文字的背景色自動發生變化。同時,當使用者在 Hello World 文字上點選時,背景色變回白色。 相應的HTML標記如下:
1 2 3 4 |
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color" /> <hello-world/> </body> |
修改後的 helloWorld 指令如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app.directive('helloWorld', function() { return { restrict: 'AE', replace: true, template: '<p style="background-color:{{color}}">Hello World', link: function(scope, elem, attrs) { elem.bind('click', function() { elem.css('background-color', 'white'); scope.$apply(function() { scope.color = "white"; }); }); elem.bind('mouseover', function() { elem.css('cursor', 'pointer'); }); } }; }); |
我們注意到指令定義中的 link 函式。 它有三個引數:
- scope – 指令的scope。在我們的例子中,指令的scope就是父controller的scope。
- elem – 指令的jQLite(jQuery的子集)包裝DOM元素。如果你在引入AngularJS之前引入了jQuery,那麼這個元素就是jQuery元素,而不是jQLite元素。由於這個元素已經被jQuery/jQLite包裝了,所以我們就在進行DOM操作的時候就不需要再使用 $()來進行包裝。
- attr – 一個包含了指令所在元素的屬性的標準化的引數物件。舉個例子,你給一個HTML元素新增了一些屬性:,那麼可以在 link 函式中通過 attrs.someAttribute 來使用它。
link函式主要用來為DOM元素新增事件監聽、監視模型屬性變化、以及更新DOM。在上面的指令程式碼片段中,我們新增了兩個事件, click,和 mouseover。click 處理函式用來重置 <p> 的背景色,而 mouseover 處理函式改變滑鼠為 pointer。在模板中有一個表示式 {{color}},當父scope中的 color 發生變化時,它用來改變 Hello World 文字的背景色。 這個 plunker 演示了這些概念。
compile函式
compile 函式在 link 函式被執行之前用來做一些DOM改造。它接收下面的引數:
- tElement – 指令所在的元素
- attrs – 元素上賦予的引數的標準化列表
要注意的是 compile 函式不能訪問 scope,並且必須返回一個 link 函式。但是如果沒有設定 compile 函式,你可以正常地配置 link 函式,(有了compile,就不能用link,link函式由compile返回)。compile函式可以寫成如下的形式:
1 2 3 4 5 6 7 8 9 10 |
app.directive('test', function() { return { compile: function(tElem,attrs) { //do optional DOM transformation here return function(scope,elem,attrs) { //linking function here }; } }; }); |
大多數的情況下,你只需要使用 link 函式。這是因為大部分的指令只需要考慮註冊事件監聽、監視模型、以及更新DOM等,這些都可以在 link 函式中完成。 但是對於像 ng-repeat 之類的指令,需要克隆和重複 DOM 元素多次,在 link 函式執行之前由 compile 函式來完成。這就帶來了一個問題,為什麼我們需要兩個分開的函式來完成生成過程,為什麼不能只使用一個?要回答好這個問題,我們需要理解指令在Angular中是如何被編譯的!
指令是如何被編譯的
當應用引導啟動的時候,Angular開始使用 $compile 服務遍歷DOM元素。這個服務基於註冊過的指令在標記文字中搜尋指令。一旦所有的指令都被識別後,Angular執行他們的 compile 方法。如前面所講的,compile 方法返回一個 link 函式,被新增到稍後執行的 link 函式列表中。這被稱為編譯階段。如果一個指令需要被克隆很多次(比如 ng-repeat),compile函式只在編譯階段被執行一次,複製這些模板,但是link 函式會針對每個被複制的例項被執行。所以分開處理,讓我們在效能上有一定的提高。這也說明了為什麼在 compile 函式中不能訪問到scope物件。 在編譯階段之後,就開始了連結(linking)階段。在這個階段,所有收集的 link 函式將被一一執行。指令創造出來的模板會在正確的scope下被解析和處理,然後返回具有事件響應的真實的DOM節點。
改變指令的Scope
預設情況下,指令獲取它父節點的controller的scope。但這並不適用於所有情況。如果將父controller的scope暴露給指令,那麼他們可以隨意地修改 scope 的屬性。在某些情況下,你的指令希望能夠新增一些僅限內部使用的屬性和方法。如果我們在父的scope中新增,會汙染父scope。 其實我們還有兩種選擇:
- 一個子scope – 這個scope原型繼承子父scope。
- 一個隔離的scope – 一個孤立存在不繼承自父scope的scope。
這樣的scope可以通過指令定義物件中 scope 屬性來配置。下面的程式碼片段是一個例子:
1 2 3 4 5 6 7 8 |
app.directive('helloWorld', function() { return { scope: true, // use a child scope that inherits from parent restrict: 'AE', replace: 'true', template: '<h3>Hello World!!</h3>' }; }); |
上面的程式碼,讓Angular給指令建立一個繼承自父socpe的新的子scope。 另外一個選擇,隔離的scope:
1 2 3 4 5 6 7 8 |
app.directive('helloWorld', function() { return { scope: {}, // use a new isolated scope restrict: 'AE', replace: 'true', template: '<h3>Hello World!!</h3>' }; }); |
這個指令使用了一個隔離的scope。隔離的scope在我們想要建立可重用的指令的時候是非常有好處的。通過使用隔離的scope,我們能夠保證我們的指令是自包含的,可以被很容易的插入到HTML應用中。 它內部不能訪問父的scope,所保證了父scope不被汙染。 在我們的 helloWorld 指令例子中,如果我們將 scope 設定成 {},那麼上面的程式碼將不會工作。 它會建立一個新的隔離的scope,那麼相應的表示式 {{color}} 會指向到這個新的scope中,它的值將是 undefined. 使用隔離的scope並不意味著我們完全不能訪問父scope的屬性。其實有一些技術可以允許我們訪問父scope的屬性,甚至監視他們的變化。我們會在指令這個系列的第二部分中討論這些技術,以及一些更高階的概念,比如 Controller 函式。 第二部分也會和你一起使用Angular指令建立一個較為豐富的記事本應用。 所以,請保持關注。