0

I want to update model value from custom directive with attribute value. Let's imagine that I have 4 numbers. I want to do the following:

  • Sum first two numbers (1+2)
  • Sum second two numbers (3+4)
  • Sum both sums (sum1+sum2) - cascade sum

Formula is expressed in attribute value of custom directive. I'm not so experienced in Angular and have some partial working solutions, but I think I'm going in the wrong direction, so I'll post code without custom directive.

Here is the code for which i'm trying to build custom directive, what would be the best approach for writing that directive (formula)?

EDIT: input fields with formula directive will be read-only, and they only have one purpose - to recalculate values from other fields depending on the formula.

<!DOCTYPE html>
<html ng-app>

<head>
  <script data-require="angular.js@*" data-semver="1.4.0-beta.5" src="https://code.angularjs.org/1.4.0-beta.5/angular.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body>
  <h1>Cascade sum example</h1>
  <input ng-model="A1" type="number">
  <input ng-model="A2" type="number">
  <input ng-model="A3" type="number">
  <input ng-model="A4" type="number">
  <input ng-model="A5" type="number" formula="A1+A2" readonly>
  <input ng-model="A6" type="number" formula="A3+A4" readonly>
  <input ng-model="A7" type="number" formula="A5+A6" readonly>
</body>

</html>

2
  • What happens if you change the values in the "inputs" after A4? It doesn't seem to make sense that those are inputs, as well. Commented Mar 10, 2015 at 17:11
  • I updated my question, inputs with formula directive will be readonly, so I only need to recalculate those inputs. Form will be dynamically generated from JSON, and formula inputs only display to user recalculated values from other fields. Commented Mar 10, 2015 at 17:32

2 Answers 2

2

(A working plunker of the solution - http://plnkr.co/edit/nhlI4fSsK58mWS18RgUh?p=preview)

Here's a simple template to illustrate the usage:

<h1>Cascade sum example</h1>
<ul>
  <li ng-repeat="(key, input) in inputs">
    <input type="number" ng-model="input.value"
      formula="::input.formulaFn"/>
    <span>{{::key}}</span>
    <span ng-if="::input.formula">({{input.formula}})</span>
  </li>
</ul>

Firstly, we take the inputs from a database, and create a formula function for each one. The formula function, when called, produces a value for the given formula:

Inputs.getAll()
  .then(function(inputs) {
    $scope.inputs = inputs

    _.each($scope.inputs, function(input) {
      if (input.formula) {
        input.formulaFn = parseFormula(input.formula)
      }
    })
  })

  function parseFormula(expr) {
      var parsed = $parse(expr)
      return function apply() {
          return parsed($scope.values)
      }
  }

The meat is parseFormula. It uses angular's $parse to convert an expression(e.g 'A1+A2') into a function(plus(a,b)). if you called parsed with an object containing the properties A1 and A2, it would produce the sum of their values - this is what apply() is doing.

Our current object, $scope.inputs, can't be used to supply our parsed expressions with the needed values(it needs to look like 'A1': 1, instead of 'A1': { ... }). Unfortunately, we can't use the same object to hold both our values and our ng-models, due to angular's ng-repeat peculiarities with binding and primitives(you can read more about it here - https://github.com/angular/angular.js/wiki/Understanding-Scopes#ng-repeat - this is also informally known as the "dot rule"). This is why we need to have both $scope.inputs(to provide our bindable models), and a synchronized $scope.values, which we'll use for our expressions.

$scope.values = {}
$scope.$watch('inputs', function(value) {
  $scope.values = _.mapValues($scope.inputs, function(input) {
    return input.value
  })
}, true)

The directive is fairly simple. If there's a formula on the element(this is actually the formula functions that we created earlier), it will watch it(this means that the function is called every digest). Once the formula produces a new value(e.g. for 'A1+A2', A1 or A2 were changed), then we simply synchronize the ngModel with it.

.directive('formula', function() {
  return {
    scope: {
      formula: '=',
    },
    require: 'ngModel',
    link: function(scope, element, attrs, ngModelCtrl) {
      if (!scope.formula) return
      element.attr('readonly', true)
      scope.$watch(scope.formula, function(value) {
        ngModelCtrl.$setViewValue(value)
        ngModelCtrl.$render()
      })
    }
  }
})
Sign up to request clarification or add additional context in comments.

1 Comment

I had a very similar solution, but I tried to use same object for ng-model and hold values, you really nailed it :) I can't thank you enough for this detailed answer. I mark this answer as accepted.
2

I don't think you would need to write your own directive to solve a problem like this. Angular allows you to evaluate expressions inside of the html page. For example you can evaluate your formula inline using your ng-model values like so, <input ng-model="A5" type="number" value={{A1+A2}}> angular will take this and replace it with the value of A1+A2.

EDIT: see https://jsfiddle.net/v4b37wzm/3/

7 Comments

I need both model and directive, because I can't do multi-level sums. For example, Instead of ng-model="A5", I could write ng-model="A1+A2", but then I can't reference that model anymore later and can't write model for A7 like A5+A6 because A5 doesn't exist anymore. This is just an example, I will have form with hundreds of cells, so I need behaviour like in my example.
As @Sacho said above it seems weird that you have a user input that would be dependent on two values being added together. Wouldn't allowing users to change the value defeat the purpose of this formula.
Form will be dynamically generated from JSON, and fields with formulas will be read-only, user can't change values in formula fields. I just need to recalculate values in formula fields when value change in some of "normal" fields. I will edit my question and say formula are read-only values so there is better description.
I added a JSFiddle to my answer with what I think you are looking for
This works, but I need to dynamically generate those values, in ng-repeat. If I remove scope.values initialization the code doesn't work.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.