Is binding objects to angular's $rootScope in a service bad?
Asked Answered
T

5

32

In angular, I have an object that will be exposed across my application via a service.

Some of the fields on that object are dynamic, and will be updated as normal by bindings in the controllers that use the service. But some of the fields are computed properties, that depend on the other fields, and need to be dynamically updated.

Here's a simple example (which is working on jsbin here). My service model exposes fields a, b and c where c is calculated from a + B in calcC(). Note, in my real application the calculations are a lot more complex, but the essence is here.

The only way I can think to get this to work, is to bind my service model to the $rootScope, and then use $rootScope.$watch to watch for any of the controllers changing a or b and when they do, recalculating c. But that seems ugly. Is there a better way of doing this?


A second concern is performance. In my full application a and b are big lists of objects, which get aggregated down to c. This means that the $rootScope.$watch functions will be doing a lot of deep array checking, which sounds like it will hurt performance.

I have this all working with an evented approach in BackBone, which cuts down the recalculation as much as possible, but angular doesn't seem to play well with an evented approach. Any thoughts on that would be great too.


Here's the example application.

var myModule = angular.module('myModule', []);

//A service providing a model available to multiple controllers
myModule.factory('aModel', function($rootScope) {
  var myModel = {
    a: 10,
    b: 10,
    c: null
  };

  //compute c from a and b
  calcC = function() {
    myModel.c = parseInt(myModel.a, 10) * parseInt(myModel.b, 10);
  };

  $rootScope.myModel = myModel;
  $rootScope.$watch('myModel.a', calcC);
  $rootScope.$watch('myModel.b', calcC);

  return myModel;
});


myModule.controller('oneCtrl', function($scope, aModel) {
  $scope.aModel = aModel;
});

myModule.controller('twoCtrl', function($scope, aModel) {
  $scope.anotherModel = aModel;
});
Truc answered 28/1, 2013 at 23:15 Comment(2)
Do you need to look for changes to the individual elements of arrays a and b? If not, maybe you can just $watch the array lengths instead of doing deep checking.Tephra
Angular handles your events for you. When $scope changes, all your watch expressions are dirty checked. If you are dealing with large data tables that don't change often, you may wish to investigate one-time binding.Bakke
C
7

Although from a high level, I agree with the answer by bmleite ($rootScope exists to be used, and using $watch appears to work for your use case), I want to propose an alternative approach.

Use $rootScope.$broadcast to push changes to a $rootScope.$on listener, which would then recalculate your c value.

This could either be done manually - i.e. when you would be actively changing a or b values, or possibly even on a short timeout to throttle the frequency of the updates. A step further from that would be to create a 'dirty' flag on your service, so that c is only calculated when required.

Obviously such an approach means a lot more involvement in recalculation in your controllers, directives etc - but if you don't want to bind an update to every possible change of a or b, the issue becomes a matter of 'where to draw the line'.

Choctaw answered 18/2, 2013 at 21:57 Comment(0)
U
4

I must admit, the first time I read your question and saw your example I thought to myself "this is just wrong", however, after looking into it again I realized it wasn't so bad as I thought it would be.

Let's face the facts, the $rootScope is there to be used, if you want to share anything application-wide, that's the perfect place to put it. Of course you will need to careful, it's something that's being shared between all the scopes so you don't want to inadvertently change it. But let's face it, that's not the real problem, you already have to be careful when using nested controllers (because child scopes inherit parent scope properties) and non-isolated scope directives. The 'problem' is already there and we shouldn't use it as an excuse not follow this approach.

Using $watch also seems to be a good idea. It's something that the framework already provides you for free and does exactly what you need. So, why reinventing the wheel? The idea is basically the same as an 'change' event approach.

On a performance level, your approach can be in fact 'heavy', however it will always depend on the frequency you update the a and b properties. For example, if you set a or b as the ng-model of an input box (like on your jsbin example), c will be re-calculated every time the user types something... that's clearly over-processing. If you use a soft approach and update a and/or b solely when necessary, then you shouldn't have performance problems. It would be the same as re-calculate c using 'change' events or a setter&getter approach. However, if you really need to re-calculate c on real-time (i.e: while the user is typing) the performance problem will always be there and is not the fact that you are using $rootScope or $watch that will help improve it.

Resuming, in my opinion, your approach is not bad (at all!), just be careful with the $rootScope properties and avoid ´real-time´ processing.

