1

I am trying to understand how to accomplish conditional formatting with AngularJS.

My scenerio, I have a table with a bunch of values like this:

<tr ng-repeat='r in row'>
  <td>{{r.valueA | number:0}}</td>
  <td>{{r.valueB | number:0}}</td>
  <td>{{r.valueA - r.valueB | number:0}}</td>
  <td>{{total.valueA - r.valueA | number:0}}</td>
  <td>{{total.valueA - (r.valueA - r.valueB | number:0)}}</td>
</tr>

What I want to happen is for these cells to change the text red when the number is negative.

Below is what I have tried:

A) Using the ng-class directive route:

<tr ng-repeat='r in row'>
  <td ng-class="{'text-red':r.valueA < 0}">{{r.valueA | number:0}}</td>
  <td ng-class="{'text-red':r.valueB < 0}">{{r.valueB | number:0}}</td>
  <td ng-class="{'text-red':(r.valueA - r.valueB) < 0}">{{r.valueA - r.valueB | number:0}}</td>
  <td ng-class="{'text-red':(total.valueA - r.valueA) < 0}">{{total.valueA - r.valueA | number:0}}</td>
  <td ng-class="{'text-red':(total.valueA - (r.valueA - r.valueB)) < 0}">{{total.valueA - (r.valueA - r.valueB) | number:0}}</td>
</tr>

...and it works, but there is a lot of needless typing. Surely there is a better way.

B) A custom filter but cannot get it to work:

myApp.filter('numberVariance',
    ['$filter',
    function (filter) {
        var numberFilter = filter('number');
        return function (amount, fractionDigits) {
            if (value === "0") {
                return "-";
            }
            var value = numberFilter(amount, fractionDigits);
            if (amount < 0)
                return "<span class='text-red'>" + value + "</span>";
            return value;
        };
    }]);

...this escapes the returned HTML string. I would rather find a solution that does not use ng-html-bind or "unsafe" strings.

C) A custom directive. This feels like it is the best fit, but I cannot get this to work either:

myApp.directive('varianceValue', function () {
    return {
        restrict: 'A',
        link: function (scope, el, attr) {
            $(el).toggleClass("text-red", ($(el).text().indexOf('-') > -1));
        },
    }
});

...this works fine on first load, but does not toggle the class as the value updates.

3
  • 1
    Just a comment on C) custom directive. You aren't watching for anything to change so it won't toggle the class. Either use scope.$on or attr.$observe to watch for a change and run your jQuery toggle again. Commented Aug 26, 2014 at 19:04
  • @fiskers7 I got it working with scope.$watch. Should I have used one of the others you mention instead? Commented Aug 26, 2014 at 21:07
  • Either one works, it really comes down to how your directive is setup and used. Commented Aug 27, 2014 at 12:47

4 Answers 4

1

Using $timeout gives Angular time to evaluate the expression and your directive should work just fine.

Working example

app.directive('varianceValue', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, el, attr) {
          $timeout(function(){
            $(el).toggleClass("text-red", ($(el).text().indexOf('-') > -1));
          },0);
        },
    }
});

HTML

<table>
  <tbody>
    <tr ng-repeat="r in row">
      <td variance-value>{{r.valueA | number:0}}</td>
      <td variance-value>{{r.valueB | number:0}}</td>
      <td variance-value>{{r.valueA - r.valueB | number:0}}</td>
      <td variance-value>{{total.valueA - r.valueA | number:0}}</td>
      <td variance-value>{{total.valueA - (r.valueA - r.valueB | number:0)}}</td>
    </tr>
  </tbody>
</table>
Sign up to request clarification or add additional context in comments.

2 Comments

This clearly works in your example, but it does not work for me. I'm not sure why.
I got this to work by using scope.$watch('row' instead of the $timeout. Here is a fork of your plunker: plnkr.co/edit/ZwtxVEY1qRiqOLf0u9QR?p=preview
1

You could use the angular ng-class directive, to add the appropriarate class when the computed value is negative.

It is the cleanest approach (using the angular philosophy) because:

  • it uses a CSS class that is binded to a computed variable

  • every part of this solution has a distinct role ( css for the visuals, a true/false value to control the class )

  • it is flexible, easy to be modified and can be easily scaled up ( add more classes, different logic in getting the colors according to the value etc)

  • The computations are performed in the controller

  • As for the extra typing: It can be further reduced by adding an additional (nested) ng-repeat to generate the <td>'s for each row.

A working example:

HTML:

<style>
    .red {
        color: red
    }
</style>

<span ng-app="myApp" ng-controller="mainCtrl">
    <table>
        <tr ng-repeat='r in row'>
            <td ng-class="{ red: isRed(getColumnVal(r, 1)) }">
                {{getColumnVal(r, 1)}}
            </td>
            <td ng-class="{ red: isRed(getColumnVal(r, 2)) }">
                {{getColumnVal(r, 2)}}
            </td>
            <td ng-class="{ red: isRed(getColumnVal(r, 3)) }">
                {{getColumnVal(r, 3)}}
            </td>
        </tr>
    </table>
</span>

JS:

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

myApp.controller('mainCtrl', ['$scope', function ($scope) {

    // the array of objects*
    $scope.row = [
        {
            valueA: 1,
            valueB: 2
        },
        {
            valueA: 3,
            valueB: 4
        }
    ];

    // returns the value for the given object* and the given table column
    $scope.getColumnVal = function (o, col) {
        var columnVal = 0;

        switch (col) {
            case 1:
                columnVal = o.valueA
                break;
            case 2:
                columnVal = o.valueB;
                break;
            case 3:
                columnVal = o.valueA - o.valueB;
                break;
            default: 
                columnVal = 0;
        }

        return columnVal;
    };

    // returns true if the given val is smaller than zero, else returns false
    $scope.isRed = function (val) {
        var isRed = false;
        if (val < 0) {
            isRed = true;
        }
        return isRed;
    };

}]);

To further reduce the typping:

avoid writting each <td> by adding an additional ng-repeat

HTML:

<style>
    .red {
        color: red
    }
</style>
<span ng-app="myApp" ng-controller="mainCtrl">
    <table>
        <tr ng-repeat='r in row'>
            <td ng-repeat='c in columns' ng-class="{ red: isRed(getColumnVal(r, c)) }">
                {{getColumnVal(r, c)}}
            </td>
        </tr>
    </table>
</span>

and add to the controller the following statement that defines the columns, according to your needs:

$scope.columns = [1, 2, 3]; // This is for 3 columns, extend to your needs

1 Comment

Thanks for this answer! I will have to think on it, and there is a lot here I can take away. I am new to angular and still learning the philosophy; though this does seem to add controller-view coupling just for the view to do a basic conditional formatting. I appreciate your suggestion on repeating the table cells as well.
0

I would use ng-class, but pull the logic out into descriptively named functions in the controller/service depending on how your app is set up.

Comments

0

Below is the solution I ended up going with. Thank you all for your contributions.

myApp.directive('varianceValue', [ function () {
    return {
        restrict: 'A',
        link: function (scope, el, attr) {
            scope.$watch(
                function () { return el.text() },
                function () { $(el).toggleClass("text-red", ($(el).text().indexOf('-') > -1)); }
            );
        },
    }
}]);

I messed around with various $watch values and most of them brought the application to a crawl, but the version above is holding strong.

Comments

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.