AngularJS: come guardare le variabili di servizio?


414

Ho un servizio, dì:

factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return service;
}]);

E vorrei usare fooper controllare un elenco che è reso in HTML:

<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>

Affinché il controller rilevi quando aService.fooviene aggiornato, ho messo insieme questo schema in cui aggiungo un servizio al controller $scopee quindi uso $scope.$watch():

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.aService = aService;
  $scope.foo = aService.foo;

  $scope.$watch('aService.foo', function (newVal, oldVal, scope) {
    if(newVal) { 
      scope.foo = newVal;
    }
  });
}

Questo sembra a lungo termine, e lo sto ripetendo in ogni controller che utilizza le variabili del servizio. C'è un modo migliore per realizzare la visione di variabili condivise?


1
È possibile passare un terzo parametro a $ watch impostato su true per deep watch aService e tutte le sue proprietà.
SirTophamHatt,

7
$ scope.foo = aService.foo è sufficiente, puoi perdere la riga sopra. E ciò che fa all'interno di $ watch non ha senso, se si desidera assegnare un nuovo valore a $ scope.foo basta farlo ...
Jin

4
Potresti semplicemente fare riferimento aService.foonel markup HTML? (come questo: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks

1
Ho aggiunto un esempio senza Callbacks o $ watch , vedi la risposta sotto ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@MikeGledhill, hai ragione. Penso che sia dovuto alla natura di Javascript, puoi vedere questo schema in molti altri posti (non solo in angolare, ma in generale in JS). Da un lato, trasferisci il valore (e non viene vincolato) e dall'altro trasferisci un oggetto (o il valore che fa riferimento all'oggetto ...), ecco perché le proprietà vengono aggiornate correttamente (come perfettamente mostrato nell'esempio di Zymotik sopra).
Christophe Vidal,

Risposte:


277

Puoi sempre usare il buon vecchio modello di osservatore se vuoi evitare la tirannia e il sovraccarico di $watch.

Nel servizio:

factory('aService', function() {
  var observerCallbacks = [];

  //register an observer
  this.registerObserverCallback = function(callback){
    observerCallbacks.push(callback);
  };

  //call this when you know 'foo' has been changed
  var notifyObservers = function(){
    angular.forEach(observerCallbacks, function(callback){
      callback();
    });
  };

  //example of when you may want to notify observers
  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

E nel controller:

function FooCtrl($scope, aService){
  var updateFoo = function(){
    $scope.foo = aService.foo;
  };

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};

21
@Moo ascolta l' $destoryevento sull'ambito e aggiungi un metodo di annullamento della registrazione aaService
Jamie

13
Quali sono i vantaggi di questa soluzione? Ha bisogno di più codice in un servizio e un po 'la stessa quantità di codice in un controller (dal momento che dobbiamo anche annullare la registrazione su $ destroy). Potrei dire per la velocità di esecuzione, ma nella maggior parte dei casi non importa.
Alex Che,

6
non sono sicuro di come questa sia una soluzione migliore di $ watch, l'interrogatore stava chiedendo un modo semplice di condividere i dati, sembra ancora più ingombrante. Preferirei usare $ broadcast di questo
Jin

11
$watchil modello di osservatore è semplicemente scegliere se eseguire il polling o spingere, ed è fondamentalmente una questione di prestazioni, quindi usalo quando le prestazioni contano. Uso un modello di osservatore quando altrimenti dovrei "approfondire" la visione di oggetti complessi. Allego interi servizi al $ scope invece di guardare i singoli valori di servizio. Evito il $ watch di angular come il diavolo, c'è abbastanza di quello che succede nelle direttive e nel binding dei dati angolare nativo.
Dtheodor,

107
Il motivo per cui stiamo usando un framework come Angular è di non preparare i nostri schemi di osservatori.
Codice Whisperer,

230

In uno scenario come questo, in cui oggetti multipli / sconosciuti potrebbero essere interessati alle modifiche, utilizzare $rootScope.$broadcastdall'elemento che viene modificato.

Piuttosto che creare il proprio registro di ascoltatori (che devono essere ripuliti su vari $ destro), dovresti essere in grado di farlo $broadcastdal servizio in questione.

È comunque necessario codificare i $ongestori in ciascun listener ma il modello è disaccoppiato da più chiamate $digeste quindi evita il rischio di osservatori di lunga durata.

In questo modo, anche gli ascoltatori possono andare e venire dal DOM e / o diversi ambiti figlio senza che il servizio ne cambi il comportamento.

** aggiornamento: esempi **

Le trasmissioni avrebbero più senso nei servizi "globali" che potrebbero avere un impatto su innumerevoli altre cose nella tua app. Un buon esempio è un servizio utente in cui ci sono un certo numero di eventi che potrebbero aver luogo come login, logout, aggiornamento, inattivo, ecc. Credo che sia qui che le trasmissioni hanno più senso perché qualsiasi ambito può ascoltare un evento, senza anche iniettando il servizio, e non è necessario valutare alcuna espressione o risultati della cache per ispezionare le modifiche. Spara e dimentica (quindi assicurati che sia una notifica di fuoco e dimentica, non qualcosa che richiede un'azione)

.factory('UserService', [ '$rootScope', function($rootScope) {
   var service = <whatever you do for the object>

   service.save = function(data) {
     .. validate data and update model ..
     // notify listeners and provide the data that changed [optional]
     $rootScope.$broadcast('user:updated',data);
   }

   // alternatively, create a callback function and $broadcast from there if making an ajax call

   return service;
}]);

Il servizio sopra trasmette un messaggio ad ogni ambito quando la funzione save () è stata completata e i dati erano validi. In alternativa, se si tratta di una risorsa $ o di un invio ajax, spostare la chiamata di trasmissione nel callback in modo che si attivi quando il server ha risposto. Le trasmissioni si adattano particolarmente bene a quel modello perché ogni ascoltatore aspetta l'evento senza la necessità di ispezionare l'ambito su ogni singolo $ digest. L'ascoltatore sarebbe simile a:

.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {

  var user = UserService.getUser();

  // if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
   $scope.name = user.firstname + ' ' +user.lastname;

   $scope.$on('user:updated', function(event,data) {
     // you could inspect the data to see if what you care about changed, or just update your own scope
     $scope.name = user.firstname + ' ' + user.lastname;
   });

   // different event names let you group your code and logic by what happened
   $scope.$on('user:logout', function(event,data) {
     .. do something differently entirely ..
   });

 }]);

Uno dei vantaggi di questo è l'eliminazione di più orologi. Se combinassi campi o derivassi valori come nell'esempio sopra, dovresti guardare sia il nome che le proprietà del cognome. Guardando la funzione getUser () funzionerebbe solo se l'oggetto utente fosse sostituito sugli aggiornamenti, non si attiverebbe se l'oggetto utente avesse semplicemente le sue proprietà aggiornate. Nel qual caso dovresti fare un controllo approfondito e questo è più intenso.

$ broadcast invia il messaggio dall'ambito in cui viene chiamato in tutti gli ambiti figlio. Quindi chiamarlo da $ rootScope si attiverà su ogni ambito. Se, ad esempio, si dovesse trasmettere $ dall'ambito del proprio controller, si attiverà solo negli ambiti che ereditano dall'ambito del proprio controller. $ emit va nella direzione opposta e si comporta in modo simile a un evento DOM in quanto bolle la catena dell'ambito.

Tieni presente che ci sono scenari in cui $ broadcast ha molto senso e ci sono scenari in cui $ watch è un'opzione migliore, specialmente se in un ambito isolato con un'espressione di watch molto specifica.


1
Uscire dal ciclo $ digest è una buona cosa, soprattutto se le modifiche che stai osservando non sono un valore che andrà direttamente e immediatamente nel DOM.
XML

È lì per evitare il metodo .save (). Sembra eccessivo quando si sta solo monitorando l'aggiornamento di una singola variabile nel servizio condiviso. Possiamo guardare la variabile dall'interno del servizio condiviso e trasmetterla quando cambia?
JerryKur,

Ho provato diversi modi per condividere i dati tra i controller, ma questo è l'unico che ha funzionato. Ben fatto, signore.
abettermap

Preferisco questo alle altre risposte, sembra meno confuso , grazie
JMK,

9
Questo è il modello di progettazione corretto solo se il tuo controller di consumo ha più possibili fonti di dati; in altre parole, se hai una situazione MIMO (Multiple Input / Multiple Output). Se stai solo usando un modello uno-a-molti, dovresti usare il riferimento diretto agli oggetti e lasciare che la struttura angolare esegua l'associazione bidirezionale per te. Horkyze ha collegato questo sotto, ed è una buona spiegazione dell'associazione automatica a due vie, ed è limitazioni: stsc3000.github.io/blog/2013/10/26/…
Charles

47

Sto usando un approccio simile a @dtheodot, ma usando la promessa angolare invece di passare i callback

app.service('myService', function($q) {
    var self = this,
        defer = $q.defer();

    this.foo = 0;

    this.observeFoo = function() {
        return defer.promise;
    }

    this.setFoo = function(foo) {
        self.foo = foo;
        defer.notify(self.foo);
    }
})

Quindi ovunque usi solo il myService.setFoo(foo)metodo per aggiornare fooil servizio. Nel controller puoi usarlo come:

myService.observeFoo().then(null, null, function(foo){
    $scope.foo = foo;
})

I primi due argomenti thensono callback di successo ed errore, il terzo è callback di notifica.

Riferimento per $ q.


Quale sarebbe il vantaggio di questo metodo rispetto alla trasmissione $ descritta di seguito da Matt Pileggi?
Fabio,

Bene, entrambi i metodi hanno i loro usi. I vantaggi della trasmissione per me sarebbero la leggibilità umana e la possibilità di ascoltare in più luoghi lo stesso evento. Immagino che il principale svantaggio sia che la trasmissione sta emettendo messaggi a tutti gli ambiti discendenti, quindi potrebbe essere un problema di prestazioni.
Krym,

2
Avevo un problema a $scope.$watchcausa del quale sembrava che il funzionamento di una variabile di servizio non funzionasse (lo scopo su cui stavo guardando era un modale ereditato da $rootScope) - funzionava. Fantastico trucco, grazie per averlo condiviso!
Seiyria,

4
Come ripuliresti te stesso con questo approccio? È possibile rimuovere il callback registrato dalla promessa quando l'ambito viene distrutto?
Abris,

Buona domanda. Onestamente non lo so. Proverò a fare alcuni test su come è possibile rimuovere la notifica di richiamata dalla promessa.
Krym,

41

Senza orologi o richiamate di osservatori ( http://jsfiddle.net/zymotik/853wvv7s/ ):

JavaScript:

angular.module("Demo", [])
    .factory("DemoService", function($timeout) {

        function DemoService() {
            var self = this;
            self.name = "Demo Service";

            self.count = 0;

            self.counter = function(){
                self.count++;
                $timeout(self.counter, 1000);
            }

            self.addOneHundred = function(){
                self.count+=100;
            }

            self.counter();
        }

        return new DemoService();

    })
    .controller("DemoController", function($scope, DemoService) {

        $scope.service = DemoService;

        $scope.minusOneHundred = function() {
            DemoService.count -= 100;
        }

    });

HTML

<div ng-app="Demo" ng-controller="DemoController">
    <div>
        <h4>{{service.name}}</h4>
        <p>Count: {{service.count}}</p>
    </div>
</div>

Questo JavaScript funziona mentre stiamo restituendo un oggetto dal servizio anziché un valore. Quando un oggetto JavaScript viene restituito da un servizio, Angular aggiunge watch a tutte le sue proprietà.

Si noti inoltre che sto usando 'var self = this' poiché ho bisogno di mantenere un riferimento all'oggetto originale quando viene eseguito il timeout $, altrimenti 'this' farà riferimento all'oggetto window.


3
Questo è un ottimo approccio! Esiste un modo per associare solo una proprietà di un servizio all'ambito anziché all'intero servizio? Fare $scope.count = service.countnon funziona.
jvannistelrooy,

È inoltre possibile nidificare la proprietà all'interno di un oggetto (arbitrario) in modo che venga passato per riferimento. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross,

1
Approccio eccellente! Sebbene ci siano molte risposte forti e funzionali in questa pagina, questa è di gran lunga a) la più facile da implementare eb) la più facile da capire quando si legge il codice. Questa risposta dovrebbe essere molto più alta di quella attuale.
CodeMoose,

Grazie @CodeMoose, l'ho semplificato ancora di più oggi per i nuovi utenti di AngularJS / JavaScript.
Zymotik,

2
Che Dio ti benedica. Ho perso milioni di ore, direi. Perché stavo lottando con 1.5 e angularjs passando da 1 a 2 e volevo anche condividere dati
Amna,

29

Mi sono imbattuto in questa domanda alla ricerca di qualcosa di simile, ma penso che meriti una spiegazione approfondita di ciò che sta accadendo, nonché alcune soluzioni aggiuntive.

Quando un'espressione angolare come quella che hai usato è presente nell'HTML, Angular imposta automaticamente un $watchper $scope.fooe aggiornerà l'HTML ogni volta che $scope.foocambia.

<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>

Il problema non detto qui è che una delle due cose sta interessando in modo aService.foo tale che i cambiamenti non vengano rilevati. Queste due possibilità sono:

  1. aService.foo viene impostato su un nuovo array ogni volta, rendendo obsoleto il riferimento ad esso.
  2. aService.fooviene aggiornato in modo tale che $digestnon venga attivato un ciclo sull'aggiornamento.

Problema 1: riferimenti obsoleti

Considerando la prima possibilità, supponendo che $digestvenga applicato a, se aService.foofosse sempre lo stesso array, l'impostazione automatica $watchrileverà le modifiche, come mostrato nello snippet di codice di seguito.

Soluzione 1-a: assicurarsi che l'array o l'oggetto sia lo stesso oggetto su ogni aggiornamento

Come puoi vedere, la ng-repeat presumibilmente allegata aService.foonon si aggiorna quando aService.foocambia, ma la ng-repeat allegata aService2.foo fa . Questo perché il nostro riferimento a non aService.fooè aggiornato, ma il nostro riferimento aService2.foonon lo è. Abbiamo creato un riferimento all'array iniziale con $scope.foo = aService.foo;, che è stato quindi scartato dal servizio al suo prossimo aggiornamento, il che significa che $scope.foonon faceva più riferimento all'array che desideravamo più.

Tuttavia, mentre ci sono diversi modi per assicurarsi che il riferimento iniziale sia mantenuto intatto, a volte può essere necessario cambiare l'oggetto o l'array. O se la proprietà del servizio fa riferimento a una primitiva come a Stringo Number? In questi casi, non possiamo semplicemente fare affidamento su un riferimento. Allora, cosa possiamo fare?

Molte delle risposte fornite in precedenza forniscono già alcune soluzioni a questo problema. Tuttavia, sono personalmente favorevole all'utilizzo del metodo semplice suggerito da Jin e dalle settimane successive nei commenti:

fai semplicemente riferimento aService.foo nel markup html

Soluzione 1-b: collegare il servizio all'ambito e fare riferimento {service}.{property}in HTML.

Significato, basta fare questo:

HTML:

<div ng-controller="FooCtrl">
  <div ng-repeat="item in aService.foo">{{ item }}</div>
</div>

JS:

function FooCtrl($scope, aService) {
    $scope.aService = aService;
}

In questo modo, $watchverrà risolto aService.foosu ciascuno $digest, che otterrà il valore aggiornato correttamente.

Questo è un po 'quello che stavi cercando di fare con la soluzione alternativa, ma in modo molto meno circolare. È stato aggiunto un elemento superfluo $watchnel controller che fooattiva esplicitamente $scopeogni volta che cambia. Non hai bisogno di quel extra $watchquando ti colleghi aServiceinvece aService.fooal $scope, e ti leghi esplicitamente al aService.foomarkup.


Ora va tutto bene, supponendo che $digestvenga applicato un ciclo. Nei miei esempi precedenti, ho usato il $intervalservizio di Angular per aggiornare gli array, che avvia automaticamente un $digestloop dopo ogni aggiornamento. Ma cosa succede se le variabili di servizio (per qualsiasi motivo) non vengono aggiornate all'interno del "mondo angolare". In altre parole, non$digest viene attivato automaticamente un ciclo ogni volta che cambia la proprietà del servizio?


Problema 2: mancante $digest

Molte delle soluzioni qui risolveranno questo problema, ma sono d'accordo con Code Whisperer :

Il motivo per cui stiamo usando un framework come Angular è di non preparare i nostri schemi di osservatori

Pertanto, preferirei continuare a utilizzare il aService.fooriferimento nel markup HTML come mostrato nel secondo esempio sopra e non dovrei registrare un callback aggiuntivo all'interno del controller.

Soluzione 2: utilizzare un setter e getter con $rootScope.$apply()

Sono stato sorpreso che nessuno abbia ancora suggerito l'uso di un setter e getter . Questa funzionalità è stata introdotta in ECMAScript5 e esiste da anni ormai. Naturalmente, ciò significa che, per qualsiasi motivo, è necessario supportare browser molto vecchi, questo metodo non funzionerà, ma mi sento come se i getter e i setter fossero ampiamente sottoutilizzati in JavaScript. In questo caso particolare, potrebbero essere abbastanza utili:

factory('aService', [
  '$rootScope',
  function($rootScope) {
    var realFoo = [];

    var service = {
      set foo(a) {
        realFoo = a;
        $rootScope.$apply();
      },
      get foo() {
        return realFoo;
      }
    };
  // ...
}

Qui ho aggiunto una variabile 'privato' nella funzione di servizio: realFoo. Questo viene aggiornato e recuperato usando rispettivamente le funzioni get foo()e sull'oggetto.set foo()service

Notare l'uso di $rootScope.$apply()nella funzione impostata. Ciò garantisce che Angular sia a conoscenza di eventuali modifiche a service.foo. Se ricevi errori 'inprog', vedi questa utile pagina di riferimento o se usi Angular> = 1.3 puoi semplicemente usare $rootScope.$applyAsync().

Diffidare anche di questo se aService.fooviene aggiornato molto frequentemente, poiché ciò potrebbe influire in modo significativo sulle prestazioni. Se le prestazioni rappresentano un problema, è possibile impostare un modello di osservatore simile alle altre risposte qui utilizzando il setter.


3
Questa è la soluzione corretta e più semplice. Come dice @NanoWizard, $ digest servicesnon cerca le proprietà che appartengono al servizio stesso.
Sarpdoruk Tahmaz,

28

Per quanto posso dire, non devi fare qualcosa di così elaborato. Hai già assegnato foo dal servizio al tuo ambito e dal momento che foo è un array (e a sua volta un oggetto viene assegnato per riferimento!). Quindi, tutto ciò che devi fare è qualcosa del genere:

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.foo = aService.foo;

 }

Se alcune, altre variabili in questo stesso Ctrl dipendono dal cambio di foo, allora sì, avresti bisogno di un orologio per osservare foo e apportare modifiche a quella variabile. Ma finché è un semplice riferimento, guardare non è necessario. Spero che sia di aiuto.


35
Ci ho provato e non riuscivo $watcha lavorare con una primitiva. Invece, ho definito un metodo sul servizio che restituirebbe il valore di base: somePrimitive() = function() { return somePrimitive }E ho assegnato una proprietà $ portata a tale metodo: $scope.somePrimitive = aService.somePrimitive;. Quindi ho usato il metodo scope in HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok No, non usare le primitive. Aggiungili in un oggetto. I primitivi non sono mutabili e quindi l'associazione di dati a 2 vie non funzionerà
Jimmy Kane,

3
@JimmyKane, sì, le primitive non dovrebbero essere usate per il databinding bidirezionale, ma penso che la domanda riguardasse la visione delle variabili di servizio, non l'impostazione dell'associazione a 2 vie. Se è necessario solo guardare una proprietà / variabile del servizio, non è necessario un oggetto: è possibile utilizzare una primitiva.
Mark Rajcok,

3
In questa configurazione sono in grado di modificare i valori di aService dall'ambito. Ma l'ambito non cambia in risposta alla modifica di aService.
Ouwen Huang,

4
Anche questo non funziona per me. La semplice assegnazione $scope.foo = aService.foonon aggiorna automaticamente la variabile dell'ambito.
Darwin Tech,

9

È possibile inserire il servizio in $ rootScope e guardare:

myApp.run(function($rootScope, aService){
    $rootScope.aService = aService;
    $rootScope.$watch('aService', function(){
        alert('Watch');
    }, true);
});

Nel tuo controller:

myApp.controller('main', function($scope){
    $scope.aService.foo = 'change';
});

Un'altra opzione è quella di utilizzare una libreria esterna come: https://github.com/melanke/Watch.JS

Funziona con: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

È possibile osservare le modifiche di uno, molti o tutti gli attributi dell'oggetto.

Esempio:

var ex3 = {
    attr1: 0,
    attr2: "initial value of attr2",
    attr3: ["a", 3, null]
};   
watch(ex3, function(){
    alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");​

2
NON POSSO CREDERE CHE QUESTA RISPOSTA NON È LA PIÙ VOTATA !!! Questa è la soluzione più elegante (IMO) in quanto riduce l'entropia informativa e probabilmente mitiga la necessità di ulteriori gestori di mediazione. Vorrei votare di più se potessi ...
Cody,

L'aggiunta di tutti i servizi al $ rootScope, i suoi benefici ed è potenziali insidie, sono dettagliati un po 'qui: stackoverflow.com/questions/14573023/...
Zymotik

6

È possibile guardare le modifiche all'interno della fabbrica stessa e quindi trasmettere una modifica

angular.module('MyApp').factory('aFactory', function ($rootScope) {
    // Define your factory content
    var result = {
        'key': value
    };

    // add a listener on a key        
    $rootScope.$watch(function () {
        return result.key;
    }, function (newValue, oldValue, scope) {
        // This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
        $rootScope.$broadcast('aFactory:keyChanged', newValue);
    }, true);

    return result;
});

Quindi nel controller:

angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

    $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
        // do something
    });
}]);

