W

Passaggio dati tra controller Angular tramite servizio

Molto spesso nei nostri progetti c’è la necessità di passare dati fra diversi controller; una delle soluzioni che ho visto adottare spesso è quella di utilizzare emit / broadcast dalla parte di chi il dato lo vuole condividere e registrare uno o più listener dalla parte dei controller che devono essere notificati di questo nuovo dato.

Il tipo di approccio sopra pò presentare qualche criticità, sia nel capire il codice per chi non l’ha scritto (e anche per chi l’ha scritto, se passa molto tempo) sia per la sua implementazione; se abbiamo il controller A che deve condividere il dato xx con il controller B ed entrambi possono modificare xx, ci si trova  a dover creare un emit sia sua A sia su B e, allo stesso modo, un listener sia su A sia su B. Infine, una parola anche sulla testabilità: con tale approccio si può testare, ma anche la parte di testing diventa più difficoltosa e poco lineare.

Dopo questa premessa, vi propongo un approccio diverso, sfruttando un service per scambiare i dati e mantenere lo stato; tutto questo è possibile perchè in Angular i service sono singleton, quindi una volta istanziati dal container DI la stessa istanza viene passata nel costruttore di ogni oggetto che ne faccia richiesta tramite dipendenza nel costruttore.

Nel codice allegato (che trovate alla fine) c’è un esempio funzionante di quanto vi sto dicendo; ve lo descrivo brevemente di seguito.

Abbiamo una semplice pagina con una input box, un pulsante ed una lista.

L’input box e il pulsante sono controllati dal controller First mentre la lista dal controller Second (si, lo so, poca fantasia… ); l’utente può inserire una stringa non vuota nell’input e, premendo il pulsante, aggiungerla alla lista che viene poi mostrata da Second.

Entrambi i controller First e Second hanno una dipendenza verso il service myService (sempre molta fantasia…), che mantiene una collezione delle stringhe.
In particolare i diversi attori:

FirstController

  1. Ha la proprietà nuovoDato, in bind con la input
  2. Espone il metodo addDato in bind con il pulsante; quando premuto, chiama il metodo addDato di myService, passando come argomento nuovoDato

SecondController

  1. Espone la funzione dati, in bind con l’elemento <li> tramite la direttiva ngRepeat. La funzione dati, al suo interno, richiama il metodo getList di myService, che ritorna la collezione di stringhe

MyService

  1. Ha la proprietà privata myData che mantiene al suo interno la lista delle stringhe (si tratta di un array, inizializzata con già un dato al suo interno “Dato già presente…”)
  2. Espone il modo addDato, che prende in ingresso una stringa e la aggiunge all’array myData
  3. Espone il metodo getList che ritorna la lista delle strighe (array myData)

Il tutto è molto semplice, come potete vedere, ma anche lineare; un unico punto in cui il dato viene mantenuto e aggiornato; i controller non fanno altro che chiamare i metodi del servizio senza doversi preoccupare di notifiche e gestione di eventi.

Tutto il codice client è scritto in TypeScript

Soluzione Visual Studio 2015

Share Button
 
W

JavaScript – Rendere testabile codice già esistente ma NON testabile

Sono appena “caduto” su codice JS scritto in questo modo:

