¿Cómo puedo comunicar un object de configuration de un controller a los services?

tl; dr;

Necesito comunicar el estado que varios services necesitan y origina en datos vinculados al scope de un controller. ¿Qué sería una buena forma de hacer zen Angular?

Volver la historia

Estoy desarrollando una aplicación de una sola página y después de pensarlo mucho he decidido usar AngularJS. Las páginas se presentan de una manera similar a:

enter image description here

El layout real no importa mucho, el concepto sigue siendo el mismo para layouts similares. Necesito comunicar información que está vinculada al scope de SettingsController a los services que requieren los controlleres en ngView . También necesito actualizar el contenido obtenido del service en el controller cuando los usuarios hacen una modificación a cualquier control deslizante.

Lo que he intentado

La única forma en que he pensado es algo así como: http://jsfiddle.net/5sNcG/ donde tengo que escribir un enlace y agregar oyentes al cambio de scope. Probablemente estoy muy lejos de aquí y hay una manera obvia de hacerlo, sin embargo, a pesar de mis esfuerzos, no puedo encontrarlo.

 /code from fiddle. var app = angular.module("myApp",[]); app.controller("HomeCtrl",function($scope,FooService,$interval){ FooService.change(function(){ console.log("HI",FooService.getFoo()); $scope.foo = FooService.getFoo(); }); }); app.factory("Configuration",function(){ var config = {data:'lol'}; var callbacks = []; return { list:function(){ return config;}, update:function(){ callbacks.forEach(function(x){ x();}); }, change:function(fn){ callbacks.push(fn); // I never remove these, so this is a memory leak! } } }); app.service("FooService",function(Configuration){ return { getFoo: function(){ return Configuration.list().data+" bar"; },change:function(fn){ Configuration.change(fn); } } }); app.controller("SettingsCtrl",function($scope,Configuration){ $scope.config = Configuration.list(); $scope.$watch('config',function(){ Configuration.update(); },true); }); 

También he considerado las transmisiones de $rootScope pero parece un estado más global

No importa lo que bash, tengo un singleton con estado global y todos sabemos que no quiero un singleton .

Dado que esto parece un caso de uso angular bastante común. ¿Cuál es la forma idiomática de resolver este problema?

4 Solutions collect form web for “¿Cómo puedo comunicar un object de configuration de un controller a los services?”

Me he enfrentado a algo similar en el pasado, y he considerado cuatro enfoques posibles:

  • Usando $broadcast
  • Almacenamiento de configuraciones en $rootScope
  • Patrón de observador (como lo tienes aquí)
  • Usando $watch desde un controller

Aquí están mis pensamientos sobre cada uno:

$ emisión

En una presentación de AngularJS que vi, Miško Hevery habló sobre el uso de $broadcast (es decir, events) y los casos de uso para tal. Lo esencial es que $broadcast tiene más la intención de reactjsr a events que no están estrechamente relacionados con lo que sea que esté trabajando, de lo contrario, es preferible una alternativa. También sobre este tema, la guía de Mejores Prácticas en el wiki angular recomienda que:

Solo use. $ Broadcast (),. $ Emit () y. $ On () para events atómicos: events que son relevantes globalmente en toda la aplicación (como la authentication de un usuario o el cierre de la aplicación).

Aquí, como tiene configuraciones que están estrechamente asociadas a lo que puebla a ng-view , sugeriría una alternativa al uso de $broadcast es preferible.

$ rootScope

Este es un estado global como usted menciona (y desea evitar). No era / no es mi preference personal exponer configuraciones a toda mi aplicación, a pesar de que a menudo es la opción fácil. Personalmente, reservo $rootScope para la configuration y las variables "suaves", como el título de la página, etc. No elegiría usar esta opción.

Patrón de observador

El logging de devoluciones de llamadas frente a la fábrica de Configuración es un enfoque sólido. Con respecto a sus repeticiones de llamada persistentes, puede escuchar el evento $destroy en el scope, llamando a un método de remove en su fábrica de Configuración para eliminar la callback. Esto podría considerarse un buen ejemplo de cómo se usa $broadcast ; el controller se preocupa por el evento y debe reactjsr ante él, pero el evento en sí no es específico de los datos compartidos por los controlleres / Servicio de configuration.

$ reloj

Al usar un service compartido, se puede inyectar en cualquier controller relacionado con la configuration. En este momento, cualquier cambio en la configuration activará su callback, cuando tal vez algunas vistas solo pueden estar relacionadas con una o dos configuraciones. $watch le permitirá observar más fácilmente los cambios solo en esos attributes. No puedo hablar de la sobrecarga frente al logging de devoluciones de llamadas, pero esta parece ser la forma más 'angular' para mí.

Así es como esto podría implementarse usando $watch :

 var app = angular.module("myApp",[]); app.factory("Configuration",function(){ var data = { settingOne: true, settingTwo: false }; return data; }) app.controller("SettingsCtrl",function($scope, Configuration){ // do something }) app.controller("HomeCtrl",function($scope, Configuration){ // detect any change to configuration settings $scope.$watch(function() { return Configuration; }, function(data) { // do something }, true) // alternatively only react to settingTwo changing $scope.$watch(function() { return Configuration.settingTwo }, function(data) { // do something }) }) 

Tenga en count que si requiriera una fábrica de Configuración un poco más complicada, podría cambiar a usar methods getter / setter y mantener los ajustes de configuration en privado. Luego, en $watch , debería ver la llamada al método en lugar de la propiedad en sí.

ACTUALIZAR:

Al momento de responder, prefería el enfoque de un $watch dentro de un controller. Después de un time desarrollando con el marco, ahora trato de mantener $watch fuera del controller, prefiriendo, en lo posible, invocar directamente una function en el punto de cambio del valor, o mediante el aprovechamiento de ng-change .

Una razón para esto es la complejidad que agrega al probar el controller, pero tal vez sea más ineficiente: por cada $digest cycle cycle angular, cada $watch registrado será evaluado independientemente, y puede estar respondiendo a un cambio realizado en un valor con un $watch existente.

En lugar de condenar las desventajas y las soluciones en esta perspectiva, aquí hay un artículo muy bueno sobre exactamente este problema: Angular JS: probablemente no deba usar $ watch en sus controlleres .

ACTUALIZACIÓN 🙂

Hice un demo plunker: http://plnkr.co/edit/RihW4JFD8y65rDsoGNwb?p=preview


Hay muchas maneras diferentes y esta pregunta es de alguna manera muy amplia, pero quería compartir otro enfoque.

Su problema comienza con la forma en que se comunica entre los controlleres y los services.

En lugar de crear services como objects con methods (que lo obligan a usar el patrón de observador), puede dirigir los ámbitos directamente a los services creando services de objects de datos y dejar que $digest haga el trabajo por usted.

La misma razón por la que angulares usa $ scope es para permitirle usar POJO en lugar de un patrón de observador como otros frameworks. Cuando crea este tipo de services impulsados ​​por methods, introduce el mismo patrón que angular intenta evitar.

Es importante tener en count que debe apuntar a las properties en el object de service y no a la reference del object en sí.

Esto es un ejemplo:

 app.factory('FooService', function($rootScope, Configuration){ $scope = $rootScope.$new(); $scope.Configuration = Configuration; var foo = { data : null }; $scope.$watch('Configuration', function(config){ foo.data = // do something with config }, true) return foo; }); app.factory('Configuration', function(){ return { data : 'lol' } }); app.controller('SettingsCtrl', function($scope, Configuration){ $scope.config = Configuration; }); app.controller("HomeCtrl",function($scope, FooService){ $scope.foo = FooService; }); 

En realidad, prefiero el uso de $scope.$broadcast principalmente por motivos de performance, pero también porque es la forma más elegante de compartir estados en diferentes partes de la aplicación. Realmente no me importan los estados globales, uso espacios de nombres para mis events.

Pregunta muy interesante.

Hemos estado usando angular durante algunos meses y actualmente estamos considerando cómo hacer esto mejor. Todavía estamos tratando de descubrir cuál puede ser una solución óptima, quizás esto ayude a llegar allí.

Creo que la solución original que ha proporcionado es bastante similar, pero hay algunas consideraciones que deben tenerse en count:

  1. Según lo que hemos experimentado, la mayoría de los cambios en la configuration requieren una lógica específica y, por lo tanto, un manejador dedicado.
  2. La configuration debe cambiarse solo después de que se complete / valide / guarde el cambio (por ejemplo, si muchos elementos están enlazados al usuario, no deberían cambiar hasta que se complete el cambio)
  3. Usando rootScope. $ Emitir y rootScope. $ En dar una buena implementación de pub / sub. Se pueden usar convenciones simples para espacios de nombres de los events

También creo que usar el service compartido que se inyecta cuando es necesario es el path a seguir.

Modifiqué el genial ejemplo de Plunker de Ilan Frumer: http://plnkr.co/edit/YffbhCMJbTPdcjZDl0UF?p=preview

Descomponer el problema en dos puede ayudar a pensar en lo que puede ser una solución.

Actualizar el service de configuration con los cambios realizados en la página de configuration

Para esto, usar un reloj $ se ve como la solución óptima, esperas que la configuration específica sea un cambio y, como respuesta, el service de configuration sabe qué se ha cambiado. Prefiero hacerlo explícitamente, para mantener el flujo del cambio claro y consistente.

Esto se puede hacer haciendo una copy local de los datos de configuration y observando los cambios.

 app.factory('Configuration', function($rootScope){ return { var config = { user: "xxxx" } return { config: config, set: function(item, value) { config[item] = value; $rootScope.$emit("configChanged." + item); }, changed: function(item, callback, scope) { var deregister = $rootScope.$on("configChanged." + item, function() { callback(config[item], config) }); callback(config[item], config); if (scope) { scope.$on("$destroy", deregister); } } } } }); app.controller('SettingsCtrl', function($scope, $timeout, Configuration){ // Get a local copy - configuration shouldn't change until change // is completed $scope.data = angular.copy(Configuration.config); // Keep UI interactions in the controller // If more complex UI is requinetworking another way could even use a // directive for this $scope.$watch("data.user", function(user) { Configuration.set('user', $scope.data.user); }); }); app.factory('DetailsService', function(Configuration, $http){ var details = { data : null, }; Configuration.changed("user", function(user) { // Handle user change ... }); }); 

Cómo los services / controlleres observan cambios

Esto también tiene dos opciones.

  1. Cuando no se requiere una lógica específica, se puede realizar un enlace simple a la configuration
  2. Cuando se debe hacer una lógica específica, regístrese para el cambio de evento (como en el service Details anterior)

En caso de que haya services de múltiples estados, otra optimization puede ser extraer las funciones "establecer", "cambiar" a una implementación genérica.

Espero eso ayude.

Tienes dos opciones:

  1. Use el service compartido en combinación con $watch . Es decir, lo que implementó, o puso de manera diferente el Patrón de Mediador .
  2. Use $scope.$broadcast y / o $scope.$emit con $scope.$on events para comunicar cambios.

Personalmente, no veo nada de malo en la opción (1). Tampoco considero que sea un estado global en el sentido tradicional. Su configuration está contenida en el service de Configuration , solo puede acceder a este service a través de inyección DI lo que lo hace comprobable.

Una mejora posible es crear un service ConfigMediator y mover la funcionalidad de actualización y callback para separar las preocupaciones.

  • ¿MV * en polymer, models y services como elementos poliméricos?
  • pregunta simple de javascript
  • Javascript: mejor patrón Singleton
  • Patrón de Singleton javascript
  • Cómo crear un service de Singleton en Angular 2 y leer los parameters de URL
  • Patrones singleton de JavaScript: ¿diferencias?
  • Buen patrón Singleton en JavaScript
  • Módulo Singleton e import del object jQuery global
  • ¿Cómo preservar javascript "este" context dentro del patrón singleton?
  • ¿Forma más simple / más limpia de implementar singleton en JavaScript?
  • Únton de JavaScript semántico con constructor
  • Javascript tiene muchos buenos JS marco (como Node.js AngularJS Vue.js React.js) es el mejor lenguaje de script.