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:
- Dispendiosità. Intasa il ciclo
$digest
di AngularJS. - Confusione. Da dove viene generato l'evento? Non ci è dato di saperlo.
- Mancanza di modularità. Siete costretti a legare il codice ad un'app specifica.
- 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?
- Non richiede
$rootScope
: diminuisce il carico sul sistema$digest
di AngularJS. - Sapete esattamente dove ha origine l'evento anche grazie al parametro
from
. - È altamente portabile.
- Potete restare in ascolto dell'evento all'interno di tutta l'app, proprio come con
$rootScope
. - 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.