Jason Maggard
Jason Maggard

Reputation: 1672

New to Angular - Computed Variables

I am moving to Angular from Knockout, and I have a few issues. I'm assuming that I must be doing something a non-angular type of way.

http://jsfiddle.net/LostInDaJungle/BxELP/

I linked to jsfiddle so I didn't have to include my code here
Stack Overflow will not let me post my question without a code block.

Here is a very basic fiddle that outlines two of my major problems...

Problem 1: val1 and val2 are initialized as 3 and 4, and add up to 7 properly. However, if you change either of the values in the text boxes, the new value is treated as a string and I get concatenation instead of addition. Change val1 to 4 and you get 44 when it should be 8. What is the best way around this behaviour?

Problem 2: Calculated fields. I can get a calculated field by using the curly brackets like {{val1 + val2}} and have the calculated fields auto update when the underlying model changes, but this is totally unacceptable. In my full fledged app, we generate a "cost" that is used several times throughout and having to put in the cost calculation each and every time is a pain. Not to mention that when this calculation changes, I now have the unenviable task of finding 15 different places that use the cost calculation and updating them all.

In addition, if I try to put an ng-model="cost" on the input with the curly brackets, then the curly brackets don't work. So nothing jumps out at me as a way to bind cost.

http://jsfiddle.net/LostInDaJungle/QNVwe/

This example is more like the structure I desire. However, unlike a ko.observable, the calculated fields do not update when the values that generate them change. The boilerplate solution everyone has foisted on me is to write a bunch of ng-change handlers... But that is awful. If width changes change the cost and change the payback calculations, etc... It quickly becomes a tangled mess.

Both of these methods fail as far as separating logic from presentation. Method one has my business logic embedded in my HTML. Method two puts a whole bunch of ng-change handlers in my code which isn't that much different from having to write a whole mess of onChange handlers in plain ol' HTML. If I HAVE to do a bunch of ng-change handlers, I would just as soon do an onChange handler in Javascript because I can at least declare them outside of my presentation layer.

Here's a knockout version of the same:

http://jsfiddle.net/LostInDaJungle/eka4S/2/

This is more like what I would expect... Nothing but data-binds on my inputs, all program logic nicely contained within the view model. Also, since my computable is a Javascript function, I don't have to scratch my head about how to ensure my two values are numeric.

So....

Computed variables: Is there a way to watch the underlying variables and update the computed amount automatically? Without having to bury my program logic in my HTML?

Is there a good way to keep my numbers from turning into strings?

Thank you for your help.

FYI, also posted to Google Groups: https://groups.google.com/forum/#!topic/angular/0dfnDTaj8tw

Upvotes: 23

Views: 35874

Answers (7)

Dev_Corps
Dev_Corps

Reputation: 361

The $watch function that is made available through the $scope variable is best for this job in my opinion.

$scope.$watch(function(scope) { return scope.data.myVar },
              function(newValue, oldValue) {
                  document.getElementById("myElement").innerHTML =
                      "" + newValue + "";
              }
             );

The $watch function takes in a: value function & a listener function

The above example is taken from this awesome article: http://tutorials.jenkov.com/angularjs/watch-digest-apply.html

After reading through it, I learnt a lot and was able to implement the solution I was looking for.

Upvotes: 0

Jens
Jens

Reputation: 5877

About problem 1:

You should use input type="number" if possible. That would take care of parsing numbers properly. Even if you have an older browser angular would take care of formatting them as numbers.

About problem 2:

Your answer is good Jason if you just need to show plain text on the screen. However if you would like to bind an input with a model to an arbitrary expression, you need something else.

I wrote a directive you can use to bind an ng-model to any expression you want. Whenever the expression changes the model is set to the new value.

module.directive('boundModel', function() {
  return {
    require: 'ngModel',
    link: function(scope, elem, attrs, ngModel) {
      scope.$watch(attrs.boundModel, function(newValue, oldValue) {
        if(newValue != oldValue) {
          ngModel.$setViewValue(newValue);
          ngModel.$render();
        }
      });
    }
  };
})

You can use it in your templates like this:

<input type="text" ng-model="total" bound-model="value1 + value2">

Or like this:

<input type="text" ng-model="total" bound-model="cost()">

Where cost() is a simple function of the scope like this:

$scope.cost = function() { return $scope.val1 + $scope.val2 };

The good thing is that you keep using a model for your input and you don't have to dinamically update your value attribute, which doesn't work well in angular.

Upvotes: 2

Ahmad El-Banna
Ahmad El-Banna

Reputation: 1

u can bind to a function

function CTRL ($scope) {
$scope.val1 = 3;
$scope.val2 = 4;
$scope.sum = function(){
   return ($scope.val1 *1 + $scope.val2 *1);
};

}

it will work the same the binding expression will work but in much more complex cases we need functions

Upvotes: 0

Claude Martin
Claude Martin

Reputation: 765

I'm new to AngularJS but I think that $parse could be used:

http://docs.angularjs.org/api/ng/service/$parse

This is interesting if you have the expression as a string. You can use a path of properties and that string can be generated dynamically. This works if you don't know the expression at compile time, a lot like eval() but probably a lot faster and maybe more secure(?).

Here's an example:

function Ctrl($scope,$parse) {
  var expression = 'model.val1 + model.val2';//could be dynamically created
  $scope.model = {
    val1: 0,
    val2: 0,
    total: function() { 
        return ($parse(expression))($scope); 
    }
  };
}

Upvotes: 0

Jason Maggard
Jason Maggard

Reputation: 1672

Ok,

A few hours later and I think I have my answer.

Using $scope.$watch.

$scope.$watch('(height * width) * 40', function(v) {$scope.cost = v;});

or

$scope.$watch('height + width', function() {$scope.cost = (Number(height) * Number(width)) * 40;});

This auto-updates any computables for watched variables. And it gives me a way to work with these without having to live inside curly brackets.

Also, the computed values can be reused and tracked for cascading updates:

$scope.$watch('height * width', function(v) {$scope.dim = v;});
$scope.$watch('dim * 40', function(v) {$scope.cost = v;});

So if height and/or width change, dim is updated, and since dim has changed, cost is updated.

Upvotes: 13

Mac
Mac

Reputation: 1566

I changed your third input to:

<input type="text" value="{{val1 * 1 + val2}}" />

which causes Angular.js to treat the values as numbers, not strings.

Here is the fiddle. I gleaned the answer from here.

Upvotes: 6

blaster
blaster

Reputation: 8937

For a calculated field, add a method to your controller . . .

$scope.cost = function() { return $scope.val1 + $scope.val2 };

and then bind to it directly. It will know when it needs to recalculate as its constituent values change.

<div>{{cost()}}</div>

Upvotes: 31

Related Questions