In questo modo inserisci tutto il relativo codice di fabbrica nella sua descrizione, quindi puoi fare affidamento solo sulla trasmissione dall'esterno


6

== == AGGIORNATO

Molto semplice ora in $ watch.

Penna qui .

HTML:

<div class="container" data-ng-app="app">

  <div class="well" data-ng-controller="FooCtrl">
    <p><strong>FooController</strong></p>
    <div class="row">
      <div class="col-sm-6">
        <p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
      </div>
      <div class="col-sm-6">
        <p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
        <p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
        <p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
      </div>
    </div>
  </div>

  <div class="well" data-ng-controller="BarCtrl">
    <p><strong>BarController</strong></p>
    <p ng-if="name">Name is: {{ name }}</p>
    <div ng-repeat="item in items">{{ item.name }}</div>
  </div>

</div>

JavaScript:

var app = angular.module('app', []);

app.factory('PostmanService', function() {
  var Postman = {};
  Postman.set = function(key, val) {
    Postman[key] = val;
  };
  Postman.get = function(key) {
    return Postman[key];
  };
  Postman.watch = function($scope, key, onChange) {
    return $scope.$watch(
      // This function returns the value being watched. It is called for each turn of the $digest loop
      function() {
        return Postman.get(key);
      },
      // This is the change listener, called when the value returned from the above function changes
      function(newValue, oldValue) {
        if (newValue !== oldValue) {
          // Only update if the value changed
          $scope[key] = newValue;
          // Run onChange if it is function
          if (angular.isFunction(onChange)) {
            onChange(newValue, oldValue);
          }
        }
      }
    );
  };
  return Postman;
});

