AngularJS: usare gli eventi nei servizi invece di $rootScope

Short link

Quest'articolo è una traduzione di "Listening to Service event instead of $rootScope" di Joel Chu.

Uno dei pilastri di un progetto a cui sto lavorando è un calendario che ho trovato su GitHub, il Bootstrap Material Datetimepicker. È ricco di opzioni ma ha un 70% di cose di cui non ho bisogno. E il restante 30% ha avuto bisogno di un pesante refactoring, così ho creato il mio nb-ng-calendar.

Il sistema funziona usando $rootScope.$broadcast ovunque ci sia bisogno di una chiamata ad un'azione. Odio questa cosa. Ci sono diversi motivi:

  1. Dispendiosità. Intasa il ciclo $digest di AngularJS.
  2. Confusione. Da dove viene generato l'evento? Non ci è dato di saperlo.
  3. Mancanza di modularità. Siete costretti a legare il codice ad un'app specifica.
  4. E $broadcast procede dall'alto verso il basso ed $emit al contrario. La relazione tra il codice e l'evento non è chiara.

L'unico vantaggio ad usare $rootScope.$broadcast o $rootScope.$emit è quando non vi interessa l'origine e la relazione del codice con l'evento.

Come possiamo creare un sistema che abbia la stessa flessibilità ma non lo stesso svantaggio?

Usare $rootScope.$broadcast

Osservate questo codice:


// Controller
    $scope.someCallback = function(data)
    {
        $rootScope.$broadcast('callbackFired' , data);
    };

 // Direttiva

    $rootScope.$on('callbackFired' , function(evt , data)
    {
         $scope.someData = data; 
         // Quindi mostreremo o nasconderemo qualcosa
    });

 // Template della direttiva
    <div ng-show="someData">
         {{someData.message}}
    </div>

Si tratta di un pattern comune. Ma cosa succede se dobbiamo scrivere una libreria che deve agganciarsi ad un altro sistema?

angular.module('yourModuleName').service(['$rootScope' , function($rootScope) 
 {
      $rootScope.$on('someEventThatMightCrashWithOther', function(evt , data) 
      {
          // ...
      });

 }]);

Dovrete scrivere una robusta documentazione per spiegare come usare il vostro codice.

Un servizio basato sugli eventi

La terminologia di AngularJS penso sia fuorviante. Molti sviluppatori chiamano i loro Factory , Service come Model. Non è del tutto corretto.

Per me un servizio (vedi panes.js) andrebbe inteso in questi termini:

Un servizio è un modo di aggiungere funzionalità al codice senza uno stretto legame.

Quindi un Service dovrebbe essere flessibile e adattarsi a diverse situazioni. Quindi perché non creare un servizio basato sugli eventi?

Il seguente esempio è tratto dalla libreria a cui sto lavorando:

var CalendarServiceClass = function()
 {
      var self = this;
      self.triggers = {};

      self.$on = function(event , callback) 
      {
           if (!self.triggers[event]) {
               self.triggers[event] = [];
           }
           self.triggers[event].push( callback );
      });

      self.$trigger = function(event , params , from)
      {
           if (self.trigger[event]) {
                for (var i in self.triggers[event]) {
                    self.triggers[event][i].call(self , params , from);
                }
           }
      });

      // continua...

      return self; 
 };

Questa classe viene inizializzata all'interno di un Provider in quanto dispone di diverse opzioni che lo sviluppatore può impostare. E viene usata da nbNgCalendarService, e all'interno di nbNgCalendarDirectiveCtrl vengono definite le direttive:

angular.module('nb.ng.calendar').controller('nbNgCalendarDirectiveCtrl',
['$scope' , 'nbNgCalendarService' , function ($scope , nbcService)
{
      var self = this;
      /**
       *  Metodo invocato all'interno di una cella di tabella
       */
      self.selectDay = function(evt , day)
      {
          evt.preventDefault();
          nbcService.$trigger('dateSelected' , day);
      };

      nbcService.$on('dateSelected' , function(day) {
           // Elabora il giorno selezionato
      });
}]);

E in un'altra direttiva invocata se è stata impostata l'opzione selectedOnClose:

// close-btn-directive.js 
 link: function(scope , el , attr) 
 {
      nbNgCalendarService.$on('dateSelected' , function(day) 
      {
           if (scope.calConfig.selectedOnClose === true) {
                el.trigger('click');
           }
      });
 });

E i vantaggi?

  1. Non richiede $rootScope: diminuisce il carico sul sistema $digest di AngularJS.
  2. Sapete esattamente dove ha origine l'evento anche grazie al parametro from.
  3. È altamente portabile.
  4. Potete restare in ascolto dell'evento all'interno di tutta l'app, proprio come con $rootScope.
  5. Il pattern funziona anche in altri framework come React e anche in Node.js.

Questo pattern mi ha aiutato nei miei progetti: spero sia così anche per voi.

L'idea è venuta da due post su StackOverflow:

Ho creato un package per Bower:

bower install nb-event-service --save

E uno per Node.js:

npm install nb-event-service --save

O su Github.

Aggiornamento della versione 0.3.0

Ora potete passare un array di eventi ai metodi $on e $trigger. Immaginate una direttiva che esegue un'azione quando viene cliccato un pulsante:

// Direttiva
     link: function(scope, el) 
     {
         eventService.$on(['event1' ,'event2'], function()
         {
              el.animate('animazione');
         });
     }

Bottone 1

<button on-click="vm.eventService.$trigger('event1' , 'btn1')">click</button>

Bottone 2

<button on-click="vm.eventService.$trigger('event2' , 'btn2')">click me!</button>

Ora la direttiva è in ascolto di entrambi gli eventi.