14

I saw this solution http://jsfiddle.net/gronky/GnTDJ/ and it works. That is, when you input 25, it is pushed back to model as 0.25

HTML:

<script type="text/javascript" ng:autobind
        src="http://code.angularjs.org/0.9.17/angular-0.9.17.js"></script>
<script>
function Main() {
    this.var = '1.0000';
}
</script>
<div ng:controller="Main">
    <input type="text" name="var" ng:format="percent">
    <pre>var = {{var|json}}</pre>
</div>​

JavaScript:

angular.formatter('percent', {
  parse: function(value) {
    var m = value.match(/^(\d+)\/(\d+)/);
    if (m != null)
      return angular.filter.number(parseInt(m[1])/parseInt(m[2]), 2);
    return angular.filter.number(parseFloat(value)/100, 2);
  },
  format: function(value) {
    return angular.filter.number(parseFloat(value)*100, 0);
  },
});
​

I tried making it work on latest AngularJS, it doesn't work anymore though http://jsfiddle.net/TrJcB/ That is, when you input 25, it is pushed back as 25 also, it doesn't push the correct 0.25 value to model.

Or perhaps there's already a built-in formatter for percent? I wanted currency formatter too, or comma separated number.

Hao
  • 8,047
  • 18
  • 63
  • 92

4 Answers4

36

Another way to implement percentage filter (works with angular#~1.2):

angular.module('moduleName')
.filter('percentage', ['$filter', function($filter) {
    return function(input, decimals) {
        return $filter('number')(input*100, decimals)+'%';
    };
}]);

How to use it:

<span>{{someNumber | percentage:2}}</span>
Igor Rafael
  • 489
  • 1
  • 4
  • 7
24

The fiddle doesn't work with current Angular version since quite a few APIs have changed since. angular.formatter is no longer available and neither is angular.filter.

The way to write it now is to use a directive and make use of $parser and $formatter available on the directive controller. So your link function will look something like

link: function(scope, ele, attr, ctrl){
        ctrl.$parsers.unshift(
            function(viewValue){
                return $filter('number')(parseFloat(viewValue)/100, 2);
            }
        );
        ctrl.$formatters.unshift(
            function(modelValue){
                return $filter('number')(parseFloat(modelValue)*100, 2);
            }
        );
      }

Also the filters are now accessed through $filter service. You can find the documentation here: https://docs.angularjs.org/api/ng/filter/number

Updated fiddle for the original example: http://jsfiddle.net/abhaga/DdeCZ/18/

Currency filter is already available in angular: https://docs.angularjs.org/api/ng/filter/currency

frfahim
  • 515
  • 9
  • 21
abhaga
  • 5,455
  • 2
  • 21
  • 20
  • Thanks! Regarding second inquiry, I'm looking for an input for currency instead, i.e. when keying-in input, the control automatically put comma(s). Perhaps I'll just start from your fiddle on percent for accomplishing the currency input. Thanks very much! – Hao Dec 03 '12 at 00:13
  • 1
    Maybe it's worth noting that if one is in another locale, the other formatters/parsers can make the model `undefined` (ie: `$filter('number')` returns `"4,00"` when another formatter asks for a `"."` decimal separator). – sgy Nov 11 '14 at 19:46
  • Is this supposed to return a string, and not a decimal number? How could I get it to return a decimal? – Ingó Vals Apr 08 '15 at 10:37
2

Here's a full directive that will parse, format, and perform Angular validation on the inputs. (Tested against angular 1.2 & 1.3.)

We use this so that our data model to/from server can be expressed in decimal notation (0.7634), but we provide a human-readable format to the user (76.34), and enforce a maximum precision. Note that this directive is concerned purely with the numeric aspects. I find it easier to insert a '%' into the template separately, rather than including it here.

It defaults to enforcing input values from -100 to 100, but you can supply your own bounds with attrs pct-min and pct-max.

'use strict';

angular.module('XLDirectives')
  .directive('xlPercentage', function($filter) {
    // A directive for both formatting and properly validating a percentage value. 
    // Assumes that our internal model is expressed as floats -1 to +1: .099 is 9.9%
    // Formats display into percents 1-100, and parses user inputs down to the model. 
    // Parses user input as floats between 0 and 100 into floats less than 1. 
    // Validates user input to be within the range -100 to +100. 
    // Sets Angular $valid property accordingly on the ngModelController.
    // If a `pct-max` or `pct-min` attribute is specified on the <input>, will use those bounds instead.
    // If a `pct-decimals` attr present, will truncate inputs accordingly. 

    function outputFormatter(modelValue, decimals) {
      var length = decimals || 2;
      if (modelValue != null) {
        return $filter('number')(parseFloat(modelValue) * 100, length);
      } else {
        return undefined;
      }
    };

    function inputParser(viewValue, decimals) {
      var length = decimals || 4;
      if (viewValue != null) {
        return $filter('number')(parseFloat(viewValue) / 100, length);
      } else {
        return undefined;
      }

    }

    function isWithinBounds(value, upper, lower) {
      if (value >= lower && value <= upper) {
        return true;
      } else {
        return false;
      }
    }

    return {
      restrict: 'A',
      require: 'ngModel',
      link: function postLink(scope, element, attrs, ctrl) {
        ctrl.$parsers.unshift(function(viewValue) {
          // confirm the input from the view contains numbers, before parsing
          var numericStatus = viewValue.match(/(\d+)/),
            min = parseFloat(attrs.pctMin) || -100,
            max = parseFloat(attrs.pctMax) || 100,
            decimals = parseFloat(attrs.pctDecimals) || 4,
            bounded = isWithinBounds(viewValue, max, min);
          if (numericStatus !== null && bounded) {
            ctrl.$setValidity('percentage', true);
            // round to max four digits after decimal
            return inputParser(viewValue, decimals);
          } else {
            ctrl.$setValidity('percentage', false);
            return undefined
          }
        });

        ctrl.$formatters.unshift(outputFormatter);
        // we have to watch for changes, and run the formatter again afterwards
        element.on('change', function(e) {
          var element = e.target;
          element.value = outputFormatter(ctrl.$modelValue, 2);
        });
      }
    };
  });


// REFS: 
// http://stackoverflow.com/questions/17344828/angularjs-should-i-use-a-filter-to-convert-integer-values-into-percentages
// http://stackoverflow.com/questions/13668440/how-to-make-a-percent-formatted-input-work-on-latest-angularjs
XML
  • 19,206
  • 9
  • 64
  • 65
  • 1
    Note: attrs.pct-min should be attrs.pctMin and attrs.pct-max should be attrs.pctMax (post edit submitted) – Olivier Clément Jan 08 '15 at 20:57
  • 1
    @OlivierClément: Thanks for your comment (and attempted edit). I had improved this directive in production with some of these changes, but forgot to update it here. – XML Jan 12 '15 at 08:31
0

I modified abhaga's answer to allow for .## and ## input. In my opinion this is a lot more user-friendly

link: function(scope, element, attr, ngModel) {
            ngModel.$parsers.unshift(
                function(viewValue){
                    var perc = parseFloat(viewValue);
                    if (perc<0 || perc>100 || !isFinite(perc)){
                        return null;
                    }
                    if (perc>1 && perc<=100){
                        return parseFloat($filter('number')(perc/100));
                    }
                    return perc;
                }
            );
            ngModel.$formatters.unshift(
                function(modelValue){
                    if(!isFinite(modelValue)){
                        return "";
                    }
                    return $filter('number')(parseFloat(modelValue)*100, 2);
                }
            );
        }
Nick Bolles
  • 439
  • 5
  • 14