開始寫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

Comments

Be the first to post a comment

Post a comment