How can I pass the settings object from the controller to the services?

TL; DR;

I need to report the state that several services require and comes from data related to the scope of the controller. What would be a good and Angular zen 'way to do this?

Background story

I am developing a one-page application and after much deliberation decided to use AngularJS. Pages are laid out as follows:

enter image description here

The actual layout does not matter much, the concept remains the same for similar layouts. I need to communicate information that is bound to the SettingsController scope to the services that the controllers require in ngView . I also need to update the content received from the service in the controller when users make changes to any slider.

What i tried

The only way I thought about is something like: http://jsfiddle.net/5sNcG/ , where I need to write a binding and add listeners to change the scope. I probably left here, and there is an obvious way for Angular to do this, however, despite my efforts, I cannot find it.

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

I also looked at $rootScope , but this seems like a more global state

No matter what I try, I have a singleton with a global state , and we all know. I don't need a singleton .

Since this seems like a pretty common use case for Angular. What is the idiomatic way to solve this problem?

+6
source share
4 answers

I came across something similar in the past and looked at four possible approaches:

  • Using $broadcast
  • Saving $rootScope
  • Observer pattern (as you are here)
  • Using $watch from the controller

Here are my thoughts on every occasion:

$ broadcast

In the AngularJS view, I saw that Mishko Hevery talked about using $broadcast (i.e. events) and use cases for this. The bottom line is that $broadcast more designed to respond to events that are not closely related to what you are working with, otherwise an alternative is most likely preferable. Also on this subject, the Best Practices guide in the angular wiki recommends:

Use only $ broadcast () ,. $ emit () and. $ on () for atomic events: Events that are applicable globally throughout the application (for example, user authentication or application closure).

Here, since you have settings that are closely related to what ng-view fills, this suggests that it is preferable to use $broadcast .

$ rootScope

This is the global state that you mention (and want to avoid). I was not / am not my personal preference to either open the settings for my entire application, despite the fact that this was often a simple option. I personally reserved $rootScope for configuration settings and soft variables such as page name, etc. I would not decide to use this option.

Observer Pattern

Registering factory callbacks is a robust approach. As for your persistent callbacks, you can listen for the $destroy event in the scope by calling the remove method on your Factory Configuration to remove the callback. This can be considered a good example of using $broadcast ; the controller refers to the event and must respond to it, but the event itself does not apply to data common to the controllers / configuration service.

$ watch

Using a common service, it can be entered into any controller that matches the settings. Right now, any change to config will trigger your callback when perhaps some views may concern only one or two configuration parameters. $watch will make it easier for you to watch changes for these attributes only. I can’t talk with the overhead and register callbacks, but for me it seems like the most β€œangular” one.

Here's how to do it with $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 }) }) 

Note that if you need a slightly more complex factory configuration, you can switch to getter / setter methods and keep the configuration settings private. Then in $watch you should watch the method call instead of the property itself.

UPDATE:

During the response, I preferred the a $watch approach in the controller. After some time, developing using the framework, I now try to completely leave $watch from the controller, instead preferring, where possible, direct access to the function at the point of changing the value or by using ng-change .

One of the reasons for this is the complexity that it adds to testing the controller, but perhaps moreover, that it is inefficient: every $digest angular cycle causes, every registered $watch will be evaluated independently, and it can respond very well to a change made into value with existing $watch .

Instead of judging the flaws and solutions from this point of view, there is a very good article in this problem: Angular JS - you probably t use $ watch in your controllers .

+6
source

UPDATE :)

I made a demo plunker: http://plnkr.co/edit/RihW4JFD8y65rDsoGNwb?p=preview


There are many different ways, and this question is somehow very broad, but I would like to share a different approach.

Your problem begins with how you communicate between controllers and services.

Instead of creating services as objects using methods (which force you to use the observer pattern), you can point out the scope directly to services by creating data object services, and $digest does the work for you.

The very reason angular uses $ scope is because it allows you to use a POJO, not an observer pattern, like other frameworks. When you create such method-driven services, you introduce the same pattern that angular avoids.

It is important to note that you must point to the properties of the service object, and not to the link to the object itself.

This is an example:

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

In fact, I prefer to use $scope.$broadcast mainly for performance reasons, and also because it is the most elegant way to share states across different parts of the application. I really don't care about global states; I use namespaces for my events.

+4
source

Very interesting question.

We have been using angular for several months and are now considering how to do this better. We are still trying to figure out what might be the best solution, perhaps it will help in getting it.

I think the original solution you provided is pretty similar, but there are a few considerations that should be taken:

  • From what we experienced, most of the changes in the settings require a certain logic and, therefore, dedicated handlers.
  • The configuration should only be changed after the changes are completed / valid / saved (for example, if a lot of items are attached to the user, they should not be changed until the change is completed)
  • Using rootScope. $ emit and rootScope. $ to ensure good pub / sub implementation. Simple conventions can be used for event namespaces.

I also believe that using the shared service, which is introduced when necessary, is the way to go.

I changed the example of Ilan Frumer cool plunker: http://plnkr.co/edit/YffbhCMJbTPdcjZDl0UF?p=preview

Breaking the problem in two can help determine what can be the solution.

Configuration service update with changes made on the settings page

To do this, using $ watch looks like the optimal solution, you expect the specific config to be changed, and as a response, let the configuration service know what has been changed. I prefer to do this explicitly to keep the flow of change clear and consistent.

This can be done by creating a local copy of the configuration data and observing the changes.

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

How changes in services / controllers are observed

It also has two options.

  • If no specific logic is required, simple configuration binding can be performed.
  • When you need to perform certain logic, register for an event change (for example, in the DetailsService above)

If there are multiple service states, another optimization may be to extract the "installed", "changed" functions into the overall implementation.

Hope this helps.

+3
source

You have two options:

  • Use a shared service in conjunction with $watch . That is, what you implemented, otherwise, the "Mediator" .
  • Use $scope.$broadcast and / or $scope.$emit with $scope.$on events for messaging.

Personally, I do not see anything wrong with option (1). I also do not think that this is a global state in the traditional sense. Your configuration is contained in the Configuration service, you can only access this service with a DI injection, which makes it suitable for testing.

One possible improvement is the creation of the ConfigMediator service and the transfer of update and callback functionality to it to share problems.

+1
source

All Articles