neděle 26. dubna 2015

Způsoby komunikace mezi controllery v AngularJS

V Angularu existuje několik různých způsobů, jak lze komunikovat mezi controllery. Také předpokládám, že je lze použít i pro komunikaci mezi controllerem a direktivou. Nutno dodat, že jsem zatím použil v produkci jen první způsob komunikace. To však neznamená, že ostatní způsoby jsou méně kvalitní.

Při komunikaci máme 2 subjekty - ten, kdo upozorňuje nebo publikuje změny (publisher) a ten, kdo na tyto změny poslouchá a reaguje (subscriber).

1) Způsob komunikace pomocí $rootScope jsem viděl nejčastěji popsané. Základní myšlenkou je použít nejvýše nadřazený $rootScope k vyslání události všem podřazeným posluchačům. Pro vyslání události jsou k dispozici dvě různé metody, které můžeme použít.

První z nich je $broadcast(). Podle dokumentace (https://docs.angularjs.org/api/ng/type/$rootScope.Scope) tato metoda propaguje směrem od rodiče k potomkům (shora dolů). :

http://plnkr.co/ZESyGrcUojE74wqNMt4y

.controller('PubController', function ($rootScope) {
    this.doSthOnClickAndNotifyOthers = function (valueToPass) {
        $rootScope.$broadcast("pubController.notifyHello", valueToPass);
        // do sth else    };
})
.controller('SubController', function ($scope) {
    $scope.$on("pubController.notifyHello", function (event, value) {
    });
});

Povšimněte si u subscriber controlleru registraci k poslechu události pomocí $scope.$on(). Protože $scope je potomkem $rootScope, dostane upozornění v případě, že se zavolá $broadcast() na rodiči, tedy na $rootScope.

Druhá metoda $emit() propaguje událost opačným směrem než $broadcast(), tedy směrem zdola nahoru. V dřívějších verzích AngularJS se upřednostňoval tento způsob, protože $broadcast() volal i $scope, které neposlouchaly na danou událost, a proto byl výrazně pomalejší (http://stackoverflow.com/questions/11252780/whats-the-correct-way-to-communicate-between-controllers-in-angularjs/19498009#19498009). Nyní už by měla být chyba opravena.

http://plnkr.co/F8bkF7YEpwKAloTWZCHC

.controller('PubController', function ($rootScope) {
    this.doSthOnClickAndNotifyOthers = function (valueToPass) {
        $rootScope.$emit("pubController.notifyHello", valueToPass);
        // do sth else    };
})
.controller('SubController', function ($rootScope) {
    var thisController = this;
    thisController.values = [];
    $rootScope.$on("pubController.notifyHello", function (event, value) {
        thisController.values.push(value);
    });
});

Hlavním rozdílem mezi $emit() a $broadcast() je u registrace subscriber na událost. U $emit() je použito $rootScope místo $scope. Důvodem je propagace události pomocí $emit() směrem nahoru. Protože $rootScope je rodič všech jiných $scope a je tedy nejvýše v hierarchii Scope, tak nemůžeme pro registraci posluchače použít nic jiného než zase $rootScope. Nevýhoda této metody je nutnost odregistrovat posluchač v případě, že zanikne daný controller. AngularJS automaticky odregistruje všechny události navěšené na $scope controlleru při jeho zániku. Z tohoto důvodu u $broadcast() nebylo potřeba ručně odregistrovat. Z $rootScope se však události nikdy neodstraní a zůstávají zde do té doby, než skončí Angular. To může způsobit přetečení paměti. Více o řešení tohoto problému najdete v odkazu na Plunker.

2) http://plnkr.co/EUKADZ6T88Gws6XvqGXX

Tento způsob je zlepšení předchozí metody, protože odstraňuje závislost na $rootScope v controlleru a $rootScope se volá jen v servisních třídách. Další výhodu vidím v tom, že všechny názvy události lze shromáždit na jednom místě (např. zde v NotificationService) a v controllerech volat jen metody z této servisní třídy. O tomto způsobu jsem se dočetl zde: http://codingsmackdown.tv/blog/2013/04/29/hailing-all-frequencies-communicating-in-angularjs-with-the-pubsub-design-pattern/

.service('NotificationService', function ($rootScope) {
    var _HELLO_CLICK = "notifyHello";

    this.onHello = function ($scope, handler) {
        $scope.$on(_HELLO_CLICK, function (event, args) {
            handler(args);
        });
    };

    this.notifyHello = function (args) {
        $rootScope.$broadcast(_HELLO_CLICK, args);
    };

    return this;
})

NotificationService zajišťuje upozorňování událostí. Pro každou událost by tu měla být dvojice metod, jedna pro vyslání události a druhá pro poslouchání.

.controller('PubController', function (NotificationService) {
    this.doSthOnClickAndNotifyOthers = function (valueToPass) {
        NotificationService.notifyHello(valueToPass);
    };
})
.controller('SubController', function ($scope, NotificationService) {
    NotificationService.onHello($scope, function (value) {
        // do sth when hello    });
});

Controller, který je subscriber, předá do servisní třídy svůj $scope a fci, kterou se má zavolat, když přijde událost.

3) Promise API a notify()
http://plnkr.co/7clVRbi6OiV26OMtnIGH

Tento způsob jsem neviděl popsaný pro použití komunikace mezi controllery. Podobnou metodu jsem použil pro komunikaci pomocí websocketu, kdy se po celou dobu udržuje otevřené spojení se serverem a klient čeká a reaguje na odpovědi. Obdobně lze použít i pro případ, kdy potřebujete každých x sekund udělat AJAX dotaz na server a v případě změn aktualizovat stránku.

Prvně je potřeba vytvořit service třídu a v ní Deferred objekt pro událost, na kterou chceme reagovat.

var helloNotificationDefer = $q.defer();

Následně vytvoříme 2 metody - jednu pro publikování změn a druhou pro poslouchání těchto změn.

this.whenHello = function () {
    return helloNotificationDefer.promise;
};

this.notifyHello = function (valueToPass) {
    helloNotificationDefer.notify(valueToPass);
};

Metoda notify() v Deferred objektu, umožňuje opakovaně posílat události všem posluchačům. Na rozdíl od metod resolve() a reject() neukončuje Promise objekt, ale nechává ji otevřenou. Proto se také skvěle hodí např. pro komunikaci se serverem pomocí websocketu.
.controller('PubController', function (NotificationService) {
    this.doSthOnClickAndNotifyOthers = function (valueToPass) {
        NotificationService.notifyHello(valueToPass);
        // do sth else    };
})
.controller('SubController', function (NotificationService) {
    NotificationService.whenHello().then(null, null, function (value) {
        // do sth when got click event
    })
});

<div ng-controller="PubController as pubCtrl">
    <button ng-click="pubCtrl.doSthOnClickAndNotifyOthers('hello')">Click to pass 'hello'</button>
</div>
V publisher controlleru zavoláme metodu notifyHello(), pokud uživatel klikne na tlačítko. V subscriber controlleru musím poslouchat na danou událost. Pokud přijde událost, zavolá se fce, kterou jsem předal v části then(). Všimněte si předání dvou null argumentů v této metodě. První null je pro volání resolve(), druhý je pro reject() na Promise objektu a až třetí argument je pro notify() metodu. Protože resolve() ani reject() nikde nevoláme, není potřeba na tyto události reagovat,.
Mezi největší výhodu tohoto způsobu vidím v použití Promise API místo $rootScope. $rootScope je v Angularu něco jako window v JavaScriptu a pokud máme mnoho události, může to zaneřádit $rootScope.