這一篇從自定義指令出發,記錄了定義一個指令時影響指令行為的各種因素。
試著感受這些因素,讓自己更高效地編寫AngularJS應用。
Directive
先從定義一個簡單的指令開始。
定義一個指令本質上是在HTML中通過元素、屬性、類或註釋來新增功能。
AngularJS的內建指令都是以ng
開頭,如果想自定義指令,建議自定義一個字首代表自己的名稱空間。
這裡我們先使用my
作為字首:
var myApp = angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'A',
replace: true,
template: '<p>Kavlez</p>'
};
})
如此一來,我們可以這樣使用,注意命名是camel-case:
<my-directive />
<!-- <my-directive><p>Kavlez</p></my-directive> -->
directive()
接受兩個引數
- name:字串,指令的名字
- factory_function:函式,指令的行為
應用啟動時,以name作為該應用的標識註冊factory_function返回的物件。
在factory_function中,我們可以設定一些選項來改變指令的行為。
下面記錄一下定義指令時用到的選項
restrict (string)
該屬性用於定義指令以什麼形式被使用,這是一個可選引數,本文開頭定義的指令用的也是A,其實該選項預設為A。
也就是元素(E)、屬性(A)、類(C)、註釋(M)
(ps:EMAC? EMACS? 挺好記哈)
比如上面定義的myDirective
,可以以任何形式呼叫。
- E(元素)
<my-directive></my-directive>
- A(屬性,預設值)
<div my-directive="expression"></div>
- C(類名)
<div class="my-directive:expression;"></div>
- M(註釋)
<--directive:my-directive expression-->
priority (Number)
也就是優先順序,預設為0。
在同一元素上宣告瞭多個指令時,根據優先順序決定哪個先被呼叫。
如果priority相同,則按宣告順序呼叫。
另外,no-repeat
是所有內建指令中優先順序最高的。
terminal (Boolean)
終端? 而且還是Boolean?
被名字嚇到了,其實terminal的意思是是否停止當前元素上比該指令優先順序低的指令。
但是相同的優先順序還是會執行。
比如,我們在my-directive
的基礎上再加一個指令:
.directive('momDirective',function($rootScope){
return{
priority:3,
terminal:true
};
})
呼叫發現my-directive
不會生效:
<div mom-directive my-directive="content" ></div>
template (String/Function)
至少得輸出點什麼吧? 但template也是可選的。
String型別時,template可以是一段HTML。
Function型別時,template是一個接受兩個引數的函式,分別為:
- tElement
- tAttrs
函式返回一段字串作為模板。
templateUrl (String/Function)
這個就和上面的template很像了,只不過這次是通過URL請求一個模板。
String型別時,templateURL自然是一個URL。
Function型別時返回一段字串作為模板URL。
replace (Boolean/String)
預設值為false,以文章開頭定義的指令為例,假設我們這樣呼叫了指令
<my-directive></my-directive>
replace為true時,輸出:
<p>Kavlez</p>
replace為false時,輸出:
<my-directive><p>Kavlez</p></my-directive>
transclude (Boolean)
該選項預設為false,翻譯過來叫'嵌入',感覺還是有些生澀。
template
和scope
已經可以做很多事情了,但有一點不足。
比如在原有元素的基礎上新增內容,transclude
的例子如下:
<body ng-app="myApp">
<textarea ng-model="content"></textarea>
<div my-directive title="Kavlez">
<hr>
{{content}}
</div>
</body>
<script type="text/javascript">
var myApp = angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'EA',
scope: {
title: '@',
content: '='
},
transclude: true,
template: '<h2 class="header">{{ title }}</h2>\
<span class="content" ng-transclude></span>'
};
});
</script>
發現div下的hr並沒有被移除,就是這樣的效果。
注意不要忘了在模板中宣告ng-transclude
。
scope (Boolean/Object)
預設為false,true時會從父作用域繼承並建立一個自己的作用域。
而ng-controller
的作用也是從父作用域繼承並建立一個新的作用域。
比如這樣,離開了自己的作用域就被打回原形了:
<div ng-init="content='from root'">
{{content}}
<div ng-controller="AncestorController">
{{content}}
<div ng-controller="ChildController">
{{content}}
</div>
{{content}}
</div>
{{content}}
</div>
.controller('ChildController', function($scope) {
$scope.content = 'from child';
})
.controller('AncestorController', function($scope) {
$scope.content = 'from ancestor';
})
但不要誤解,指令巢狀並不一定會改變它的作用域。
既然true
時會從父作用域繼承並建立一個自己的作用域,那麼我們來試試改為false
會是什麼樣子:
<div ng-init="myProperty='test'">
{{ myProperty }}
<div my-directive ng-init="myProperty = 'by my-directive'">
{{ myProperty }}
</div>
{{ myProperty }}
</div>
.directive('myDirective', function($rootScope) {
return {
scope:false
};
})
顯然,結果是三行'by my-directive'。
非true即false? naive!
其實最麻煩的還是隔離作用域,
我們稍微改動一下myDirective,改為輸出<p>{{內容}}</p>
。
於是我試著這樣定義:
<body ng-app="myApp" >
<p ng-controller="myController">
<div my-directive="I have to leave." ></div>
{{myDirective}}
</p>
</body>
<script type="text/javascript">
var myApp = angular.module('myApp', [])
.directive('myDirective', function($rootScope) {
$rootScope.myDirective = 'from rootScope';
return {
priority:1000,
restrict: 'A',
replace: true,
scope: {
myDirective: '@',
},
template: '<p>{{myDirective}}</p>'
};
})
.controller('myController',function($scope){
$scope.myDirective = 'from controller';
});
</script>
這裡需要注意的不是@
,重點是隔離作用域。
根據上面的例子輸出,template中的{{myDirective}}
不會影響到其他作用域。
我們再試試這樣:
<input type="text" ng-model="content">
<p ng-controller="myController" >
<div my-directive="{{content}}" ></div>
{{content}}
</p>
發現大家都在一起變,也就是說值是通過複製DOM屬性並傳遞到隔離作用域。
ng-model
是個強大的指令,它將自己的隔離作用域和DOM作用域連在一起,這樣就是一個雙向資料繫結。
如何向指令的隔離作用域中傳遞資料,這裡用了@
。
或者也可以寫成@myDirective
,也就是說換個名字什麼的也可以,比如我用@myCafe
什麼的給myDirective賦值也是沒問題的,總之是和DOM屬性進行繫結。
另外,我們也可以用=
進行雙向繫結,將本地作用域的屬性同父級作用域的屬性進行雙向繫結。
比如下面的例子中,隔離作用域裡的內容只能是'abc' :
<body ng-app="myApp" ng-init="content='abc'">
<p ng-controller="myController" >
<input type="text" ng-model="content">
<div my-directive="content" ></div>
{{content}}
</p>
</body>
<script type="text/javascript">
var myApp = angular.module('myApp', [])
.directive('myDirective', function($rootScope) {
return {
priority:1000,
restrict: 'A',
replace: true,
scope: {
myDirective: '=',
},
template: '<p>from myDirective:{{myDirective}}</p>'
};
})
.controller('myController',function($scope){
$scope.content = 'from controller';
});
</script>
在隔離作用域訪問指令外部的作用域的方法還有一種,就是&
。
我們可以使用&
與父級作用域的函式進行繫結,比如下面的例子:
<body ng-app="myApp">
<div ng-controller="myController">
<table border='1'>
<tr>
<td>From</td>
<td><input type="text" ng-model="from"/></td>
</tr>
<tr>
<td>To</td>
<td><input type="text" ng-model="to"/></td>
</tr>
<tr>
<td>Content</td>
<td><textarea cols="30" rows="10" ng-model="content"></textarea></td>
</tr>
<tr>
<td>Preview:</td>
<td><div scope-example to="to" on-send="sendMail(content)" from="from" /></td>
</tr>
</table>
</div>
</div>
</body>
<script type="text/javascript">
var myApp = angular.module('myApp', [])
.controller('myController',function($scope){
$scope.sendMail=function(content){
console.log('content is:::'+content);
}
})
.directive('scopeExample',function(){
return{
restrict:'EA',
scope: {
to: '=',
from: '=' ,
send: '&onSend'
},
template:'<div>From:{{from}}<br>\
To:{{to}}<br>\
<button ng-click="send()">Send</button>\
</div>'
}
})
</script>
controller (String/Function)
控制器也可以在指令裡定義,比如:
.directive('myDirective', function() {
restrict: 'A',
controller: 'myController'
}).controller('myController', function($scope, $element, $attrs,$transclude) {
//...
})
相同的效果,也可以這樣宣告:
directive('myDirective', function() {
restrict: 'A',
controller:function($scope, $element, $attrs, $transclude) {
//...
}
});
controllerAs (String)
可以從名字和型別看出,這個選項是用來設定控制器的別名的。
比如這樣:
directive('myDirective', function() {
restrict: 'A',
controller:function($scope, $element, $attrs, $transclude) {
//...
}
});
compile (Object/Function)
雖說這個東西不是很常用吧,但卻是值得了解的選項。
compile
和link
,這兩個選項關係到AngularJS的生命週期。
先在這裡簡單記錄一下我對生命週期的認識。
- 應用啟動前,所有的指令以文字的形式存在。* 應用啟動後便開始進行compile和link,DOM開始變化,作用域與HTML進行繫結。* 在編譯階段,AngularJS會遍歷整個HTML並處理已宣告的指令。
- 一個指令的模板中可能使用了另外一個指令,這個指令的模板中可能包含其他指令,如此層層下來便是一個模板樹。* 在DOM尚未進行資料繫結時對DOM進行操作開銷相對較小,這時像
ng-repeat
之類的指令對DOM進行操作則再合適不過了。 - 我們可以用編譯函式訪問編譯後的DOM,在資料繫結之前用編譯函式對模板DOM進行轉換,編譯函式會返回模板函式。
也就是說,設定compile函式的意義在於:在指令和實時資料被放到DOM中之前修改DOM。
此時完全可以毫無顧慮地操作DOM。 - 接著我們便可以進入下一個階段,連結階段。
- 最後,模板函式傳遞給指令指定的連結函式,連結函式對作用域和DOM進行連結。
好了,接下來我們就試試compile:
<body ng-app="myApp">
<my-directive ng-model="myName"></my-directive>
</body>
<script type="text/javascript">
var myApp = angular.module('myApp', [])
.directive('myDirective', function($rootScope) {
$rootScope.myName = 'Kavlez';
return {
restrict: 'EA',
compile:function(tEle, tAttrs, transcludeFn) {
var h2 = angular.element('<h2></h2>');
h2.attr('type', tAttrs.type);
h2.attr('ng-model', tAttrs.ngModel);
h2.html("hello {{"+tAttrs.ngModel+"}}");
tEle.replaceWith(h2);
}
};
});
</script>
原文出處 AngularJS - 自定義指令