NG筆記27-呼叫Directive內部函式
開始寫Directive之後,常面臨一個難題:Directive提供函式執行特定作業,當我們在DOM中引用Directive,如先前介紹透過Isolated Scope宣告{ callback: "&" },就可輕鬆由Directive呼叫外部Scope的Callback函式;但反過來,外部Scope要怎麼觸發Isolated Scope內部的函式呢?
歷經一些研究、嘗試,我找到幾種做法,整理分享如下。
先說明範例情境,我寫了一個無聊的Directive serverTime,它透過$http.get("/")向伺服器隨便發一個GET Request,再由Response headers["date"]偷出伺服器時間。Directive的Isolated Scope有個function refresh(),每次呼叫時重新由伺服器取得時間,藉以驗證Directive內部函式已執行。
方法一是我認為最標準的做法,外部Scope需額外提供一個Trigger屬性,Directive使用$scope.$watch() Trigger屬性,每次Trigger值改變(可用Trigger = new Date()或指定為數字再Trigger++)就立刻執行refresh(),程式碼如下:(註:使用物件化形式寫ViewModel及Diretive,並配合$injector處理依賴注入,細節說明可參考前一篇文章) Online Demo
<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="utf-8">
<title>Directive Communication: $watch</title>
</head>
<body ng-controller="ctrl as vm">
<div>
<span ng-bind="vm.Time"></span>
<button ng-click="vm.Refresh()">Refresh</button>
</div>
<hr />
<div server-time time="vm.Time" trigger="vm.Trigger"></div>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<script>
function myViewModel($scope) {
var self = this;
self.Time = null;
self.Trigger = 0;
self.Refresh = function () {
self.Trigger++;
}
}
myViewModel.$injector = ["$scope"];
function serverTime($http) {
return {
scope: {
time: "=",
trigger: "="
},
link: function (scope, element, attr) {
function refresh() {
$http.get("/").success(function (data, status, headers) {
scope.time = headers("date");
});
}
scope.$watch("trigger", function () {
refresh();
});
},
template: "<pre>Server Time : {{time}}</pre>"
};
}
serverTime.$inject = ["$http"];
angular.module("app", [])
.controller("ctrl", myViewModel)
.directive("serverTime", serverTime);
</script>
</body>
</html>
|
這種做法很符合MVVM精神,由Directive自行訂閱指定屬性,在其改變時做出適當反應。ViewModel與Directive完全獨立,彼此不依賴。但為此額外要多設一個屬性(Trigger),而「改變Trigger值的目的是為了觸發指定函式」的概念有些迂迴,算是缺點。
方法二,使用$broadcast()與$on()。Angular在Scope間可用$scope.$on註冊事件(想像成jQuery.bind()),並透過$broadcast()觸發所屬子Scope的指定事件、$emit()觸發所屬父Scope的指定事件(如同jQuery.trigger())。因此,只要在Directive $scope.$on("refresh-svr-time", refresh),在ViewModel中$scope.$broadcast("refressh-svr-time")即可達到由ViewModel觸發Directive refresh()的目的。Online Demo
function myViewModel($scope) {
var self = this;
self.Time = null;
self.Refresh = function () {
$scope.$broadcast("refresh-svr-time");
}
}
myViewModel.$injector = ["$scope"];
function serverTime($http) {
return {
scope: {
time: "=",
},
link: function (scope, element, attr) {
function refresh() {
$http.get("/").success(function (data, status, headers) {
scope.time = headers("date");
});
}
refresh();
scope.$on("refresh-svr-time", function () {
refresh();
});
},
template: "<pre>Server Time : {{time}}</pre>"
};
}
serverTime.$inject = ["$http"];
|
使用$broadcast()/$on()傳遞訊息,ViewModel與Directive仍維持不直接接觸,不用多設Trigger屬性,用$broadcast()觸發事件也比較直覺。但"refresh-svr-time"事件名稱的出現,意味著Directive邏輯滲入ViewModel端,二者的彼此獨立性略遜方式一。
方法三,ViewModel增加供雙向繫結的物件屬性(我喜歡叫它ApiProxy),Directive建立時,動態在ApiProxy注入一個物件,其中可安插各式函式、屬性,成為ViewModel與Directive間溝通的橋樑,ViewModel即可輕易使用Directive主動外露的狀態屬性及方法。Online Demo
function myViewModel($scope) {
var self = this;
self.Time = null;
self.ApiProxy;
self.Refresh = function () {
self.ApiProxy && self.ApiProxy.Refresh && self.ApiProxy.Refresh();
}
}
myViewModel.$injector = ["$scope"];
function serverTime($http) {
return {
scope: {
time: "=",
apiProxy: "="
},
link: function (scope, element, attr) {
function refresh() {
$http.get("/").success(function (data, status, headers) {
scope.time = headers("date");
});
}
refresh();
scope.apiProxy = {
Refresh: refresh
};
},
template: "<pre>Server Time : {{time}}</pre>"
};
}
serverTime.$inject = ["$http"];
|
由於ApiProxy物件可加入任意屬性、方法,是我認為最直覺最彈性的做法。實務上可用TypeScript定義專屬Class規範ApiProxy的型別,確保ViewModel正確存取ApiProxy的屬性及方法,減少程式出錯可能。但ApiProxy做法固然簡便,卻隱藏一項危機:ViewModel必須知道ApiProxy物件規格 ,在ViewModel摻雜Directive邏輯,使二者產生相依性,有違SoC準則。
以上是我所知道三種ViewModel呼叫Directive內部函式的方法,各有優劣,大家偏好哪一種?歡迎回饋。
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs