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

414

Tengo un servicio, digamos:

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

  return service;
}]);

Y me gustaría usar foopara controlar una lista que se representa en HTML:

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

Para que el controlador detecte cuándo aService.foose actualiza, he improvisado este patrón donde agrego un servicio al controlador $scopey 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 largo, y lo he estado repitiendo en cada controlador que usa las variables del servicio. ¿Hay una mejor manera de lograr ver variables compartidas?

berto
fuente
1
Puede pasar un tercer parámetro a $ watch set en true a deep watch aService y todas sus propiedades.
SirTophamHatt
77
$ scope.foo = aService.foo es suficiente, puede perder la línea de arriba. Y lo que hace dentro de $ watch no tiene sentido, si desea asignar un nuevo valor a $ scope.foo simplemente hágalo ...
Jin
44
¿Podrías hacer referencia aService.fooen el marcado html? (como este: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks
1
He agregado un ejemplo sin Callbacks o $ relojes, vea la respuesta a continuación ( jsfiddle.net/zymotik/853wvv7s )
Zymotik
1
@ MikeGledhill, tienes razón. Creo que se debe a la naturaleza de Javascript, puede ver este patrón en muchos otros lugares (no solo en Angular, sino en JS en general). Por un lado, transfiere el valor (y no se vincula) y, por otro lado, transfiere un objeto (o el valor que hace referencia al objeto ...), y es por eso que las propiedades se actualizan correctamente (como perfectamente mostrado en el ejemplo de Zymotik arriba).
Christophe Vidal

Respuestas:

277

Siempre puede usar el viejo patrón de 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
};
dtheodor
fuente
21
@Moo escuche el $destoryevento en el alcance y agregue un método para cancelar el registro aaService
Jamie
13
¿Cuáles son las ventajas de esta solución? Necesita más código en un servicio, y algo la misma cantidad de código en un controlador (ya que también tenemos que anular el registro en $ destroy). Podría decir que la velocidad de ejecución, pero en la mayoría de los casos simplemente no importará.
Alex Che
66
No estoy seguro de cómo esta es una mejor solución que $ watch, el interrogador estaba pidiendo una forma simple de compartir los datos, se ve aún más engorroso. Prefiero usar $ broadcast que esto
Jin
11
$watchvs patrón de observador es simplemente elegir si sondear o empujar, y es básicamente una cuestión de rendimiento, así que úselo cuando el rendimiento sea importante. Utilizo un patrón de observador cuando, de lo contrario, tendría que "observar" en profundidad objetos complejos. Adjunto servicios completos al $ scope en lugar de mirar valores de servicio únicos. Evito los $ watch de angular como el diablo, ya hay suficiente de eso en las directivas y en el enlace de datos angular nativo.
dtheodor
107
La razón por la que estamos usando un marco como Angular es no cocinar nuestros propios patrones de observación.
Código Whisperer
230

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

En lugar de crear su propio registro de oyentes (que deben limpiarse en varias destrucciones de $), debería poder hacerlo $broadcastdesde el servicio en cuestión.

Todavía debe codificar los $oncontroladores en cada escucha, pero el patrón está desacoplado de múltiples llamadas $digesty, por lo tanto, evita el riesgo de observadores de larga duración.

De esta manera, también, los oyentes pueden ir y venir desde el DOM y / o diferentes ámbitos secundarios 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 innumerables 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 aquí es donde las transmisiones tienen más sentido porque cualquier ámbito puede escuchar un evento, sin incluso inyectando el servicio, y no necesita evaluar ninguna expresión o resultado de la memoria caché para inspeccionar los cambios. Simplemente se 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 = <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;
}]);

El servicio anterior transmitiría un mensaje a cada ámbito cuando la función save () se completara y los datos fueran válidos. Alternativamente, si se trata de un recurso $ o un envío ajax, mueva la llamada de difusión a la devolución de llamada para que se active cuando el servidor haya respondido. Las transmisiones se adaptan particularmente bien a ese patrón porque cada oyente solo espera el evento sin la necesidad de inspeccionar el alcance 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 estaba combinando campos o derivando valores como el ejemplo anterior, tendría que ver 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 dispararía si el objeto del usuario simplemente tuviera sus propiedades actualizadas. En cuyo caso tendrías que hacer una vigilancia profunda y eso es más intenso.