app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.setItems = function(items) {
    PostmanService.set('items', items);
  };
  $scope.setName = function(name) {
    PostmanService.set('name', name);
  };
}]);

app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.items = [];
  $scope.name = '';
  PostmanService.watch($scope, 'items');
  PostmanService.watch($scope, 'name', function(newVal, oldVal) {
    alert('Hi, ' + newVal + '!');
  });
}]);

1
mi piace PostmanService ma come devo cambiare la funzione $ watch sul controller se devo ascoltare più di una variabile?
jedi

Ciao jedi, grazie per il testa a testa! Ho aggiornato la penna e la risposta. Consiglio di aggiungere un'altra funzione di orologio per questo. Quindi, ho aggiunto una nuova funzionalità a PostmanService. Spero che questo aiuti :)
Hayatbiralem,

In realtà sì, sì :) Se condividi maggiori dettagli sul problema, forse posso aiutarti.
Hayatbiralem,

4

Sulla dtheodor di rispondere si potrebbe usare qualcosa di simile alla seguente per garantire che non si dimentica di annullare la registrazione del callback ... Qualcuno potrebbe obiettare a passare la $scopea un servizio però.

factory('aService', function() {
  var observerCallbacks = [];

  /**
   * Registers a function that will be called when
   * any modifications are made.
   *
   * For convenience the callback is called immediately after registering
   * which can be prevented with `preventImmediate` param.
   *
   * Will also automatically unregister the callback upon scope destory.
   */
  this.registerObserver = function($scope, cb, preventImmediate){
    observerCallbacks.push(cb);

    if (preventImmediate !== true) {
      cb();
    }

    $scope.$on('$destroy', function () {
      observerCallbacks.remove(cb);
    });
  };

  function notifyObservers() {
    observerCallbacks.forEach(function (cb) {
      cb();
    });
  };

  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

Array.remove è un metodo di estensione simile al seguente:

/**
 * Removes the given item the current array.
 *
 * @param  {Object}  item   The item to remove.
 * @return {Boolean}        True if the item is removed.
 */
Array.prototype.remove = function (item /*, thisp */) {
    var idx = this.indexOf(item);

    if (idx > -1) {
        this.splice(idx, 1);

        return true;
    }
    return false;
};

2

Ecco il mio approccio generico.

mainApp.service('aService',[function(){
        var self = this;
        var callbacks = {};

        this.foo = '';

        this.watch = function(variable, callback) {
            if (typeof(self[variable]) !== 'undefined') {
                if (!callbacks[variable]) {
                    callbacks[variable] = [];
                }
                callbacks[variable].push(callback);
            }
        }

        this.notifyWatchersOn = function(variable) {
            if (!self[variable]) return;
            if (!callbacks[variable]) return;

            angular.forEach(callbacks[variable], function(callback, key){
                callback(self[variable]);
            });
        }

        this.changeFoo = function(newValue) {
            self.foo = newValue;
            self.notifyWatchersOn('foo');
        }

    }]);

Nel tuo controller

function FooCtrl($scope, aService) {
    $scope.foo;

    $scope._initWatchers = function() {
        aService.watch('foo', $scope._onFooChange);
    }

    $scope._onFooChange = function(newValue) {
        $scope.foo = newValue;
    }

    $scope._initWatchers();

}

FooCtrl.$inject = ['$scope', 'aService'];

2

Per quelli come me che cercano solo una soluzione semplice, questo fa quasi esattamente quello che ti aspetti dall'uso del normale $ watch nei controller. L'unica differenza è che valuta la stringa nel suo contesto javascript e non su un ambito specifico. Dovrai iniettare $ rootScope nel tuo servizio, sebbene sia usato solo per agganciare correttamente i cicli digest.

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};

2

mentre affrontavo un problema molto simile, ho visto una funzione nell'ambito e ho avuto la funzione di restituire la variabile di servizio. Ho creato un violino js . puoi trovare il codice qui sotto.

    var myApp = angular.module("myApp",[]);

myApp.factory("randomService", function($timeout){
    var retValue = {};
    var data = 0;

    retValue.startService = function(){
        updateData();
    }

    retValue.getData = function(){
        return data;
    }

    function updateData(){
        $timeout(function(){
            data = Math.floor(Math.random() * 100);
            updateData()
        }, 500);
    }

    return retValue;
});

myApp.controller("myController", function($scope, randomService){
    $scope.data = 0;
    $scope.dataUpdated = 0;
    $scope.watchCalled = 0;
    randomService.startService();

    $scope.getRandomData = function(){
        return randomService.getData();    
    }

    $scope.$watch("getRandomData()", function(newValue, oldValue){
        if(oldValue != newValue){
            $scope.data = newValue;
            $scope.dataUpdated++;
        }
            $scope.watchCalled++;
    });
});

2

Sono arrivato a questa domanda, ma ho scoperto che il mio problema era che stavo usando setInterval quando avrei dovuto usare il provider angolare $ intervallo. Questo è anche il caso di setTimeout (utilizzare invece $ timeout). So che non è la risposta alla domanda del PO, ma potrebbe aiutare alcuni, poiché mi ha aiutato.


È possibile utilizzare setTimeouto qualsiasi altra funzione non angolare, ma non dimenticare di racchiudere il codice nel callback con $scope.$apply().
magnetronnie,

2

Ho trovato un'ottima soluzione sull'altro thread con un problema simile ma un approccio totalmente diverso. Fonte: AngularJS: $ watch all'interno della direttiva non funziona quando viene modificato il valore $ rootScope

Fondamentalmente la soluzione lì dice di NON usare $watchin quanto è una soluzione molto pesante. Invece propongono di usare $emite $on.

Il mio problema era guardare una variabile nel mio servizio e reagire nella direttiva . E con il metodo sopra è molto semplice!

Il mio modulo / esempio di servizio:

angular.module('xxx').factory('example', function ($rootScope) {
    var user;

    return {
        setUser: function (aUser) {
            user = aUser;
            $rootScope.$emit('user:change');
        },
        getUser: function () {
            return (user) ? user : false;
        },
        ...
    };
});

Quindi in pratica guardo il mio user - ogni volta che è impostato su un nuovo valore ho $emituno user:changestato.

Ora nel mio caso, nella direttiva ho usato:

angular.module('xxx').directive('directive', function (Auth, $rootScope) {
    return {
        ...
        link: function (scope, element, attrs) {
            ...
            $rootScope.$on('user:change', update);
        }
    };
});

Ora nella direttiva ascolto sul $rootScopee sul cambiamento dato - reagisco rispettivamente. Molto semplice ed elegante!


1

// servizio: (niente di speciale qui)

myApp.service('myService', function() {
  return { someVariable:'abc123' };
});

// ctrl:

myApp.controller('MyCtrl', function($scope, myService) {

  $scope.someVariable = myService.someVariable;

  // watch the service and update this ctrl...
  $scope.$watch(function(){
    return myService.someVariable;
  }, function(newValue){
    $scope.someVariable = newValue;
  });
});

1

Un po 'brutto, ma ho aggiunto la registrazione delle variabili dell'ambito al mio servizio per un interruttore:

myApp.service('myService', function() {
    var self = this;
    self.value = false;
    self.c2 = function(){};
    self.callback = function(){
        self.value = !self.value; 
       self.c2();
    };

    self.on = function(){
        return self.value;
    };

    self.register = function(obj, key){ 
        self.c2 = function(){
            obj[key] = self.value; 
            obj.$apply();
        } 
    };

    return this;
});

E poi nel controller:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}