Undue answered 29/1, 2013 at 16:11 Comment(9)
I have to disagree. You said "if you want to share anything application-wide, [$rootScope is] the perfect place to put it". No, that's what services are for. Not all scopes inherit from each other, but all inherit from $rootScope. The "problem" may already be there, but it's certainly not amplified to the degree to which you and the OP are suggesting. Further, calculating "c" every time "a" or "b" changes is totally unnecessary - some views may need that (in which case it can easily occur) but most only need to recalculate "c" when they need it. Bad architecture, bad performance.Farandole
@JoshDavidMiller I understand your point of view, but all solutions have their own drawbacks. For example, your solution is based on the fact that a and b will be changing lots of times, but have you though on the opposite scenario? Imagine that you don't change a and b at all but you access c 30 times in 30 different places of the app, in that case you will be calculating c 29 times more than necessary. And why do you assume that everything that goes into the $rootScope is bad? If it's a model (despite being hold by a service) why don't share it on the scope?Undue
Let me just clarify 2 other things. Fist, child scopes do inherit properties from their parent scopes (and all the way up to the $rootScope). That's the all idea of prototypical inheritance (as explained here). Second, I'm not saying that one approach is better than the other, I'm just showing what @Truc should be aware of when choosing any of the approaches.Undue
Hi! My solution is irrespective of the number of times b and c change. The PS in my post specifically addressed that case. I also don't assume everything in $rootScope is bad, but I do not see any use case where the same exact model will be manipulated in every view (or even in most views!) of our app. So why are we polluting every single scope with a model it probably doesn't even need? And if we have 5 or 10 or 20 models, are we going to throw them all in $rootScope? I'm saying that as a design pattern, it may not be the best option architecturally. But it won't blow up. :-)Farandole
Exactly, your PS is basically the same solution that the OP is presenting, the difference is that you don't need to use custom getters and setters. Using g&s or using $watch is, from a macro point of view, almost the same thing. Calculate c inside a a or b setter is almost the same as watching for changes on one of those properties and calculate c afterwards. As far as I can see, we agree that performance is not provided by the approach itself but by an application specific requirement: How often c will need to be calculated.Undue
Regarding the $rootScope, I state my own post "if you want to share anything application-wide, that's the perfect place to put it". I'm not taking any assumptions here. If the OP wants to share some model application-wide, let him do it. If it "pollutes" scopes, well, that's also what I've explained on my answer, child scopes are always "polluted" by their own parent scopes, that's why I think we shouldn't use it as an excuse. It's more a question of careful programming than a design pattern issue =)Undue
Hmm. I seem to be having trouble communicating this. I never in any comment nor in my answer said my problem was when c was calc'd; not sure why you're focusing on that. My problem is where c is calc'd. A model doesn't belong in a scope that doesn't need it. "Always polluted"? Emphatically no, they are not. They inherit. Prot inheritance implies that we shouldn't arbitrarily nest our scopes; a scope is a child of another because it relates! Otherwise why have multiple scopes? AngularJS already has a mechanism for this very case - why force something with drawbacks to work?Farandole
"I never in any comment nor in my answer said my problem was when c was calc'd": Neither have I =) . "My problem is where c is calc'd": By where do you mean the object that holds the calc function itself? In all solutions c is calc'd inside the service, so I'm not getting the point here. "A model doesn't belong in a scope that doesn't need it": Have you ever reused ctrls/directives in different places of your app? Assuming that scope's parents will always be the same is just wrong. (want to start a chat?)Undue
Hopping in late here. Angular is a little slow compared to other frameworks such as Backbone, but Angular has never been about speed, it's about developer convenience, iterating fast, and getting the product out there. Making your code more complex in order to make it faster defeats the point of Angular. If blazing performance is a requirement, consider a different project. If fast iteration and developer productivity is the requirement, Angular is the correct tool.Bakke
H
3

I realize this is a year and a half later, but since I've recently had the same decision to make, I thought I'd offer an alternative answer that "worked for me" without polluting $rootScope with any new values.

It does, however still rely on $rootScope. Rather than broadcasting messages, however, it simply calls $rootScope.$digest.

The basic approach is to provide a single complex model object as a field on your angular service. You can provide more than as you see fit, just follow the same basic approach, and make sure each field hosts a complex object whose reference doesn't change, i.e. don't re-assign the field with a new complex object. Instead, only modify the fields of this model object.

var myModule = angular.module('myModule', []);

//A service providing a model available to multiple controllers
myModule.service('aModel', function($rootScope, $timeout) {
  var myModel = {
    a: 10,
    b: 10,
    c: null
  };

  //compute c from a and b
  calcC = function() {
    myModel.c = parseInt(myModel.a, 10) * parseInt(myModel.b, 10);
  };
  calcC();

  this.myModel = myModel;

  // simulate an asynchronous method that frequently changes the value of myModel. Note that
  // not appending false to the end of the $timeout would simply call $digest on $rootScope anyway
  // but we want to explicitly not do this for the example, since most asynchronous processes wouldn't 
  // be operating in the context of a $digest or $apply call. 
  var delay = 2000; // 2 second delay
  var func = function() {
    myModel.a = myModel.a + 10;
    myModel.b = myModel.b + 5;
    calcC();
    $rootScope.$digest();
    $timeout(func, delay, false);
  };
  $timeout(func, delay, false);
});

