AngularJS: Comment surveiller les variables de service?

J’ai un service, dis:

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

Et je voudrais utiliser foo pour contrôler une liste qui est rendue en HTML:

 
{{ item }}

Pour que le contrôleur détecte la mise à jour de aService.foo j’ai aService.foo ce modèle où j’ajoute un service à la $scope du contrôleur, puis utilise $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; } }); } 

Cela semble long et je l’ai répété dans chaque contrôleur qui utilise les variables du service. Existe-t-il un meilleur moyen d’observer les variables partagées?

Vous pouvez toujours utiliser le bon vieux modèle d’observateur si vous voulez éviter la tyrannie et les frais généraux de $watch .

Dans le service:

 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(); }); }); 

Et dans le contrôleur:

 function FooCtrl($scope, aService){ var updateFoo = function(){ $scope.foo = aService.foo; }; aService.registerObserverCallback(updateFoo); //service now in control of updating foo }; 

Dans un scénario comme celui-ci, où des objects multiples / inconnus pourraient être intéressés par des modifications, utilisez $rootScope.$broadcast de l’élément en cours de modification.

Plutôt que de créer votre propre registre d’écouteurs (qui doivent être nettoyés sur différents $ destroys), vous devriez pouvoir $broadcast partir du service en question.

Vous devez toujours coder les gestionnaires $on dans chaque écouteur, mais le modèle est découplé de plusieurs appels à $digest et évite ainsi le risque de voir des observateurs de longue durée.

De cette manière, les auditeurs peuvent également aller et venir du DOM et / ou de différentes scopes enfants sans que le service ne modifie son comportement.

** mise à jour: exemples **

Les diffusions auraient plus de sens dans les services “globaux” qui pourraient avoir un impact sur d’innombrables autres éléments de votre application. Un bon exemple est un service Utilisateur où il existe un certain nombre d’événements tels que la connexion, la déconnexion, la mise à jour, l’inactivité, etc. Je pense que c’est là que les diffusions ont le plus de sens. même injecter le service, et il n’a pas besoin d’évaluer les expressions ou les résultats du cache pour inspecter les modifications. Il se contente de déclencher et d’oublier (assurez-vous qu’il s’agit d’une notification d’incendie et d’oubli, et non de quelque chose qui nécessite une action)

 .factory('UserService', [ '$rootScope', function($rootScope) { var service =  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; }]); 

Le service ci-dessus diffusait un message à chaque étendue lorsque la fonction save () était terminée et que les données étaient valides. Sinon, s’il s’agit d’une ressource $ ou d’une soumission ajax, déplacez l’appel de diffusion dans le rappel pour qu’il se déclenche lorsque le serveur a répondu. Les émissions conviennent particulièrement bien à ce modèle, car chaque auditeur attend simplement l’événement sans avoir à inspecter la scope de chaque fichier $ digest. L’auditeur ressemblerait à:

 .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 .. }); }]); 

L’un des avantages est l’élimination de plusieurs montres. Si vous combiniez des champs ou dérivez des valeurs telles que l’exemple ci-dessus, vous devez surveiller les propriétés firstname et lastname. Regarder la fonction getUser () ne fonctionnerait que si l’object utilisateur était remplacé sur les mises à jour, il ne se déclencherait pas si l’object utilisateur avait simplement ses propriétés mises à jour. Dans ce cas, vous devrez faire une surveillance approfondie et c’est plus intensif.

$ broadcast envoie le message depuis la scope dans laquelle il a été appelé dans n’importe quelle scope enfant. Donc l’appeler depuis $ rootScope se déclenche sur toutes les scopes. Si vous deviez diffuser à partir de la scope de votre contrôleur, par exemple, il ne s’exécuterait que dans les étendues qui héritent de la scope de votre contrôleur. $ emit va dans la direction opposée et se comporte de manière similaire à un événement DOM en ce sens qu’il gonfle la chaîne de scope.

Gardez à l’esprit qu’il existe des scénarios où $ broadcast est très judicieux, et il y a des scénarios où $ watch est une meilleure option – en particulier dans une scope isolée avec une expression de surveillance très spécifique.

J’utilise une approche similaire à @dtheodot mais en utilisant la promesse angular au lieu de transmettre des rappels

 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); } }) 

Ensuite, utilisez myService.setFoo(foo) méthode myService.setFoo(foo) pour mettre à jour foo on service. Dans votre contrôleur, vous pouvez l’utiliser comme:

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

Les deux premiers arguments sont then les rappels de succès et d’erreur, le troisième est le rappel de notification.

Référence pour $ q.