Grazie. Una piccola domanda: perché ritorni thisda quel servizio anziché self?
shrekuu,

4
Perché gli errori vengono fatti a volte. ;-)
nclu,

Buona pratica return this;fuori dai tuoi costruttori ;-)
Cody

1

Dai un'occhiata a questo plunker: questo è l'esempio più semplice che mi viene in mente

http://jsfiddle.net/HEdJF/

<div ng-app="myApp">
    <div ng-controller="FirstCtrl">
        <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
        <br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
    </div>
    <hr>
    <div ng-controller="SecondCtrl">
        Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
    </div>
</div>



// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
   return { FirstName: '' };
});

myApp.controller('FirstCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

0

Ho visto alcuni terribili schemi di osservatori qui che causano perdite di memoria su grandi applicazioni.

Potrei essere un po 'in ritardo, ma è così semplice.

La funzione watch controlla i cambiamenti di riferimento (tipi primitivi) se vuoi guardare qualcosa come il push dell'array, usa semplicemente:

someArray.push(someObj); someArray = someArray.splice(0);

Ciò aggiornerà il riferimento e aggiornerà l'orologio da qualsiasi luogo. Compreso un metodo getter di servizi. Tutto ciò che è una primitiva verrà aggiornato automaticamente.


0

Sono in ritardo per la parte ma ho trovato un modo migliore per farlo rispetto alla risposta postata sopra. Invece di assegnare una variabile per contenere il valore della variabile di servizio, ho creato una funzione collegata all'ambito, che restituisce la variabile di servizio.

controllore

$scope.foo = function(){
 return aService.foo;
}

Penso che questo farà quello che vuoi. Il mio controller continua a verificare il valore del mio servizio con questa implementazione. Onestamente, questo è molto più semplice della risposta selezionata.


perché è stato downvoted .. Ho anche usato una tecnica simile molte volte e ha funzionato.
indefinito il

0

Ho scritto due semplici servizi di utilità che mi aiutano a tenere traccia delle modifiche alle proprietà del servizio.

Se vuoi saltare la lunga spiegazione, puoi andare stretto a jsfiddle

  1. WatchObj

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // returns watch function
  // obj: the object to watch for
  // fields: the array of fields to watch
  // target: where to assign changes (usually it's $scope or controller instance)
  // $scope: optional, if not provided $rootScope is use
  return function watch_obj(obj, fields, target, $scope) {
    $scope = $scope || $rootScope;
    //initialize watches and create an array of "unwatch functions"
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    //unregister function will unregister all our watches
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    //automatically unregister when scope is destroyed
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}

Questo servizio viene utilizzato nel controller nel modo seguente: Supponiamo di avere un servizio "testService" con le proprietà 'prop1', 'prop2', 'prop3'. Si desidera guardare e assegnare all'ambito 'prop1' e 'prop2'. Con il servizio di sorveglianza sembrerà che:

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
}

  1. applicare Watch obj è eccezionale, ma non è sufficiente se si dispone di codice asincrono nel servizio. In tal caso, utilizzo una seconda utility simile a questa:

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

