開始寫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
<!DOCTYPEhtml> <htmlng-app="app"> <head> <metacharset="utf-8"> <title>Directive Communication: $watch</title> </head> <bodyng-controller="ctrl as vm"> <div> <spanng-bind="vm.Time"></span> <buttonng-click="vm.Refresh()">Refresh</button> </div> <hr/> <divserver-timetime="vm.Time"trigger="vm.Trigger"></div> <scriptsrc="//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> |
copyText();
這種做法很符合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"]; |
copyText();
使用$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"]; |
copyText();
由於ApiProxy物件可加入任意屬性、方法,是我認為最直覺最彈性的做法。實務上可用TypeScript定義專屬Class規範ApiProxy的型別,確保ViewModel正確存取ApiProxy的屬性及方法,減少程式出錯可能。但ApiProxy做法固然簡便,卻隱藏一項危機:ViewModel必須知道ApiProxy物件規格 ,在ViewModel摻雜Directive邏輯,使二者產生相依性,有違SoC準則。
以上是我所知道三種ViewModel呼叫Directive內部函式的方法,各有優劣,大家偏好哪一種?歡迎回饋。
[NG系列]