Marko Tošić
Marko Tošić

Reputation: 112

AngularJS update model value from custom directive

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:

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>

Upvotes: 0

Views: 890

Answers (2)

Sacho
Sacho

Reputation: 2179

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

Upvotes: 2

Jake C.
Jake C.

Reputation: 287

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/

Upvotes: 2

Related Questions