在這個系列的第一篇文章中介紹了AngularJS自訂指令以及一些簡單的例子,這篇文章我們去瞭解下AngularJS的獨立範圍,以及獨立範圍在建立自訂指令時有多重要。
什麼是獨立範圍?
預設情況下,指令是可以直接存取父範圍中的屬性的。例如,下面的指令依靠父範圍來輸出一個自訂對象的name和street屬性:
angular.module('directivesModule').directive('mySharedScope', function () {
return {
template: 'Name: {{customer.name}} Street: {{customer.street}}'
};
});
雖然這可以完成工作,但在實際使用中你必須要知道和這個指令相關的父範圍的很多資訊來確保指令能夠正常工作,或者僅僅使用ngInclude和HTML模板來完成同樣的事情(這在上一篇文章中已經討論過了)。這樣做的問題在於如果父範圍作了一點改變,這個指令很可能就不再有用了。
如果你想建立一個可重用的指令,你當然不能讓指令去依賴父範圍的任何屬性而是使用獨立範圍來代替它們。這是一個對比共用範圍和獨立範圍的示意圖:
通過上面的示意圖可以看到共用範圍允許父範圍向下延伸到指令中。獨立範圍在這種方式下是不能工作的,獨立範圍就像在你的指令一圈圍上一堵牆,父範圍無法直接翻過圍牆訪問指令內容的屬性。就好像下面這樣:
在指令中建立獨立範圍
在指令中建立獨立範圍是很簡單的:只需要在指令中添加一個scope參數即可。像下面代碼中所示,這會自動為指令建立一個獨立的範圍。
angular.module('directivesModule').directive('myIsolatedScope', function () {
return {
scope: {},
template: 'Name: {{customer.name}} Street: {{customer.street}}'
};
});
現在範圍是獨立的了,上面例子中來自父範圍中的customer對象在指令中將無法使用。當這個指令被用在下個視圖中將會有以下輸出(注意name和street的值沒有被輸出):
Name: Street:
既然獨立範圍切斷了與父範圍通訊的橋樑,那麼我們該如何處理與父範圍之間的資料交換呢?我們可以使用@、=以及&符號來定義範圍,這一眼看起來似乎有些奇怪,但熟練使用後會發現不算太糟。下面我們就來看一下這些符號如何使用。
本地範圍屬性介紹
獨立範圍提供3個不種的方式用來與外部範圍之間進行互動。這三種方式通過在指令的scope屬性中指定不同的標識符來實現,這三個標識符分別是@、=和&。下面看一下它們是如何工作的。
@本地範圍屬性
@被用來讀取在指令外部的字串值。例如,一個控制器可能在$scope對象上定義一個name屬性,你需要在指令裡面讀取這個name的值,就可以使用@來完成,通過下圖我們來進一步講解:
在控制器中定義$scope.name;
$scope.name屬性需要在指令中可讀;
指令在獨立範圍中定義一個本地範圍屬性name(注意這個屬性名稱可以是任意符合要求的名字,沒必要與外部範圍中的相同)。使用scope: {name: '@'}即可;
@字元告訴指令這個新的name屬性是一個來自外部範圍的字串值。如果外部範圍中這個name的值被修改了,指令中的這個值也會自動更新;
包含這個指令的視圖可以通過name屬性綁定值到指令。
下面是一個整合起來的例子,假設下面的控制器在一個app中被定義:
var app = angular.module('directivesModule', []);
app.controller('CustomersController', ['$scope', function ($scope) {
var counter = 0;
$scope.customer = {
name: 'David',
street: '1234 Anywhere St.'
};
$scope.customers = [
{
name: 'David',
street: '1234 Anywhere St.'
},
{
name: 'Tina',
street: '1800 Crest St.'
},
{
name: 'Michelle',
street: '890 Main St.'
}
];
$scope.addCustomer = function () {
counter++;
$scope.customers.push({
name: 'New Customer' + counter,
street: counter + ' Cedar Point St.'
});
};
$scope.changeData = function () {
counter++;
$scope.customer = {
name: 'James',
street: counter + ' Cedar Point St.'
};
};
}]);
指令建立一個獨立範圍,允許從外部範圍中綁定name屬性:
angular.module('directivesModule')
.directive('myIsolatedScopeWithName', function () {
return {
scope: {
name: '@'
},
template: 'Name: {{ name }}'
};
});
可以像下面這樣使用這個指令:
<div my-isolated-scope-with-name name="{{ customer.name }}"></div>
注意$scope.customer.name的值是如何綁定到指令獨立範圍中的name屬性上去的。
代碼將會輸出如下:
Name: David
如前面提到的,當$scope.customer.name的值改變時,指令將會立即自動作出改變。然而,如果是指令內部修改了它自己的name屬性的話,外部範圍中的$scope.customer.name值是不會作出改變的。如果你需要讓獨立範圍中的值與外部範圍中的值保持同步,你需要使用=來代替@。
有一點也比較重要的是,如果你想讓指令獨立範圍中的name屬性與綁定到視圖上的屬性不同,你可以使用下面的替代文法:
angular.module('directivesModule')
.directive('myIsolatedScopeWithName', function () {
return {
scope: {
name: '@someOtherName'
},
template: 'Name: {{ name }}'
};
});
這樣的話在指令內部將使用name屬性,而在外部的資料繫結中將使用someOtherName代替name,資料繫結寫法如下:
<div my-isolated-scope-with-name some-other-name="{{ customer.name }}"></div>
我一般更偏向於讓獨立範圍中的屬性名稱與視圖中綁定的屬性名稱保持一致,所以我一般不使用這種寫法。然而,這在某些情境下可以保持系統彈性。這在使用@、=以及&定義本地範圍時都是有效。
=本地範圍屬性
@在只需要給指令傳遞字串值時很方便實用,但在需要把在指令中對值的改變反映到外部範圍時卻無能為力。在需要建立在指令的獨立範圍和外部範圍中的雙向繫結時,你可以使用=字元,如下圖:
在控制器中定義$scope.person對象;
$scope.person對象需要通過建立雙向繫結的方式傳入到指令中;
指令建立一個自訂本地的獨立範圍屬性customer,通過使用scope: {customer: '='}完成;
=告訴指令傳入指令本地範圍中的對象需要使用雙向繫結的方式。如果外部範圍中的屬性值變動,指令本地範圍中的值也會自動更新;如果指令中修改了這個值,外部範圍中對應的也會同步被修改;
指令內的視圖模板現在可以綁定到獨立域的customer屬性。
下面是一個使用=的例子:
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' // 雙向資料繫結
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
在這個例子中,指令使用一個對象作為customer屬性的值,並且使用ngRepeat遍曆customer對象的所有屬性最後將其輸出到<li>元素中。
使用下面的方式給指令傳遞資料:
<div my-isolated-scope-with-model customer="customer"></div>
需要注意下,在使用=本地範圍屬性時你不能像使用@時那樣使用,而是直接使用屬性名稱(不需要雙花括弧)。在上面的例子中,customer對象被直接放在的customer屬性裡。指令使用ngRepeat遍曆customer對象的所有屬性並輸出它們。將會輸出以下內容:
David
1234 Anywhere St.
&本地範圍屬性
在學習使用&之前你需要先瞭解如何使用@本地範圍屬性傳遞一個字串值給指令,並且知道如何通過=本地範圍屬性完成指令與外部範圍中對象的雙向繫結。最後一個本地範圍屬性是使用&字元來綁定一個外部函數。
&本地範圍屬性允許指令調用方傳遞一個可被指令內部調用的函數。例如,假設你在寫一個指令,終端使用者點擊指令中的一個按鈕並需要在控制器中觸發一個事件。你不能把點擊事件寫入程式碼在指令的代碼內部,這樣的話外部的控制器就無法知道指令內部到底發生了什麼。在需要時觸發一個事件可以很好的解決這個問題(使用$emit或$broadcast),但是控制器需要知道具體偵聽的事件名是什麼所以也不是最優的。
更好的方法是讓指令的消費者傳遞給指令一個在需要時可以被調用的函數。每當指令檢測到指定的操作(例如檢測當使用者點擊一個按鈕)時它可以調用傳遞給它的函數。這種方式指令的消費者擁有100%的控制權,能完全知道指令中發生了什麼,並委託控制函數傳入指令。下面是一張簡易示意圖:
在控制器中定義一個叫做$scope.click的函數;
$scope.click函數需要傳入到指令中,目的是使指令在按鈕點擊時可以調用這個函數;
指令建立一個叫做action的自訂本地範圍屬性。使用scope: {action: '&'}可以做到。在這個例子中,action僅僅相當於click的一個別名。當action被調用,click也會被調用;
&字元從根本上來說相當於: “嘿,給我一個函數我可以在指令中發生某些事件時調用它”;
指令中的模板可以包含一個按鈕,當按鈕被點擊時,action(外部函數的引用)函數將會被調用。
下面是一個使用&的例子:
angular.module('directivesModule')
.directive('myIsolatedScopeWithModelAndFunction', function () {
return {
scope: {
datasource: '=',
action: '&'
},
template: '<ul><li ng-repeat="prop in datasource">{{ prop }}</li></ul> ' +
'<button ng-click="action()">Change Data</button>'
};
});
需要注意的是下面的來自指令模板代碼引用到action本地範圍函數並且在按鈕被點擊時調用。
<button ng-click="action()">Change Data</button>
下面是使用這個指令的例子。當然更建議為指令起一個短一點的名字。
<div my-isolated-scope-with-model-and-function
datasource="customer"
action="changeData()">
</div>
被傳入到指令action屬性的changeData()函數在控制器中定義,控制器的定義和文章前面的一樣,changeData()函數定義如下:
$scope.changeData = function () {
counter++;
$scope.customer = {
name: 'James',
street: counter + ' Cedar Point St.'
};
};
結尾
在這個系列的文章中你將會看到一些關鍵點,如模板、獨立範圍、本地範圍屬性等。建立獨立範圍只需要在指令定義中添加一個scope屬性,值為一個對象即可。一共有三種本地範圍屬性可用,分別是:
@ 用來傳遞一個字串值到指令
= 用於建立一個雙向繫結的對象
& 允許傳入一個可被指令內部調用的函數
譯者注
scope屬性的值可以為一個bool型,值為false時不使用獨立範圍,和不寫此屬性沒區別。
scope中定義的屬性名稱要使用駝峰命名的方式,而在模板中使用的時候要使用連字號文法,假設有一個指令叫datePicker,scope部分定義如下:
scope: {
isOpen: "=",
currentDate: "=",
onChange: "&"
}
視圖中使用方式如下(假設引號裡面的函數和範圍屬性是已經在控制器中定義的):
<div date-picker
is-open="openState"
current-date="currentDate"
on-change="dateChange()"
></div>
```
另外,如果`scope`中的一些屬性是可選的(如上面例子中,`isOpen`預設為false,指令的使用者可以選擇不傳遞這個屬性),在使用這個指令的時候AngularJS就會報錯,也就是說`scope`定義的屬性在調用指令時都需要被傳遞(不傳遞會報錯,但不影響程式運行)。解決這個問題的話可以在選擇性參數後面加一個問號`?`標識這個屬性是可選的,修改後的指令`scope`部分如下:
```js
scope: {
isOpen: "=?", // 注意這裡的問號,指定這個參數是可選的
currentDate: "=",
onChange: "&"
}