11

Angular noobie here. I would like to know what is the best way to change the dom when a value in the scope changes by some means. I read that its not good to put the dom manipulation logic in the controller and thats the job of directives.

Here is a plunkr

http://plnkr.co/edit/xWk5UBwifbCF2raVvw81?p=preview

Basically when the data changes by clicking the load data button in the plunkr above, i want the cells whose values changed to highlight automatically. I cant get it to work for the life of me.

Any help?

Jenos
  • 485
  • 1
  • 4
  • 15

3 Answers3

27

I think it would be better to observer a concrete value per highlighter instead of watching the whole collection. E.g.:

<td highlighter="person.firstName">{{ person.firstName }}</td>

This way, the highlighter-directive could be very simple, like:

app.directive('highlighter', ['$timeout', function($timeout) {
  return {
    restrict: 'A',
    scope: {
      model: '=highlighter'
    },
    link: function(scope, element) {
      scope.$watch('model', function (nv, ov) {
        if (nv !== ov) {
          // apply class
          element.addClass('highlight');

          // auto remove after some delay
          $timeout(function () {
            element.removeClass('highlight');
          }, 1000);
        }
      });
    }
  };
}]);

Though, for this to work you'll have to tell angular that the data actually changed. Currently this is not the case as angular tracks people by object-identity. The moment you overwrite it, angular will remove all associated dom-elements. To accomodate for this, use:

ng-repeat="person in people track by $index"

which will tell angular to treat the index of the array as identity.

demo: http://jsbin.com/vutevifadi/1/

Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • 1
    I was aware of `track by $index` as a fix for duplicate values in an object, but it's great to know it fixes the `newVal, oldVal` problem here, also. I was so stumped as to why they were always equal in this case. – m59 Nov 21 '13 at 09:23
  • 1
    Beware! Scope is not isolated. This will trigger a $digest on $rootScope! – Jonathan Feb 22 '15 at 15:02
6

Thank for posting the answer above. I've noticed that the animation will flicker if value changes frequently and timeout will be fired while another animation is already active.

I've fixed this by resetting the timeout if the timeout is already set.

Also, I've added code to check if value is increasing or decreasing and set different css class.

app.directive('newvalue', ['$timeout', function($timeout) {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
        element.addClass('newvalue');
        scope.$watch(attrs.newvalue, function (nv, ov) {
        function settimeout() {
            attrs.timeout = $timeout(function () {
                element.removeClass('newvalue-up');
                element.removeClass('newvalue-down');
                attrs.timeout = null;
            }, 1000);
        }
        if (nv !== ov) {
            if(attrs.timeout) {
                //newvalue already set.. reset timeout
                $timeout.cancel(attrs.timeout);
                settimeout();
            } else {
                if(nv > ov) {
                    element.addClass('newvalue-up');
                } else {
                    element.addClass('newvalue-down');
                }
                settimeout();
            }
        }
      });
    }
  };
}]);
Soichi Hayashi
  • 3,384
  • 2
  • 22
  • 15
3

After some reading I noticed there are some doubts about using $watch, considering performance. I found another solution using $observe.

A good read on $watch and $observe: https://stackoverflow.com/a/14907826/2901207

javascript:

var app = angular.module('angularjs-starter', []);
app.directive('highlightOnChange', function() {
  return {
    link : function(scope, element, attrs) {
      attrs.$observe( 'highlightOnChange', function ( val ) {
        console.log("Highlighting", val);
        element.effect('highlight');
      });
    }
  };
});

app.controller('myController', function($scope, $timeout) {
  $scope.val = 1;
  $scope.updateVal = function() {
    $scope.val = $scope.val + 1;
  };
}); 

html:

  <body ng-controller="myController">
    <div highlight-on-change="{{val}}">
      Total: {{ val }}
    </div>
    <button ng-click="updateVal()">Add to Total</button>
  </body>

original source: http://plnkr.co/edit/FFBhPIRuT0NA2DZhtoAD?p=preview from this post: https://groups.google.com/d/msg/angular/xZptsb-NYc4/YH35m39Eo2wJ

A bit more complex usage, which works perfect for me because it highlights the specific column when an update is coming.

<table class="table table-hover">
        <tr>
            <th ng-repeat="col in fc.tableColumns"><!--fc is a controller-->
                {{col.displayName}}
            </th>
        </tr>
        <tr ng-repeat="item in fc.items track by item.id">
            <td highlight-on-change="{{value}}" ng-repeat="(key,value) in item">
               @*{{key}} =*@ {{value}}
            </td>
        </tr>
    </table>

As I said, updates the specific column, while doing this somewhere in the controller.

items[index] = item;//item from server
Community
  • 1
  • 1
CularBytes
  • 9,924
  • 8
  • 76
  • 101