Sans montres ni rappels d’observateur ( 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

 

{{service.name}}

Count: {{service.count}}

Ce JavaScript fonctionne car nous transmettons un object du service plutôt qu’une valeur. Lorsqu’un object JavaScript est renvoyé par un service, Angular ajoute des montres à toutes ses propriétés.

Notez également que j’utilise ‘var self = this’ car je dois garder une référence à l’object d’origine lorsque $ timeout s’exécute, sinon «this» fera référence à l’object window.

Autant que je sache, vous n’avez pas à faire quelque chose d’aussi élaboré. Vous avez déjà affecté foo depuis le service à votre scope et depuis foo est un tableau (et à son tour un object lui est assigné par référence!). Donc, tout ce que vous devez faire est quelque chose comme ceci:

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

Si certains, d’autres variables dans cette même Ctrl dépendent de la modification de foo, alors oui, vous aurez besoin d’une montre pour observer foo et apporter des modifications à cette variable. Mais tant que c’est une simple référence, il est inutile de regarder. J’espère que cela t’aides.

Je suis tombé sur cette question en cherchant quelque chose de similaire, mais je pense que cela mérite une explication approfondie de ce qui se passe, ainsi que des solutions supplémentaires.

Lorsqu’une expression angular telle que celle que vous avez utilisée est présente dans le code HTML, Angular configure automatiquement une $watch pour $scope.foo et mettra à jour le code HTML chaque fois que $scope.foo modifié.

 
{{ item }}

Le problème non dit est que l’une des deux choses affectant aService.foo telle que les modifications ne sont pas détectées. Ces deux possibilités sont les suivantes:

  1. aService.foo est défini sur un nouveau tableau à chaque fois, provoquant la référence obsolète.
  2. aService.foo est mis à jour de telle manière qu’un cycle $digest n’est pas déclenché sur la mise à jour.

Problème 1: Références obsolètes

Compte tenu de la première possibilité, en supposant qu’un $digest est appliqué, si aService.foo était toujours le même tableau, $watch détectera automatiquement les modifications, comme indiqué dans l’extrait de code ci-dessous.

Solution 1-a: Assurez-vous que le tableau ou l’object est le même object sur chaque mise à jour

 angular.module('myApp', []) .factory('aService', [ '$interval', function($interval) { var service = { foo: [] }; // Create a new array on each update, appending the previous items and // adding one new item each time $interval(function() { if (service.foo.length < 10) { var newArray = [] Array.prototype.push.apply(newArray, service.foo); newArray.push(Math.random()); service.foo = newArray; } }, 1000); return service; } ]) .factory('aService2', [ '$interval', function($interval) { var service = { foo: [] }; // Keep the same array, just add new items on each update $interval(function() { if (service.foo.length < 10) { service.foo.push(Math.random()); } }, 1000); return service; } ]) .controller('FooCtrl', [ '$scope', 'aService', 'aService2', function FooCtrl($scope, aService, aService2) { $scope.foo = aService.foo; $scope.foo2 = aService2.foo; } ]); 
         

Array changes on each update

{{ item }}

Array is the same on each udpate

{{ item }}

Vous pouvez insérer le service dans $ rootScope et regarder:

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

Dans votre contrôleur:

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

Une autre option consiste à utiliser une bibliothèque externe comme: https://github.com/melanke/Watch.JS

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

Vous pouvez observer les modifications d’un, de plusieurs ou de tous les atsortingbuts d’object.

Exemple:

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

Vous pouvez regarder les modifications dans l’usine elle-même, puis diffuser une modification.

 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; }); 

Puis dans votre contrôleur:

 angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) { $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) { // do something }); }]); 

De cette manière, vous mettez tout le code usine correspondant dans sa description, vous ne pouvez compter que sur la diffusion de l’extérieur

== UPDATED ==

Très simple maintenant dans $ watch.

Pen ici .

HTML:

 

BarController

Name is: {{ name }}

{{ item.name }}

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 + '!'); }); }]); 

En vous basant sur la réponse de dtheodor, vous pouvez utiliser quelque chose de similaire à celui ci-dessous pour vous assurer de ne pas oublier d’enregistrer le rappel … Certains peuvent s’opposer au passage du $scope à un service.

 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 est une méthode d’extension qui ressemble à ceci:

 /** * 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; }; 

Voici mon approche générique.

 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'); } }]); 

Dans votre contrôleur

 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']; 

alors que je suis confronté à un problème très similaire, j’ai regardé une fonction dans la scope et la fonction a renvoyé la variable de service. J’ai créé un violon js . vous pouvez trouver le code ci-dessous.

  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++; }); }); 

I came to this question but it turned out my problem was that I was using setInterval when I should have been using the angular $interval provider. This is also the case for setTimeout (use $timeout instead). I know it’s not the answer to the OP’s question, but it might help some, as it helped me.

I have found a really great solution on the other thread with a similar problem but totally different approach. Source: AngularJS : $watch within directive is not working when $rootScope value is changed

Basically the solution there tells NOT TO use $watch as it is very heavy solution. Instead they propose to use $emit and $on .

My problem was to watch a variable in my service and react in directive . And with the above method it very easy!

My module/service example:

 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; }, ... }; }); 

So basically I watch my user – whenever it is set to new value I $emit a user:change status.

Now in my case, in the directive I used:

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

Now in the directive I listen on the $rootScope and on the given change – I react respectively. Very easy and elegant!

For those like me just looking for a simple solution, this does almost exactly what you expect from using normal $watch in controllers. The only difference is, that it evaluates the ssortingng in it’s javascript context and not on a specific scope. You’ll have to inject $rootScope into your service, although it is only used to hook into the digest cycles properly.

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

// service: (nothing special here)

 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; }); }); 

A wee bit ugly, but I’ve added registration of scope variables to my service for a toggle:

 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; }); 

And then in the controller:

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

I’ve seen some terrible observer patterns here that cause memory leaks on large applications.

I might be a little late but it’s as simple as this.

The watch function watches for reference changes (primitive types) if you want to watch something like array push simply use:

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

This will update the reference and update the watch from anywhere. Including a services getter method. Anything that’s a primitive will be updated automatically.

I am late to the part but I found a nicer way to do this than the answer posted above. Instead of assigning a variable to hold the value of the service variable, I created a function attached to the scope, that returns the service variable.

controller

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

I think this will do what you want. My controller keeps checking the value of my service with this implementation. Honestly, this is much simpler than the selected answer.

I have written two simple utility services that help me track service properties changes.

If you want to skip the long explanation, you can go strait to 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; }; } 

Have a look at this plunker:: this is the simplest example i could think of

http://jsfiddle.net/HEdJF/

 

Input is : {{Data.FirstName}}

Input should also be here: {{Data.FirstName}}
// 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; });