$ broadcast envía el mensaje desde el ámbito en el que se invoca a cualquier ámbito secundario. Entonces llamarlo desde $ rootScope se disparará en cada ámbito. Si tuviera que emitir $ desde el alcance de su controlador, por ejemplo, solo se dispararía en los ámbitos que heredan del alcance de su controlador. $ emit va en la dirección opuesta y se comporta de manera similar a un evento DOM, ya que burbujea en la cadena de alcance.

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 está en un ámbito aislado con una expresión de reloj muy específica.

Matt Pileggi
fuente
1
Salir del ciclo de $ digest es una buena cosa, especialmente si los cambios que estás viendo no son un valor que irá directa e inmediatamente al DOM.
XML
¿Hay alguna forma de evitar el método .save ()? Parece excesivo cuando solo está monitoreando la actualización de una sola variable en el SharedService. ¿Podemos ver la variable desde el SharedService y emitirla cuando cambia?
JerryKur
Intenté varias formas de compartir datos entre controladores, pero esta es la única que funcionó. Bien jugado, señor.
abettermap
Prefiero esto a las otras respuestas, parece menos hacky , gracias
JMK
99
Este es el patrón de diseño correcto solo si su controlador consumidor tiene múltiples fuentes posibles de datos; en otras palabras, si tiene una situación MIMO (Entrada múltiple / Salida múltiple). Si solo está usando un patrón de uno a muchos, debe usar referencias directas a objetos y dejar que el marco angular haga el enlace bidireccional por usted. Horkyze vinculó esto a continuación, y es una buena explicación del enlace bidireccional automático y sus limitaciones: stsc3000.github.io/blog/2013/10/26/…
Charles
47

Estoy usando un enfoque similar a @dtheodot pero usando 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, solo use el myService.setFoo(foo)método para actualizar fooel servicio. En su controlador puede usarlo como:

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

Los primeros dos argumentos thenson devoluciones de llamada de éxito y error, el tercero es notificar devolución de llamada.

Referencia para $ q.

Krym
fuente
¿Cuál sería la ventaja de este método sobre la transmisión $ descrita a continuación por Matt Pileggi?
Fabio
Bueno, ambos métodos tienen sus usos. Las ventajas de la transmisión para mí serían la legibilidad humana y la posibilidad de escuchar en más lugares el mismo evento. Supongo que la principal desventaja es que la transmisión está emitiendo mensajes a todos los ámbitos descendientes, por lo que puede ser un problema de rendimiento.
Krym
2
Estaba teniendo un problema en el que hacer que $scope.$watchuna variable de servicio no pareciera funcionar (el alcance que estaba viendo era un modo que heredó de $rootScope), esto funcionó. Buen truco, ¡gracias por compartir!
Seiyria
44
¿Cómo limpiarías después de ti mismo con este enfoque? ¿Es posible eliminar la devolución de llamada registrada de la promesa cuando se destruye el alcance?
Abris
Buena pregunta. Honestamente no lo se. Intentaré hacer algunas pruebas sobre cómo podría eliminar la devolución de llamada de la promesa.
Krym
41

Sin relojes ni devoluciones de llamadas 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

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

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

También tenga en cuenta que estoy usando 'var self = this' ya que necesito mantener una referencia al objeto original cuando se ejecuta el tiempo de espera $, de lo contrario 'this' se referirá al objeto de la ventana.

Zymotik
fuente
3
Este es un gran enfoque! ¿Hay alguna manera de vincular solo una propiedad de un servicio al alcance en lugar de todo el servicio? Solo hacer $scope.count = service.countno funciona.
jvannistelrooy
También puede anidar la propiedad dentro de un objeto (arbitrario) para que se pase por referencia. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross
1
Excelente enfoque! Si bien hay muchas respuestas fuertes y funcionales en esta página, esta es, con mucho, a) la más fácil de implementar yb) la más fácil de entender cuando se lee el código. Esta respuesta debería ser mucho más alta de lo que es actualmente.
CodeMoose
Gracias @CodeMoose, lo he simplificado aún más hoy para los nuevos en AngularJS / JavaScript.
Zymotik
2
Que Dios te bendiga. He perdido como millones de horas, diría. Debido a que estaba luchando con 1.5 y angularjs cambiando de 1 a 2 y también quería compartir datos
Amna
29

