AngularJS: ¿Cómo ver las variables de servicio?

Tengo un servicio, por ejemplo:

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

Y me gustaría utilizar foo para controlar una lista que se representa en HTML:

 
{{ item }}

Para que el controlador detecte cuándo se actualiza aService.foo , he ensamblado este patrón donde agrego unService al $scope del controlador y luego 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; } }); } 

Esto se siente con muchas manos, y lo he estado repitiendo en cada controlador que usa las variables del servicio. ¿Hay una mejor manera de lograr ver las variables compartidas?

Siempre puede usar el patrón de buen viejo observador si desea evitar la tiranía y la sobrecarga de $watch .

En el servicio:

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

Y en el controlador:

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

En un escenario como este, donde los objetos múltiples / desconocidos pueden estar interesados ​​en los cambios, use $rootScope.$broadcast del elemento que se está modificando.

En lugar de crear su propio registro de oyentes (que deben limpiarse en varios $ destruidos), debería poder $broadcast desde el servicio en cuestión.

Aún debe codificar $on manejadores en cada oyente, pero el patrón está desacoplado de varias llamadas a $digest y, por lo tanto, evita el riesgo de los observadores de larga ejecución.

De esta manera, también, los oyentes pueden ir y venir del DOM y / o de diferentes scopes de los niños sin que el servicio cambie su comportamiento.

** actualización: ejemplos **

Las transmisiones tendrían más sentido en los servicios “globales” que podrían afectar un sinnúmero de otras cosas en su aplicación. Un buen ejemplo es un servicio de usuario donde hay una serie de eventos que podrían tener lugar, como inicio de sesión, cierre de sesión, actualización, inactividad, etc. Creo que es aquí donde las transmisiones tienen más sentido porque cualquier scope puede escuchar un evento, sin incluso inyectando el servicio, y no necesita evaluar ninguna expresión o resultados de la caché para inspeccionar los cambios. Simplemente dispara y se olvida (así que asegúrese de que sea una notificación de disparar y olvidar, no algo que requiera acción)

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

El servicio anterior transmitiría un mensaje a cada scope cuando se completara la función save () y los datos fueran válidos. Alternativamente, si se trata de un envío de $ resource o ajax, mueva la llamada de difusión a la callback para que se active cuando el servidor haya respondido. Las transmisiones se adaptan particularmente bien a ese patrón porque cada oyente simplemente espera el evento sin la necesidad de inspeccionar el scope en cada resumen de $. El oyente se vería así:

 .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 de los beneficios de esto es la eliminación de múltiples relojes. Si estuviera combinando campos o obteniendo valores como el ejemplo anterior, tendría que observar las propiedades de nombre y apellido. Ver la función getUser () solo funcionaría si el objeto del usuario se reemplazara en las actualizaciones, no se activaría si el objeto del usuario tuviera simplemente sus propiedades actualizadas. En ese caso, deberías hacer una vigilancia profunda y eso es más intenso.

$ broadcast envía el mensaje del ámbito al que se llama en cualquier ámbito secundario. Entonces, llamar desde $ rootScope se activará en todos los ámbitos. Si fuera a $ broadcast desde el scope de su controlador, por ejemplo, se dispararía solo en los ámbitos heredados del scope de su controlador. $ emitir va en la dirección opuesta y se comporta de manera similar a un evento DOM en el sentido de que se expande en la cadena de scope.

Tenga en cuenta que hay escenarios en los que $ broadcast tiene mucho sentido, y hay escenarios en los que $ watch es una mejor opción, especialmente si se trata de un scope aislado con una expresión de reloj muy específica.

Estoy usando un enfoque similar a @dtheodot, pero usando la promesa angular en lugar de pasar devoluciones de llamada

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

Luego, donde sea que solo use myService.setFoo(foo) método para actualizar foo on service. En tu controlador puedes usarlo como:

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

Los primeros dos argumentos de then son las devoluciones de llamadas de éxito y error, el tercero es notificación de callback.

Referencia por $ q.

Sin relojes ni devoluciones de llamada de observadores ( 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}}

Este JavaScript funciona cuando estamos reenviando un objeto desde el servicio en lugar de un valor. Cuando se devuelve un objeto de JavaScript de un servicio, Angular agrega relojes a todas sus propiedades.

También tenga en cuenta que estoy usando ‘var self = this’ porque necesito mantener una referencia al objeto original cuando se ejecuta $ timeout, de lo contrario ‘this’ se referirá al objeto window.

Por lo que puedo decir, no tienes que hacer algo tan elaborado como eso. ¡Ya asignó foo desde el servicio a su scope y desde que foo es una matriz (y a su vez un objeto se le asigna por referencia!). Entonces, todo lo que necesitas hacer es algo como esto:

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

Si hay alguna, otra variable en esta misma Ctrl depende de que cambie foo, entonces sí, necesitaría un reloj para observar a foo y hacer cambios a esa variable. Pero mientras sea una referencia simple, la observación es innecesaria. Espero que esto ayude.

Me encontré con esta pregunta buscando algo similar, pero creo que merece una explicación exhaustiva de lo que está sucediendo, así como algunas soluciones adicionales.

Cuando una expresión angular como la que usó está presente en el HTML, Angular configura automáticamente $watch for $scope.foo , y actualizará el HTML siempre que $scope.foo .

 
{{ item }}

El problema no dicho aquí es que una de dos cosas está afectando a un aService.foo ejemplo, los cambios no se detectan. Estas dos posibilidades son:

  1. aService.foo se establece en una nueva matriz cada vez, lo que hace que la referencia a ella sea obsoleta.
  2. aService.foo se está actualizando de tal forma que un ciclo $digest no se activa en la actualización.

Problema 1: referencias desactualizadas

Teniendo en cuenta la primera posibilidad, suponiendo que se aplica un $digest , si aService.foo siempre era el mismo conjunto, el $watch set automáticamente detectaría los cambios, como se muestra en el siguiente fragmento de código.

Solución 1-a: asegúrese de que la matriz u objeto sea el mismo objeto en cada actualización

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

Puede insertar el servicio en $ rootScope y mirar:

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

En tu controlador:

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

Otra opción es usar una biblioteca externa como: https://github.com/melanke/Watch.JS

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

Puede observar los cambios de uno, muchos o todos los atributos del objeto.

Ejemplo:

 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");​ 

Puede ver los cambios dentro de la fábrica y luego transmitir un cambio

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

Luego en tu controlador:

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

De esta manera, pones todo el código de fábrica relacionado dentro de su descripción, entonces solo puedes confiar en la transmisión desde el exterior

== ACTUALIZADO ==

Muy simple ahora en $ watch.

Pluma aquí .

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

Basándose en la respuesta de dtheodor, podría usar algo similar a lo que se muestra a continuación para asegurarse de que no se olvide de anular el registro de la callback … Sin embargo, algunos pueden objetar pasar el $scope a un servicio.

 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 es un método de extensión que se ve así:

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

Aquí está mi enfoque genérico.

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

En tu controlador

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

mientras enfrentaba un problema muy similar, observé una función en el scope y le pedí a la función que devolviera la variable de servicio. He creado un violín js . puedes encontrar el código a continuación.

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

Llegué a esta pregunta, pero resultó que mi problema era que estaba usando setInterval cuando debería haber estado usando el proveedor angular $ interval. Este es también el caso de setTimeout (use $ timeout en su lugar). 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 string 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.

controlador

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