angular.element(document).ready(function () {
    // Fatto così è un casino da testare!!
    angular.module("ModuloPerCostanti", [])
        .constant("parametri",
        {
            costanteUno: $("#parameters").data("costante-uno"),
            costanteDur: $("#parameters").data("costante-due")
        });

    angular.module("ModuloPrincipale", ["ModuloPerCostanti", "ModuloPerServizi", "ModuloPerDirettive", "ngSanitize", "ngResource"])
        .filter("filtroUno", function () {
            return function (data) {
               //DoSomething
            }
        })
        .filter("formatPrezzo", function () {
            return function (data) {
                //DoSomething                
            };
        })
        .controller("ControllerPrincipale", ["$rootScope", "$scope", "$http", function ($rootScope, $scope, $http) {
                // DoSomething
        }])
        .controller("ControllerSecondario", ["$rootScope", "$scope", "$compile", "$http", "$window", "$timeout", "$filter", "$sce", function ($rootScope, $scope, $compile, $http, $window, $timeout, $filter, $sce) {
            //DoSomething
        });
});

Scritto in questo modo, non sarà testabile a meno di scatenare l’evento “documentReady” e far inizializzare i moduli, i controller, i filtri, le direttive etc… (cfr: http://bittersweetryan.github.io/jasmine-presentation/#slide-17)

Questo perché? Jasmine (framework JavaScript utilizzato per il testing) carica i moduli angular in questo modo:

angular(“ModuloPerCostanti”) // Cercherà ma non troverà il modulo ModuloPerCostanti

angular(“ModuloPrincipale”) // Cercherà ma non troverà il modulo ModuloPrincipale

Purtroppo Jasmine non troverà alcun modulo, perché quella parte di codice, che definisce i moduli, viene avviata solo durante l’evento document.ready.

Il codice è scritto in quel modo perché dipende dell’HTML della pagina, in particolare la parte di “constant” richiede che il documento sia completamente caricato (di fatto quelle non sono costanti… se fosse C# non ci permetterebbe mai di compilare come costante qualche cosa di non noto a compile time); tralasciando tutto questo, come renderlo testabile senza stravolgerlo? (tenete conto che quando bisogna “rendere testabile” un codice qualcosa non funziona; test a parte, significa che stiamo usando un approccio poco lineare difficile da capire, manutenere e non è detto funzioni sempre).

Basta un piccolissimo accorgimento,utilizzando la notazione dei NameSpace javascript, in modo da minimizzare possibli conflitti:

  1. Dichiariamo il nostro nameSpace
  2. Creiamo una funzione Init che contenga tutto il nostro codice, con la notazione dei NameSpace:
    var NostroNameSpace; // Dichiariamo ma non inizializziamo il namespace
    (function(tempNameSpace){ // Funzione anonima autochiamante
        tempNameSpace.Init = function(){ // al parametro tempNameSpace viene agganciata la funzione Init
        angular.module("ModuloPerCostanti", [])
            .constant("parametri",
            {
                costanteUno: $("#parameters").data("costante-uno"),
                costanteDur: $("#parameters").data("costante-due")
            });
    
        angular.module("ModuloPrincipale", ["ModuloPerCostanti", "ModuloPerServizi", "ModuloPerDirettive", "ngSanitize", "ngResource"])
            .filter("filtroUno", function () {
                return function (data) {
                   //DoSomething
                }
            })
            .filter("formatPrezzo", function () {
                return function (data) {
                    //DoSomething                
                };
            })
            .controller("ControllerPrincipale", ["$rootScope", "$scope", "$http", function ($rootScope, $scope, $http) {
                    // DoSomething
            }])
            .controller("ControllerSecondario", ["$rootScope", "$scope", "$compile", "$http", "$window", "$timeout", "$filter", "$sce", function ($rootScope, $scope, $compile, $http, $window, $timeout, $filter, $sce) {
                //DoSomething
            });
    };
    })(NostroNameSpace || (NostroNameSpace = {})); // Se NostroNameSpace non è già stato istanziato, viene qui inizializzato ad un oggetto vuoto, altrimenti sarebbe <em>undefined</em>
    
  1. Richiamiamo l’unica funzione NostroNameSpace.Init così: angular.element(document).ready(NostroNameSpace.Init); // Di nuovo usiamo il document ready

Cosa cambia?

In Jasmine, a questo punto, faremo così:

NostroNameSpace.Init();

angular(“ModuloPerCostanti”) // Cercherà e caricherà il modulo ModuloPerCostanti

angular(“ModuloPrincipale”) // Cercherà e caricherà il modulo ModuloPrincipale

Il codice è stato rifattorizzato in maniera molto semplice, ma in questo modo possiamo testare tutto (circa 5/10 minuti di refactor, più che altro per stare attenti a non introdurre errori).

Share Button