Me topé con esta pregunta buscando algo similar, pero creo que merece una explicación detallada 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 un $watchfor $scope.fooy actualizará el HTML cada vez que $scope.foocambie.

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

El problema no mencionado aquí es que una de las dos cosas está afectando de aService.foo tal manera que 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 no esté actualizada.
  2. aService.foose actualiza de tal manera que $digestno se desencadena un ciclo en la actualización.

Problema 1: referencias obsoletas

Teniendo en cuenta la primera posibilidad, suponiendo que $digestse esté aplicando a, si aService.foosiempre fue la misma matriz, el conjunto automático $watchdetectaría los cambios, como se muestra en el fragmento de código a continuación.

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

Como puede ver, la ng-repeat supuestamente asociada a aService.foono se actualiza cuando aService.foocambia, pero la ng-repeat adjunta a aService2.foo . Esto se debe a que nuestra referencia a aService.fooestá desactualizada, pero nuestra referencia a aService2.foono lo está. Creamos una referencia a la matriz inicial con $scope.foo = aService.foo;, que luego fue descartada por el servicio en su próxima actualización, lo que significa que $scope.fooya no hace referencia a la matriz que queríamos.

Sin embargo, si bien hay varias maneras de asegurarse de que la referencia inicial se mantenga intacta, a veces puede ser necesario cambiar el objeto o la matriz. ¿O qué pasa si la propiedad del servicio hace referencia a una primitiva como a Stringo Number? En esos casos, no podemos simplemente confiar en una referencia. Entonces, ¿qué podemos hacer?

Varias de las respuestas dadas anteriormente ya dan algunas soluciones a ese problema. Sin embargo, personalmente estoy a favor de usar el método simple sugerido por Jin y thetallweeks en los comentarios:

solo referencia aService.foo en el marcado html

Solución 1-b: Adjunte el servicio al ámbito y haga referencia {service}.{property}en el HTML.

Es decir, solo haz esto:

HTML:

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

JS:

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

De esa manera, $watchse resolverá aService.fooen cada uno $digest, lo que obtendrá el valor actualizado correctamente.

Esto es algo de lo que estaba tratando de hacer con su solución, pero de una manera mucho menos redonda. Ha añadido una innecesaria $watchen el controlador que pone explícitamente fooen el $scopecada vez que cambia. No necesita ese extra $watchcuando se adjunta en aServicelugar de aService.fooal $scopey se vincula explícitamente aService.fooen el marcado.


Ahora eso está muy bien suponiendo $digestque se esté aplicando un ciclo. En mis ejemplos anteriores, utilicé el $intervalservicio de Angular para actualizar las matrices, que inicia automáticamente un $digestbucle después de cada actualización. Pero, ¿qué pasa si las variables de servicio (por cualquier razón) no se actualizan dentro del "mundo angular". En otras palabras, ¿ no se $digestactiva automáticamente un ciclo cada vez que cambia la propiedad del servicio?


Problema 2: falta $digest

Muchas de las soluciones aquí resolverán este problema, pero estoy de acuerdo con Code Whisperer :

La razón por la que estamos usando un marco como Angular es no cocinar nuestros propios patrones de observación

Por lo tanto, preferiría seguir usando la aService.fooreferencia en el marcado HTML como se muestra en el segundo ejemplo anterior, y no tener que registrar una devolución de llamada adicional dentro del Controlador.

Solución 2: use un setter y getter con $rootScope.$apply()

Me sorprendió que nadie haya sugerido el uso de un setter y getter . Esta capacidad se introdujo en ECMAScript5 y, por lo tanto, existe desde hace años. Por supuesto, eso significa que si, por cualquier razón, necesita admitir navegadores realmente antiguos, entonces este método no funcionará, pero siento que los captadores y configuradores están muy infrautilizados en JavaScript. En este caso particular, podrían ser bastante útiles:

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

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

Aquí he añadido una variable 'privado' en la función de servicio: realFoo. Esto se actualiza y recupera utilizando las funciones get foo()y set foo()respectivamente en el serviceobjeto.