Lo innescerei alla fine del mio codice asincrono per attivare il ciclo $ digest. Come quello:

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply(); //trigger $digest loop
  }.bind(this));
}

Quindi, tutto ciò insieme sembrerà così (puoi eseguirlo o aprire il violino ):

// TEST app code

var app = angular.module('app', ['watch_utils']);

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
  $scope.test1 = function() {
    testService.test1();
  };
  $scope.test2 = function() {
    testService.test2();
  };
  $scope.test3 = function() {
    testService.test3();
  };
}

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
  this.reset();
}
TestService.prototype.reset = function() {
  this.prop1 = 'unchenged';
  this.prop2 = 'unchenged2';
  this.prop3 = 'unchenged3';
}
TestService.prototype.test1 = function() {
  this.prop1 = 'changed_test_1';
  this.prop2 = 'changed2_test_1';
  this.prop3 = 'changed3_test_1';
}
TestService.prototype.test2 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
  }.bind(this));
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply();
  }.bind(this));
}
//END TEST APP CODE

//WATCH UTILS
var mod = angular.module('watch_utils', []);

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // target not always equals $scope, for example when using bindToController syntax in 
  //directives
  return function watch_obj(obj, fields, target, $scope) {
    // if $scope is not provided, $rootScope is used
    $scope = $scope || $rootScope;
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class='test' ng-app="app" ng-controller="TestWatch">
  prop1: {{prop1}}
  <br>prop2: {{prop2}}
  <br>prop3 (unwatched): {{prop3}}
  <br>
  <button ng-click="test1()">
    Simple props change
  </button>
  <button ng-click="test2()">
    Async props change
  </button>
  <button ng-click="test3()">
    Async props change with apply
  </button>
</div>

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.