Controllers that wish to depend on the service's model are then free to inject the model into their scope. For example:

$scope.theServiceModel = aModel.myModel;

And bind directly to the fields:

<div>A: {{theServiceModel.a}}</div>
<div>B: {{theServiceModel.b}}</div>
<div>C: {{theServiceModel.c}}</div>

And everything will automatically update whenever the values update within the service.

Note that this will only work if you inject types that inherit from Object (e.g. array, custom objects) directly into the scope. If you inject primitive values like string or number directly into scope (e.g. $scope.a = aModel.myModel.a) you will get a copy put into scope and will thus never receive a new value on update. Typically, best practice is to just inject the whole model object into the scope, as I did in my example.

Haerle answered 2/6, 2014 at 15:35 Comment(0)
F
2

In general, this is probably not a good idea. It's also (in general) bad practice to expose the model implementation to all of its callers, if for no other reason than refactoring becomes more difficult and onerous. We can easily solve both:

myModule.factory( 'aModel', function () {
  var myModel = { a: 10, b: 10 };

  return {
    get_a: function () { return myModel.a; },
    get_b: function () { return myModel.a; },
    get_c: function () { return myModel.a + myModel.b; }
  };
});

That's the best practice approach. It scales well, only gets called when it's needed, and doesn't pollute $rootScope.

PS: You could also update c when either a or b is set to avoid the recalc in every call to get_c; which is best depends on your implementation details.

Farandole answered 29/1, 2013 at 0:54 Comment(6)
Hi @josh-david-miller. Can you explain what you mean by "It's also (in general) bad practice to expose the model implementation to all of its callers"? Given that the angular way seems to be to do two way binding directly on model attributes, like {{ aModel.a }}, doesn't providing explicit getters and setters like get_a mean that the implementation is even more exposed?Truc
Hello! It's not bad AngularJS, it's just bad programming. I don't see how it is "more exposed" by abstracting it; you can change it at any time without ever changing your view or controllers. But I need to stress this: service != model. The two-way binding occurs between the view and $scope objects - $scope objects are your models, and we use services to get, set, and manipulate them. Be careful not to confuse the two.Farandole
@Josh, I think you will find these comments (and the embedded YouTube link) interesting... I still don't know where my models should be.Tephra
@MarkRajcok Thanks! I hadn't seen that SO post, but I had seen the video. I think the confusion is more in terminology than anything else. I think there is a distinction between the "model layer" - which must be provided through services - and the "bound model" from the controller. I think we often use "model" to mean both, which is very confusing! My point here was that we should be using scope to glue things together in our controllers, but we shouldn't expect our services to have those automatic data bindings. But it's not black and white. Sorry - I should have been more clear.Farandole
You don't have to expose the model implementation, you can just use the $rootScope.$watch function to look for local changes. Instead of $rootScope.myModel = myModel; $rootScope.$watch('myModel.a', calcC); you could do $rootScope.$watch(function(){return myModel.a}, calcC); and everything will be nice and local.Blaubok
I agree with this answer. You could also create setter functions on the model. Inject the service in any controller, change values via setter functions. Also in your controllers you could do something like: $scope.aModel = aModel; In your template, you can bind to the values of the model like this: {{ aModel.get_a() }} Whenever some other controller updates the model via the setters, these view bindings will also update.Tricuspid
L
1

From what I can see of your structure, having a and b as getters may not be a good idea but c should be a function...

So I could suggest

myModule.factory( 'aModel', function () {
  return {
    a: 10,
    b: 10,
    c: function () { return this.a + this.b; }
  };
});

With this approach you cannot ofcourse 2 way bind c to an input variable.. But two way binding c does not make any sense either because if you set the value of c, how would you split up the value between a and b?

Lordsandladies answered 29/1, 2013 at 9:48 Comment(3)
Hi @ganaraj I agree that in this simple example, making c a function would make sense. What I didn't make very obvious is that, the kinds of things I will be doing in c() in reality are relatively computationally expensive, so I would like to avoid them being recalculated as much as possible. I think I am correct in saying that making c a function would mean that it would be recalculated at least once when anything changed in the application, even if it doesn't affect c?Truc
Are you intending to put the service aModel on the scope?Lordsandladies
Are you intending to put the service aModel on the scope? Even if you put it on the scope it wont get evaluated everytime during dirty checking... If you execute that on $scope.$watch it will.. so It is still in your hand..Lordsandladies

© 2022 - 2024 — McMap. All rights reserved.