Tenga en cuenta el uso de $rootScope.$apply()en la función set. Esto asegura que Angular estará al tanto de cualquier cambio en service.foo. Si obtiene errores 'inprog', consulte esta página de referencia útil , o si usa Angular> = 1.3, simplemente puede usar $rootScope.$applyAsync().

También tenga cuidado con esto si aService.foose actualiza con mucha frecuencia, ya que eso podría afectar significativamente el rendimiento. Si el rendimiento fuera un problema, podría configurar un patrón de observación similar a las otras respuestas aquí usando el configurador.

NanoWizard
fuente
3
Esta es la solución correcta y más fácil. Como dice @NanoWizard, $ digest servicesno busca propiedades que pertenezcan al servicio en sí.
Sarpdoruk Tahmaz
28

Por lo que puedo decir, no tienes que hacer algo tan elaborado como eso. Ya ha asignado foo desde el servicio a su ámbito y dado 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 alguna, otra variable en este mismo Ctrl depende del cambio de foo, entonces sí, necesitaría un reloj para observarlo y hacer cambios a esa variable. Pero mientras sea una referencia simple, la observación es innecesaria. Espero que esto ayude.

ganaraj
fuente
35
Lo intenté y no pude $watchtrabajar con un primitivo. En su lugar, he definido un método en el servicio que devolvería el valor simple: somePrimitive() = function() { return somePrimitive }Y he asignado una propiedad de $ alcance a ese método: $scope.somePrimitive = aService.somePrimitive;. Luego usé el método de alcance en el HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok
44
@ MarkRajcok No, no use primitivas. Añádelos a un objeto. Las primitivas no son mutables, por lo que el enlace de datos de 2 vías no funcionará
Jimmy Kane
3
@JimmyKane, sí, las primitivas no deberían usarse para el enlace de datos bidireccional, pero creo que la pregunta era sobre ver las variables de servicio, no configurar el enlace bidireccional. Si solo necesita mirar una propiedad / variable de servicio, no se requiere un objeto; se puede usar una primitiva.
Mark Rajcok
3
En esta configuración, puedo cambiar los valores de aService desde el ámbito. Pero el alcance no cambia en respuesta al cambio de aService.
Ouwen Huang
44
Esto tampoco me funciona. Simplemente asignar $scope.foo = aService.foono actualiza la variable de alcance automáticamente.
Darwin Tech
9

Puede insertar el servicio en $ rootScope y ver:

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");​
Rodolfo Jorge Nemer Nogueira
fuente
2
¡NO PUEDO CREER QUE ESTA RESPUESTA NO ES LA MÁS VOTADA! Esta es la solución más elegante (IMO) ya que reduce la entropía informativa y probablemente mitiga la necesidad de controladores de mediación adicionales. Votaría esto más si pudiera ...
Cody
Agregando todos sus servicios al $ rootScope, sus beneficios y sus posibles dificultades, se detallan aquí: stackoverflow.com/questions/14573023/…
Zymotik
6

Puede ver los cambios dentro de la propia 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 su controlador:

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

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

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

Flavien Volken
fuente
6

== ACTUALIZADO ==

Muy simple ahora en $ watch.

Pluma aquí .

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 + '!');
  });
}]);
hayatbiralem
fuente
1
Me gusta PostmanService, pero ¿cómo debo cambiar la función $ watch en el controlador si necesito escuchar más de una variable?
jedi
Hola jedi, gracias por el aviso! Actualicé el bolígrafo y la respuesta. Recomiendo agregar otra función de reloj para eso. Entonces, agregué una nueva función a PostmanService. Espero que esto ayude :)
hayatbiralem
En realidad, sí lo es :) Si compartes más detalles del problema, tal vez pueda ayudarte.
hayatbiralem
4

Basándose en la respuesta de dtheodor, podría usar algo similar a lo siguiente para asegurarse de que no se olvide de anular el registro de la devolución de llamada ... Sin $scopeembargo, algunos pueden oponerse a pasar el servicio 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;
};
Jamie
fuente
2

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 su 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'];
oportunidad de elite
fuente
2

