指令定義
對于指令疾嗅,可以把它簡單的理解成在特定DOM元素上運(yùn)行的函數(shù)黍衙,指令可以擴(kuò)展這個元素的功能。
??我們可以自己創(chuàng)造新的指令料扰。directive()
這個方法是用來定義指令的:
angular.module('myApp', [])
.directive('myDirective', function ($timeout, UserDefinedService) {
// 指令定義放在這里
});
directive()
方法可以接受兩個參數(shù):
- name(字符串)
指令的名字凭豪,用來在視圖中引用特定的指令。要用駝峰式寫法晒杈。 - factory_function (函數(shù))
這個函數(shù)返回一個對象嫂伞,其中定義了指令的全部行為。$compile
服務(wù)利用這個方法返回的對象,在DOM調(diào)用指令時來構(gòu)造指令的行為帖努。
angular.application('myApp', [])
.directive('myDirective', function() {
return {
// 通過設(shè)置項(xiàng)來定義指令撰豺,在這里進(jìn)行覆寫
};
});
我們也可以返回一個函數(shù)代替對象來定義指令,但是像上面的例子一樣拼余,通過對象來定義是最佳的方式污桦。當(dāng)返回一個函數(shù)時,這個函數(shù)通常被稱作鏈接傳遞函數(shù)匙监,利用它我們可以定義指令的鏈接功能凡橱。由于返回函數(shù)而不是對象會限制定義指令時的自由度,因此只在構(gòu)造簡單的指令時才比較有用亭姥。
??當(dāng)AngularJS啟動應(yīng)用時稼钩,它會把第一個參數(shù)當(dāng)作一個字符串,并以此字符串為名來注冊第二個參數(shù)返回的對象达罗。AngularJS編譯器會解析主HTML的DOM中的元素坝撑、屬性、注釋和CSS類名中使用了這個名字的地方氮块,并在這些地方引用對應(yīng)的指令。當(dāng)它找到某個已知的指令時诡宗,就會在頁面中插入指令所對應(yīng)的DOM元素滔蝉。
<div my-directive></div>
指令的工廠函數(shù)只會在編譯器第一次匹配到這個指令時調(diào)用一次。和controller
函數(shù)類似塔沃,我們通過$injetor.invoke
來調(diào)用指令的工廠函數(shù)蝠引。當(dāng)AngularJS在DOM中遇到具名的指令時,會去匹配已經(jīng)注冊過的指令蛀柴,并通過名字在注冊過的對象中查找螃概。此時,就開始了一個指令的生命周期鸽疾,指令的生命周期開始于$compile
方法并結(jié)束于link
方法吊洼。
??定義一個指令時可以使用的全部設(shè)置選項(xiàng):
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: // 返回一個對象或連接函數(shù),如下所示:
function(tElement, tAttrs, transclude) {
return {
pre: function(scope, iElement, iAttrs, controller) { ... },
post: function(scope, iElement, iAttrs, controller) { ... }
}
// 或者
return function postLink(...) { ... }
}
};
});
restrict(字符串)
restrict
是一個可選參數(shù)制肮。它告訴AngularJS這個指令在DOM中可以何種形式被聲明冒窍。默認(rèn)restrict
的值是A,即以屬性的形式來進(jìn)行聲明豺鼻。
可選值如下:
- E(元素)
<my-directive></my-directive>
- A(屬性综液,默認(rèn)值)
<div my-directive="expression"></div>
- C(類名)
<div class="my-directive:expression;"></div>
- M(注釋)
<--directive:my-directive expression-->
這些選項(xiàng)可以單獨(dú)使用,也可以混合在一起使用:
angular.module('myDirective', function(){
return {
restrict: 'EA' // 輸入元素或?qū)傩? };
});
上面的配置可以同時用屬性或元素的方式來聲明指令:
<-- 作為一個屬性 -->
<div my-directive></div>
<-- 或者作為一個元素 -->
<my-directive></my-directive>
屬性是用來聲明指令最常用的方式儒飒,因?yàn)樗茉诎ɡ习姹镜腎E瀏覽器在內(nèi)的所有瀏覽器中正常工作谬莹,并且不需要在文檔頭部注冊新的標(biāo)簽。
優(yōu)先級priority(數(shù)值型)
優(yōu)先級參數(shù)可以被設(shè)置為一個數(shù)值。大多數(shù)指令會忽略這個參數(shù)附帽,使用默認(rèn)值0埠戳,但也有些場景設(shè)置高優(yōu)先級是非常重要的。
??如果一個元素上具有兩個優(yōu)先級相同的指令士葫,聲明在前面的那個會被優(yōu)先調(diào)用乞而。具有更高優(yōu)先級的指令總是優(yōu)先運(yùn)行。
??ngRepeat
是所有內(nèi)置指令中優(yōu)先級最高的慢显,它總是在其他指令之前運(yùn)行爪模。
terminal(布爾型)
這個參數(shù)用來告訴AngularJS停止運(yùn)行當(dāng)前元素上比本指令優(yōu)先級低的指令。但同當(dāng)前指令優(yōu)先級相同的指令還是會被執(zhí)行荚藻。
??如果元素上某個指令設(shè)置了terminal
參數(shù)并具有較高的優(yōu)先級屋灌,就不要再用其他低優(yōu)先級的指令對其進(jìn)行修飾了,因?yàn)椴粫徽{(diào)用应狱。但是具有相同優(yōu)先級的指令還是會被繼續(xù)調(diào)用共郭。
??使用了terminal
參數(shù)的例子是ngView
和ngIf
。ngIf
的優(yōu)先級略高于ngView
疾呻,如果ngIf
的表達(dá)式值為true除嘹,ngView
就可以被正常執(zhí)行,但如果ngIf
表達(dá)式的值為false岸蜗,由于ngView
的優(yōu)先級較低就不會被執(zhí)行尉咕。
template(字符串或函數(shù))
template
參數(shù)是可選的,必須被設(shè)置為以下兩種形式之一:
- 一段HTML文本璃岳;
- 一個可以接受兩個參數(shù)的函數(shù)年缎,參數(shù)為
tElement
和tAttrs
,并返回一個代表模板的字符串铃慷。tElement
和tAttrs
中的t代表template
单芜,是相對于instance
的。
AngularJS會同處理HTML一樣處理模板字符串犁柜。模板中可以通過大括號標(biāo)記來訪問作用域洲鸠,例如{{ expression }}。
如果模板字符串中含有多個DOM元素馋缅,或者只由一個單獨(dú)的文本節(jié)點(diǎn)構(gòu)成坛怪,那它必須被包含在一個父元素內(nèi)。換句話說股囊,必須存在一個根DOM元素:
template: '\
<div> <-- single root element -->\
<a >Click me</a>\
<h1>When using two elements, wrap them in a parent element</h1>\
</div>\
另外袜匿,注意每一行末尾的反斜線,這樣AngularJS才能正確解析多行字符串稚疹。在實(shí)際生產(chǎn)中居灯,更好的選擇是使用templateUrl
參數(shù)引用外部模板祭务。
templateUrl(字符串或函數(shù))
templateUrl
是可選的參數(shù),可以是以下類型:
- 一個代表外部HTML文件路徑的字符串怪嫌;
- 一個可以接受兩個參數(shù)的函數(shù)义锥,參數(shù)為
tElement
和tAttrs
,并返回一個外部HTML文件路徑的字符串岩灭。
默認(rèn)情況下拌倍,調(diào)用指令時會在后臺通過Ajax來請求HTML模板文件。有兩件事情需要知道噪径。
- 在本地開發(fā)時柱恤,需要在后臺運(yùn)行一個本地服務(wù)器,用以從文件系統(tǒng)加載HTML模板找爱,否則會導(dǎo)致Cross Origin Request Script(CORS)錯誤梗顺。
- 模板加載是異步的,意味著編譯和鏈接要暫停车摄,等待模板加載完成寺谤。
模板加載后,AngularJS會將它默認(rèn)緩存到$templateCache
服務(wù)中吮播。在實(shí)際生產(chǎn)中变屁,可以提前將模板緩存到一個定義模板的JS文件中,這樣就不需要通過XHR來加載模板了意狠。
replace(布爾型)
replace
是一個可選參數(shù)粟关,如果設(shè)置了這個參數(shù),值必須為true摄职,因?yàn)槟J(rèn)值為false誊役。默認(rèn)值意味著模板會被當(dāng)作子元素插入到調(diào)用此指令的元素內(nèi)部:
<div some-directive></div>
.directive('someDirective', function() {
return {
template: '<div>some stuff here<div>'
};
});
<!--調(diào)用指令之后的結(jié)果如下(默認(rèn)replace為false時的情況):-->
<div some-directive>
<div>some stuff here<div>
</div>
如果replace
被設(shè)置為了true:
.directive('someDirective', function() {
return {
replace: true
template: '<div>some stuff here<div>'
};
});
<!--指令調(diào)用后的結(jié)果將是:-->
<div>some stuff here<div>
scope參數(shù)(布爾型或?qū)ο螅?/h4>
scope
參數(shù)是可選的获列,可以被設(shè)置為true或一個對象谷市。默認(rèn)值是false。當(dāng)scope
設(shè)置為true時击孩,會從父作用域繼承并創(chuàng)建一個新的作用域?qū)ο蟆?br>
??如果一個元素上有多個指令使用了隔離作用域迫悠,其中只有一個可以生效。只有指令模板中的根元素可以獲得一個新的作用域巩梢。因此创泄,對于這些對象來說scope
默認(rèn)被設(shè)置為true。
??內(nèi)置指令ng-controller
的作用括蝠,就是從父級作用域繼承并創(chuàng)建一個新的子作用域鞠抑。它會創(chuàng)建一個新的從父作用域繼承而來的子作用域。
??為了進(jìn)一步證明作用域的繼承機(jī)制是向下而非向上進(jìn)行的忌警,下面的例子展示的是{{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
屬性設(shè)置為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)建可復(fù)用的組件,組件可以在未知上下文中使用箕速,并且可以避免污染所處的外部作用域或不經(jīng)意地污染內(nèi)部作用域酪碘。
??創(chuàng)建具有隔離作用域的指令需要將scope
屬性設(shè)置為一個空對象{}
。如果這樣做了盐茎,指令的模板就無法訪問外部作用域了:
<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
設(shè)置了一個高優(yōu)先級。由于ngInit
指令會以非零的優(yōu)先級運(yùn)行字柠,這個例子將會優(yōu)先運(yùn)行ngInit
指令探越,然后才是我們定義的指定,并且這個myProperty
在$scope
對象中是有效的募谎。
??示例代碼的效果與將scope
設(shè)置為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
};
});
綁定策略
使用無數(shù)據(jù)的隔離作用域并不常見数冬。AngularJS提供了幾種方法能夠?qū)⒅噶顑?nèi)部的隔離作用域节槐,同指令外部的作用域進(jìn)行數(shù)據(jù)綁定。
??為了讓新的指令作用域可以訪問當(dāng)前本地作用域中的變量拐纱,需要使用下面三種別名中的一種铜异。
??本地作用域?qū)傩裕菏褂聾符號將本地作用域同DOM屬性的值進(jìn)行綁定。指令內(nèi)部作用域可以使用外部作用域的變量:
- @ (or @attr)
現(xiàn)在秸架,可以在指令中使用綁定的字符串了揍庄。
雙向綁定:通過=可以將本地作用域上的屬性同父級作用域上的屬性進(jìn)行雙向的數(shù)據(jù)綁定。就像普通的數(shù)據(jù)綁定一樣东抹,本地屬性會反映出父數(shù)據(jù)模型中所發(fā)生的改變蚂子。 - = (or =attr)
父級作用域綁定 通過&符號可以對父級作用域進(jìn)行綁定,以便在其中運(yùn)行函數(shù)缭黔。意味著對這個值進(jìn)行設(shè)置時會生成一個指向父級作用域的包裝函數(shù)食茎。
要使調(diào)用帶有一個參數(shù)的父方法,我們需要傳遞一個對象馏谨,這個對象的鍵是參數(shù)的名稱别渔,值是要傳遞給參數(shù)的內(nèi)容。 - & (or &attr)
例如惧互,假設(shè)我們在開發(fā)一個電子郵件客戶端哎媚,并且要創(chuàng)建一個電子郵件的文本輸入框:
<input type="text" ng-model="to"/>
<!-- 調(diào)用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)" from-name="ari@fullstack.io" ></div>
在指令中做如下設(shè)置以訪問這些內(nèi)容:
scope: {
ngModel: '=', // 將ngModel同指定對象綁定
onSend: '&', // 將引用傳遞給這個方法
fromName: '@' // 儲存與fromName相關(guān)聯(lián)的字符串
}
transclude
transclude
是一個可選的參數(shù)。如果設(shè)置了喊儡,其值必須為true拨与,它的默認(rèn)值是false。
??嵌入通常用來創(chuàng)建可復(fù)用的組件艾猜,典型的例子是模態(tài)對話框或?qū)Ш綑凇?br>
??我們可以將整個模板买喧,包括其中的指令通過嵌入全部傳入一個指令中攀甚。這樣做可以將任意內(nèi)容和作用域傳遞給指令。transclude
參數(shù)就是用來實(shí)現(xiàn)這個目的的岗喉,指令的內(nèi)部可以訪問外部指令的作用域秋度,并且模板也可以訪問外部的作用域?qū)ο蟆?br>
??為了將作用域傳遞進(jìn)去,scope
參數(shù)的值必須通過{}
或true設(shè)置成隔離作用域钱床。如果沒有設(shè)置scope
參數(shù)荚斯,那么指令內(nèi)部的作用域?qū)⒈辉O(shè)置為傳入模板的作用域。
??只有當(dāng)你希望創(chuàng)建一個可以包含任意內(nèi)容的指令時查牌,才使用transclude: true
事期。
??嵌入允許指令的使用者方便地提供自己的HTML模板,其中可以包含獨(dú)特的狀態(tài)和行為纸颜,并對指令的各方面進(jìn)行自定義兽泣。
??下面一起來實(shí)現(xiàn)個小例子,創(chuàng)建一個可以被自定義的可復(fù)用指令胁孙。
??例如唠倦,假設(shè)我們想創(chuàng)建一個包括標(biāo)題和少量HTML內(nèi)容的側(cè)邊欄,如下所示:
<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元素中獲取的內(nèi)容放到它發(fā)現(xiàn)ng-transclude
指令的地方稠鼻。
??借助transclusion
,我們可以將指令復(fù)用到第二個元素上狂票,而無須擔(dān)心樣式和布局的一致性問題候齿。
??例如,下面的代碼會產(chǎn)生兩個樣式完全一致的側(cè)邊欄闺属。
<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
參數(shù)慌盯,那么在控制器中就無法正常監(jiān)聽數(shù)據(jù)模型的變化了。這就是最佳實(shí)踐總是建議在鏈接函數(shù)里使用$watch
服務(wù)的原因掂器。
controller(字符串或函數(shù))
controller
參數(shù)可以是一個字符串或一個函數(shù)亚皂。當(dāng)設(shè)置為字符串時,會以字符串的值為名字唉匾,來查找注冊在應(yīng)用中的控制器的構(gòu)造函數(shù):
angular.module('myApp', [])
.directive('myDirective', function() {
restrict: 'A', // 始終需要
controller: 'SomeController'
})
// 應(yīng)用中其他的地方孕讳,可以是同一個文件或被index.html包含的另一個文件
angular.module('myApp')
.controller('SomeController', function($scope, $element, $attrs, $transclude) {
// 控制器邏輯放在這里
});
可以在指令內(nèi)部通過匿名構(gòu)造函數(shù)的方式來定義一個內(nèi)聯(lián)的控制器:
angular.module('myApp',[])
.directive('myDirective', function() {
restrict: 'A',
controller:
function($scope, $element, $attrs, $transclude) {
// 控制器邏輯放在這里
}
});
我們可以將任意可以被注入的AngularJS服務(wù)傳遞給控制器匠楚。例如巍膘,如果我們想要將$log
服務(wù)傳入控制器,只需簡單地將它注入到控制器中芋簿,便可以在指令中使用它了峡懈。
控制器中也有一些特殊的服務(wù)可以被注入到指令當(dāng)中。這些服務(wù)有:
- $scope
與指令元素相關(guān)聯(lián)的當(dāng)前作用域与斤。 - $element
當(dāng)前指令對應(yīng)的元素肪康。 - $attrs
由當(dāng)前元素的屬性組成的對象荚恶。例如,下面的元素:
<div id="aDiv"class="box"></div>
具有如下的屬性對象:
{
id: "aDiv",
class: "box"
}
- $transclude
嵌入鏈接函數(shù)會與對應(yīng)的嵌入作用域進(jìn)行預(yù)綁定磷支。
transclude鏈接函數(shù)是實(shí)際被執(zhí)行用來克隆元素和操作DOM的函數(shù)谒撼。
在控制器內(nèi)部操作DOM是和AngularJS風(fēng)格相悖的做法,但通過鏈接函數(shù)就可以實(shí)現(xiàn)這個需求雾狈。僅在compile
參數(shù)中使用transcludeFn
是推薦的做法廓潜。
??例如,我們想要通過指令來添加一個超鏈接標(biāo)簽善榛”绲埃可以在控制器內(nèi)的$transclude
函數(shù)中實(shí)現(xiàn),如下所示:
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函數(shù)可以進(jìn)行互換移盆〉吭海控制器主要是用來提供可在指令間復(fù)用的行為,但鏈接函數(shù)只能在當(dāng)前內(nèi)部指令中定義行為咒循,且無法在指令間復(fù)用据途。
??link函數(shù)可以將指令互相隔離開來,而controller則定義可復(fù)用的行為叙甸。
??由于指令可以require其他指令所使用的控制器昨凡,因此控制器常被用來放置在多個指令間共享的動作。
??如果我們希望將當(dāng)前指令的API暴露給其他指令使用蚁署,可以使用controller參數(shù)便脊,否則可以使用link來構(gòu)造當(dāng)前指令元素的功能性。如果我們使用了scope.$watch()或者想要與DOM元素做實(shí)時的交互光戈,使用鏈接會是更好的選擇哪痰。
??技術(shù)上講,$scope會在DOM元素被實(shí)際渲染之前傳入到控制器中久妆。在某些情況下晌杰,例如使用了嵌入,控制器中的作用域所反映的作用域可能與我們所期望的不一樣筷弦,這種情況下肋演,$scope對象無法保證可以被正常更新。
??當(dāng)想要同當(dāng)前屏幕上的作用域交互時烂琴,可以使用被傳入到link函數(shù)中的scope參數(shù)作儿。
controllerAs(字符串)
controllerAs
參數(shù)用來設(shè)置控制器的別名,可以以此為名來發(fā)布控制器锅铅,并且作用域可以訪問controllerAs
借浊。這樣就可以在視圖中引用控制器,甚至無需注入$scope
号醉。
??例如反症,創(chuàng)建一個MainController
辛块,然后不要注入$scope
,如下所示:
angular.module('myApp')
.controller('MainController', function() {
this.name = "Ari";
});
現(xiàn)在铅碍,在HTML中無需引用作用域就可以使用MainController
润绵。
<div ng-app ng-controller="MainController">
<input type="text" ng-model="main.name" />
<span>{{ main.name }}</span>
</div>
這個參數(shù)看起來好像沒什么大用,但它給了我們可以在路由和指令中創(chuàng)建匿名控制器的強(qiá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(字符串或數(shù)組)
require
參數(shù)可以被設(shè)置為字符串或數(shù)組爵嗅,字符串代表另外一個指令的名字娇澎。require
會將控制器注入到其值所指定的指令中,并作為當(dāng)前指令的鏈接函數(shù)的第四個參數(shù)睹晒。
??字符串或數(shù)組元素的值是會在當(dāng)前指令的作用域中使用的指令名稱趟庄。
??scope
會影響指令作用域的指向,是一個隔離作用域伪很,一個有依賴的作用域或者完全沒有作用域戚啥。在任何情況下,AngularJS編譯器在查找子控制器時都會參考當(dāng)前指令的模板锉试。
??如果不使用^前綴猫十,指令只會在自身的元素上查找控制器。
//...
restrict: 'EA',
require: 'ngModel'
//...
指令定義只會查找定義在指令作當(dāng)前用域中的ng-model=""
呆盖。
<!-- 指令會在本地作用域查找ng-model -->
<div my-directive ng-model="object"></div>
require參數(shù)的值可以用下面的前綴進(jìn)行修飾拖云,這會改變查找控制器時的行為:
?
如果在當(dāng)前指令中沒有找到所需要的控制器,會將null作為傳給link函數(shù)的第四個參數(shù)应又。
^
如果添加了^前綴宙项,指令會在上游的指令鏈中查找require
參數(shù)所指定的控制器。
?^
將前面兩個選項(xiàng)的行為組合起來株扛,我們可選擇地加載需要的指令并在父指令鏈中進(jìn)行查找尤筐。
沒有前綴
如果沒有前綴,指令將會在自身所提供的控制器中進(jìn)行查找洞就,如果沒有找到任何控制器(或具有指定名字的指令)就拋出一個錯誤盆繁。
AngularJS 的生命周期
在AngularJS應(yīng)用起動前,它們以HTML文本的形式保存在文本編輯器中奖磁。應(yīng)用啟動后會進(jìn)行編譯和鏈接改基,作用域會同HTML進(jìn)行綁定繁疤,應(yīng)用可以對用戶在HTML中進(jìn)行的操作進(jìn)行實(shí)時響應(yīng)咖为。
在這個過程中總共有兩個主要階段秕狰。
編譯階段
第一個階段是編譯階段。在編譯階段躁染,AngularJS會遍歷整個HTML文檔并根據(jù)JavaScript中的指令定義來處理頁面上聲明的指令鸣哀。
??每一個指令的模板中都可能含有另外一個指令,另外一個指令也可能會有自己的模板吞彤。當(dāng)AngularJS調(diào)用HTML文檔根部的指令時我衬,會遍歷其中所有的模板,模板中也可能包含帶有模板的指令饰恕。
??模板樹可能又大又深挠羔,但有一點(diǎn)需要注意,盡管元素可以被多個指令所支持或修飾埋嵌,這些指令本身的模板中也可以包含其他指令破加,但只有屬于最高優(yōu)先級指令的模板會被解析并添加到模板樹中。這里有一個建議雹嗦,就是將包含模板的指令和添加行為的指令分離開來范舀。如果一個元素已經(jīng)有一個含有模板的指令了,永遠(yuǎn)不要對其用另一個指令進(jìn)行修飾了罪。只有具有最高優(yōu)先級的指令中的模板會被編譯锭环。
??一旦對指令和其中的子模板進(jìn)行遍歷或編譯,編譯后的模板會返回一個叫做模板函數(shù)的函數(shù)泊藕。我們有機(jī)會在指令的模板函數(shù)被返回前辅辩,對編譯后的DOM樹進(jìn)行修改。
??在這個時間點(diǎn)DOM樹還沒有進(jìn)行數(shù)據(jù)綁定娃圆,意味著如果此時對DOM樹進(jìn)行操作只會有很少的性能開銷汽久。基于此點(diǎn)踊餐,ng-repeat
和ng-transclude
等內(nèi)置指令會在這個時候景醇,也就是還未與任何作用域數(shù)據(jù)進(jìn)行綁定時對DOM進(jìn)行操作。
以ng-repeat
為例吝岭,它會遍歷指定的數(shù)組或?qū)ο笕担跀?shù)據(jù)綁定之前構(gòu)建出對應(yīng)的DOM結(jié)構(gòu)。
??如果我們用ng-repeat
來創(chuàng)建無序列表窜管,其中的每一個<li>
都會被ng-click
指令所修飾散劫,這個過程會使得性能比手動創(chuàng)建列表要快得多,尤其是列表中含有上百個元素時幕帆。與克隆<li>
元素获搏,再將其與數(shù)據(jù)進(jìn)行鏈接,然后對每個元素都循環(huán)進(jìn)行此操作的過程不同失乾,我們僅需要先將無需列表構(gòu)造出來常熙,然后將新的DOM(編譯后的DOM)傳遞給指令生命周期中的下一個階段纬乍,即鏈接階段。
??一個指令的表現(xiàn)一旦編譯完成裸卫,馬上就可以通過編譯函數(shù)對其進(jìn)行訪問仿贬,編譯函數(shù)的簽名包含有訪問指令聲明所在的元素(tElemente
)及該元素其他屬性(tAttrs
)的方法。這個編譯函數(shù)返回前面提到的模板函數(shù)墓贿,其中含有完整的解析樹茧泪。
??這里的重點(diǎn)是,由于每個指令都可以有自己的模板和編譯函數(shù)聋袋,每個模板返回的也都是自己的模板函數(shù)队伟。鏈條頂部的指令會將內(nèi)部子指令的模板合并在一起成為一個模板函數(shù)并返回,但在樹的內(nèi)部幽勒,只能通過模板函數(shù)訪問其所處的分支缰泡。
??最后,模板函數(shù)被傳遞給編譯后的DOM樹中每個指令定義規(guī)則中指定的鏈接函數(shù)代嗤。
compile(對象或函數(shù))
compile
選項(xiàng)可以返回一個對象或函數(shù)棘钞。
??compile
選項(xiàng)本身并不會被頻繁使用,但是link
函數(shù)則會被經(jīng)常使用干毅。本質(zhì)上宜猜,當(dāng)我們設(shè)置了link
選項(xiàng),實(shí)際上是創(chuàng)建了一個postLink()
鏈接函數(shù)硝逢,以便compile()
函數(shù)可以定義鏈接函數(shù)姨拥。
??通常情況下,如果設(shè)置了compile
函數(shù)渠鸽,說明我們希望在指令和實(shí)時數(shù)據(jù)被放到DOM中之前進(jìn)行DOM操作叫乌,在這個函數(shù)中進(jìn)行諸如添加和刪除節(jié)點(diǎn)等DOM操作是安全的。
??compile
和link
選項(xiàng)是互斥的徽缚。如果同時設(shè)置了這兩個選項(xiàng)憨奸,那么會把compile
所返回的函數(shù)當(dāng)作鏈接函數(shù),而link
選項(xiàng)本身則會被忽略凿试。
// ...
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) {
// 連接函數(shù)
};
}
//...
如果模板被克隆過排宰,那么模板實(shí)例和鏈接實(shí)例可能是不同的對象。因此在編譯函數(shù)內(nèi)部那婉,我們只能轉(zhuǎn)換那些可以被安全操作的克隆DOM節(jié)點(diǎn)板甘。不要進(jìn)行DOM事件監(jiān)聽器的注冊:這個操作應(yīng)該在鏈接函數(shù)中完成。
??編譯函數(shù)負(fù)責(zé)對模板DOM進(jìn)行轉(zhuǎn)換详炬。
??鏈接函數(shù)負(fù)責(zé)將作用域和DOM進(jìn)行鏈接盐类。在作用域同DOM鏈接之前可以手動操作DOM。在實(shí)踐中,編寫自定義指令時這種操作是非常罕見的在跳,但有幾個內(nèi)置指令提供了這樣的功能枪萄。
鏈接
用link
函數(shù)創(chuàng)建可以操作DOM的指令。
??鏈接函數(shù)是可選的硬毕。如果定義了編譯函數(shù)呻引,它會返回鏈接函數(shù)礼仗,因此當(dāng)兩個函數(shù)都定義了時吐咳,編譯函數(shù)會重載鏈接函數(shù)。如果我們的指令很簡單元践,并且不需要額外的設(shè)置韭脊,可以從工廠函數(shù)(回調(diào)函數(shù))返回一個函數(shù)來代替對象。如果這樣做了单旁,這個函數(shù)就是鏈接函數(shù)沪羔。
??下面兩種定義指令的方式在功能上是完全一樣的:
angular.module('myApp', [])
.directive('myDirective', function() {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被鏈接之前執(zhí)行
// 在這里進(jìn)行Don轉(zhuǎn)換不安全
// 之后調(diào)用'lihk'函數(shù)將無法定位要鏈接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被鏈接之后執(zhí)行
// 如果在這里省略掉編譯選項(xiàng)
//在這里執(zhí)行DOM轉(zhuǎn)換和鏈接函數(shù)一樣安全嗎
}
};
});
angular.module('myApp', [])
.directive('myDirective', function() {
return {
link: function(scope, ele, attrs) {
return {
pre: function(tElement, tAttrs, transclude) {
// 在子元素被鏈接之前執(zhí)行
// 在這里進(jìn)行Don轉(zhuǎn)換不安全
// 之后調(diào)用'lihk'h函數(shù)將無法定位要鏈接的元素
},
post: function(scope, iElement, iAttrs, controller) {
// 在子元素被鏈接之后執(zhí)行
// 如果在這里省略掉編譯選項(xiàng)
//在這里執(zhí)行DOM轉(zhuǎn)換和鏈接函數(shù)一樣安全嗎
}
}
}
}
});
當(dāng)定義了編譯函數(shù)來取代鏈接函數(shù)時,鏈接函數(shù)是我們能提供給返回對象的第二個方法象浑,也就是postLink
函數(shù)蔫饰。本質(zhì)上講,這個事實(shí)正說明了鏈接函數(shù)的作用愉豺。它會在模板編譯并同作用域進(jìn)行鏈接后被調(diào)用篓吁,因此它負(fù)責(zé)設(shè)置事件監(jiān)聽器,監(jiān)視數(shù)據(jù)變化和實(shí)時的操作DOM蚪拦。
??link函數(shù)對綁定了實(shí)時數(shù)據(jù)的DOM具有控制能力杖剪,因此需要考慮性能問題。
鏈接函數(shù)的簽名如下:
link: function(scope, element, attrs) {
// 在這里操作DOM
}
如果指令定義中有require
選項(xiàng)驰贷,函數(shù)簽名中會有第四個參數(shù)盛嘿,代表控制器或者所依賴的指令的控制器。
// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在這里操作DOM括袒,可以訪問required指定的控制器
}
如果require
選項(xiàng)提供了一個指令數(shù)組次兆,第四個參數(shù)會是一個由每個指令所對應(yīng)的控制器組成的數(shù)組。
下面看一下鏈接函數(shù)中的參數(shù):
- scope
指令用來在其內(nèi)部注冊監(jiān)聽器的作用域锹锰。 - iElement
iElement
參數(shù)代表實(shí)例元素类垦,指使用此指令的元素。在postLink
函數(shù)中我們應(yīng)該只操作此元素的子元素城须,因?yàn)樽釉匾呀?jīng)被鏈接過了蚤认。 - iAttrs
iAttrs
參數(shù)代表實(shí)例屬性,是一個由定義在元素上的屬性組成的標(biāo)準(zhǔn)化列表糕伐,可以在所有指令的鏈接函數(shù)間共享砰琢。會以JavaScript對象的形式進(jìn)行傳遞。 - controller
controller
參數(shù)指向require
選項(xiàng)定義的控制器。如果沒有設(shè)置require
選項(xiàng)陪汽,那么controller
參數(shù)的值為undefined
训唱。
控制器在所有的指令間共享,因此指令可以將控制器當(dāng)作通信通道(公共API)挚冤。如果設(shè)置了多個require
况增,那么這個參數(shù)會是一個由控制器實(shí)例組成的數(shù)組,而不只是一個單獨(dú)的控制器训挡。
ngModel
ngModel
是一個用法特殊的指令澳骤,它提供更底層的API來處理控制器內(nèi)的數(shù)據(jù)。當(dāng)我們在指令中使用ngModel
時能夠訪問一個特殊的API澜薄,這個API用來處理數(shù)據(jù)綁定为肮、驗(yàn)證、CSS更新等不實(shí)際操作DOM的事情肤京。
??ngModel
控制器會隨ngModel
被一直注入到指令中颊艳,其中包含了一些方法。為了訪問ngModelController
必須使用require
設(shè)置:
angular.module('myApp')
.directive('myDirective',function(){
return {
require: '?ngModel',
link: function(scope, ele, attrs, ngModel) {
if (!ngModel) return;
// 現(xiàn)在我們的指令中已經(jīng)有ngModelController的一個實(shí)例
}
};
});
如果不設(shè)置require
選項(xiàng)忘分,ngModelController
就不會被注入到指令中棋枕。
??注意,這個指令沒有隔離作用域妒峦。如果給這個指令設(shè)置隔離作用域重斑,將導(dǎo)致內(nèi)部ngModel
無法更新外部ngModel
的對應(yīng)值:AngularJS會在本地作用域以外查詢值。
??為了設(shè)置作用域中的視圖值舟山,需要調(diào)用ngModel.$setViewValue()
函數(shù)绸狐。ngModel.$setViewValue()
函數(shù)可以接受一個參數(shù)。
value(字符串):value
參數(shù)是我們想要賦值給ngModel
實(shí)例的實(shí)際值累盗。這個方法會更新控制器上本地的$viewValue寒矿,然后將值傳遞給每一個$parser
函數(shù)。
??當(dāng)值被解析若债,且$parser
流水線中所有的函數(shù)都調(diào)用完成后符相,值會被賦給$modelValue
屬性,并且傳遞給指令中ng-model
屬性提供的表達(dá)式蠢琳。
??最后啊终,所有步驟都完成后,$viewChangeListeners
中所有的監(jiān)聽器都會被調(diào)用傲须。
??注意蓝牲,單獨(dú)調(diào)用$setViewValue()
不會喚起一個新的digest
循環(huán),因此如果想更新指令泰讽,需要在設(shè)置$viewValue
后手動觸發(fā)digest
例衍。
??$setViewValue()
方法適合于在自定義指令中監(jiān)聽自定義事件昔期,我們會希望在回調(diào)時設(shè)置$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) {
// 設(shè)置視圖和調(diào)用apply
scope.$apply(function() {
ngModel.$setViewValue(date);
});
}
});
});
}
};
});
自定義渲染
在控制器中定義$render
方法可以定義視圖具體的渲染方式佛玄。這個方法會在$parser
流水線完成后被調(diào)用硼一。
由于這個方法會破壞AngularJS的標(biāo)準(zhǔn)工作方式,因此一定要謹(jǐn)慎使用:
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
中有幾個屬性可以用來檢查甚至修改視圖梦抢。
- $viewValue
$viewValue
屬性保存著更新視圖所需的實(shí)際字符串般贼。 - $modelValue
$modelValue
由數(shù)據(jù)模型持有。$modelValue
和$viewValue
可能是不同的奥吩,取決于$parser
流水線是否對其進(jìn)行了操作哼蛆。 - $parsers
$parsers
的值是一個由函數(shù)組成的數(shù)組,其中的函數(shù)會以流水線的形式被逐一調(diào)用圈驼。ngModel
從DOM中讀取的值會被傳入$parsers
中的函數(shù)人芽,并依次被其中的解析器處理望几。 - $formatters
$formatters
的值是一個由函數(shù)組成的數(shù)組绩脆,其中的函數(shù)會以流水線的形式在數(shù)據(jù)模型的值發(fā)生變化時被逐一調(diào)用。它和$parser
流水線互不影響橄抹,用來對值進(jìn)行格式化和轉(zhuǎn)換靴迫,以便在綁定了這個值的控件中顯示。 - $viewChangeListeners
$viewChangeListeners
的值是一個由函數(shù)組成的數(shù)組楼誓,其中的函數(shù)會以流水線的形式在視圖中的值發(fā)生變化時被逐一調(diào)用玉锌。通過$viewChangeListeners
,可以在無需使用$watch
的情況下實(shí)現(xiàn)類似的行為疟羹。由于返回值會被忽略主守,因此這些函數(shù)不需要返回值。 - $error
$error
對象中保存著沒有通過驗(yàn)證的驗(yàn)證器名稱以及對應(yīng)的錯誤信息榄融。 - $pristine
$pristine
的值是布爾型的参淫,可以告訴我們用戶是否對控件進(jìn)行了修改。 - $dirty
$dirty
的值和$pristine
相反愧杯,可以告訴我們用戶是否和控件進(jìn)行過交互涎才。 - $valid
$valid
值可以告訴我們當(dāng)前的控件中是否有錯誤。當(dāng)有錯誤時值為false力九,沒有錯誤時值為true耍铜。 - $invalid
$invalid
值可以告訴我們當(dāng)前控件中是否存在至少一個錯誤,它的值和$valid
相反跌前。
自定義驗(yàn)證
要驗(yàn)證username
在數(shù)據(jù)庫中是否合法棕兼,可以實(shí)現(xiàn)一個指令,用來在表單發(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 />
在這個自定義驗(yàn)證中抵乓,每當(dāng)ngModel
中對應(yīng)的字段發(fā)生變化就會向服務(wù)器發(fā)送請求伴挚,以檢查用戶名是否是唯一的蹭沛。