Para aquellos como yo que solo buscan una solución simple, esto hace casi exactamente lo que espera de usar $ watch normal en los controladores. La única diferencia es que evalúa la cadena en su contexto de JavaScript y no en un ámbito específico. Tendrá que inyectar $ rootScope en su servicio, aunque solo se usa para conectar correctamente los ciclos de resumen.

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

Mientras enfrentaba un problema muy similar, vi una función en su alcance y la función devolvió 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++;
    });
});
chandings
fuente
2

Llegué a esta pregunta, pero resultó que mi problema era que estaba usando setInterval cuando debería haber estado usando el proveedor de intervalo angular $. Este también es el caso de setTimeout (use $ timeout en su lugar). Sé que no es la respuesta a la pregunta del OP, pero podría ayudar a algunos, ya que me ayudó.

honkskillet
fuente
Puede usar setTimeout, o cualquier otra función no angular, pero no olvide envolver el código en la devolución de llamada $scope.$apply().
magnetronnie
2

He encontrado una solución realmente excelente en el otro hilo con un problema similar pero un enfoque totalmente diferente. Fuente: AngularJS: $ watch dentro de la directiva no funciona cuando se cambia el valor de $ rootScope

Básicamente, la solución allí le dice que NO la use, $watchya que es una solución muy pesada. En su lugar , proponen usar $emity $on.

Mi problema era observar una variable en mi servicio y reaccionar en la directiva . ¡Y con el método anterior es muy fácil!

Mi módulo / ejemplo de servicio:

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

Así que básicamente miro mi user - cada vez que se establece en un nuevo valor, $emitun user:changeestado.

Ahora en mi caso, en la directiva que utilicé:

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

Ahora en la directiva escucho sobre $rootScopey sobre el cambio dado, reacciono respectivamente. Muy facil y elegante!

Atais
fuente
1

// servicio: (nada especial aquí)

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;
  });
});
Nawlbergs
fuente
1

Un poco feo, pero he agregado el registro de variables de alcance a mi servicio para alternar:

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

Y luego en el controlador:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}
nclu
fuente
Gracias. Una pequeña pregunta: ¿por qué regresas thisde ese servicio en lugar de self?
shrekuu
44
Porque a veces se cometen errores. ;-)
nclu
Buena práctica para return this;salir de sus constructores ;-)
Cody
1

Echa un vistazo a este plunker :: este es el ejemplo más simple que se me ocurre

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;
});
anandharshan
fuente
0

He visto algunos patrones de observación terribles que causan pérdidas de memoria en aplicaciones grandes.

Puede que llegue un poco tarde, pero es tan simple como esto.

La función de observación busca cambios de referencia (tipos primitivos) si desea ver algo como la inserción de matriz simplemente use:

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

Esto actualizará la referencia y actualizará el reloj desde cualquier lugar. Incluyendo un método de obtención de servicios. Todo lo que sea primitivo se actualizará automáticamente.

Oliver Dixon
fuente
0

Llego tarde a la parte, pero encontré una mejor manera de hacerlo que la respuesta publicada anteriormente. En lugar de asignar una variable para contener el valor de la variable de servicio, creé una función adjunta al ámbito, que devuelve la variable de servicio.

controlador

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

Creo que esto hará lo que quieras. Mi controlador sigue verificando el valor de mi servicio con esta implementación. Honestamente, esto es mucho más simple que la respuesta seleccionada.

alaboudi
fuente
por qué fue rechazado ... También he usado una técnica similar muchas veces y ha funcionado.
indefinido
0

He escrito dos servicios de utilidad simples que me ayudan a rastrear los cambios en las propiedades del servicio.

Si desea omitir la larga explicación, puede ir directamente 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;
  };
}

Este servicio se utiliza en el controlador de la siguiente manera: supongamos que tiene un servicio "testService" con las propiedades 'prop1', 'prop2', 'prop3'. Desea ver y asignar al ámbito 'prop1' y 'prop2'. Con el servicio de reloj se verá así:

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. aplicar Watch obj es excelente, pero no es suficiente si tiene un código asincrónico en su servicio. Para ese caso, uso una segunda utilidad que se ve así:

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

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

Lo activaría al final de mi código asíncrono para activar el bucle $ digest. Como eso:

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

Entonces, todo eso junto se verá así (puede ejecutarlo o abrir el violín ):

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

Max
fuente