Angular.js: input[number] automatic recognition of localized decimal character not consistent across browsers

Created on 20 Nov 2014  Â·  18Comments  Â·  Source: angular/angular.js

You can try it from the official API docs: https://docs.angularjs.org/api/ng/input/input[number]

I don't know if this is a bug or a lack of parameters adjustments, but I post it anyway.

When you're in a non-english localized browser (in which the decimal mark is the comma, like in French, German, Finnish, etc), if you enter a valid number (say 12,5 or 12.5), the Angular input[number] directive recognize it like a valid number.
But as soon as you use an english-only browser, there is no way Angular can treat the same input as valid. If you enter 12,5 in the same input field, it considers it as invalid.
I think this is not consistent, and there should be a way to tell the directive what characters are considered as the decimal separator. Or maybe there is a way to set this but it is not documented.

forms breaking change not core confusing

Most helpful comment

@zzal The directive does not catch the 1000-separator. A german 1.000,12 should be parsed to 1000.12 but instead is parsed to 1.00012

All 18 comments

This looks like an i18n limitation... all boils down to
1/ The fact that browsers implement parseFloat using javascript float numbers notation
2/ A regex that follows the same pattern that parseFloat waits

Now, to solve this properly we would need to use the locale information and use it to _transform_ the number before it is parsed...

Anyhow, as a temporary workaround you can do the following

  • create a directive that hooks to <input>, in this directive ask for the controller ngModel (make this controller optional)
  • if the type is not number or if there is no controller, then do nothing
  • add a parser to the beginning of the queue that removes the . and transforms the , into .
    * warning, untested code*
    ctrl.$parsers.unshift(function(value) { return value.replace(/\./g,'').replace(',', '.');});

this should make it until there is proper support for input[number] that use the locale information

Thanks Lucas for your suggestion, but it looks like it’s already too late, even if the $parser is put at the begining, or maybe I’m missing something… When I enter some number with a comma in an english localized browser, the model is already empty.

Look at my Plunkr: http://plnkr.co/edit/Rtuq07BU4z6lW2KINBSN?p=preview

Hi @zzal . I think the problem exists because of the special handling of type=number by the browser (Plunkr tested with Firefox 33.1). Take a look at my Plunkr:

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

As you can see the value 1,5 (with a german decimal separator) leads to an empty string. It would be really nice if we do not have to use type=number but something like <input type="text" ng-number>.

Hi everyone,

Yes, I think it's best to rewrite the input[type=number] directive.

Here's my take, and it's fully working.

  .directive('asNumber', [
    '$locale',
    function ($locale, undefined)
    {
      return {
        restrict: 'A',
        require: '?ngModel',
        compile: function(tElement, tAttrs)
        {
          if (tElement[0].nodeName !== 'INPUT')
          {
            throw('Error. asNumber directive must be used inside an <input> element.');
          }
          tElement.attr('pattern','[0-9]*');

          return function (scope, element, attrs, ngModelCtrl, undefined)
          {
            if (!ngModelCtrl)
            {
              return;
            }

            var step, newValue;
            var maxAttr = (attrs.hasOwnProperty('max') && attrs.max !== '') ? parseInt(attrs.max,10) : false,
                minAttr = (attrs.hasOwnProperty('min') && attrs.min !== '') ? parseInt(attrs.min,10) : false,
                stepAttr = (attrs.hasOwnProperty('step') && attrs.step !== '') ? parseInt(attrs.step,10) : 1;

            element.on('keydown',function(event)
            {
              // Arrow key incrementation:
              if (event.keyCode === 38 || event.keyCode === 40)
              {
                event.preventDefault();
                step = (event.shiftKey) ? (stepAttr * 10) : stepAttr;
                if (event.keyCode === 40) // Arrow down
                {
                  step *= -1;
                }

                newValue = (isNaN(ngModelCtrl.$modelValue)) ? step : ngModelCtrl.$modelValue + step;

                if (maxAttr !== false && newValue > maxAttr)
                {
                  newValue = maxAttr;
                }
                else if (minAttr !== false && newValue < minAttr)
                {
                  newValue = minAttr;
                }
                newValue = String(newValue);
                if ($locale.NUMBER_FORMATS.DECIMAL_SEP === ',')
                {
                  newValue = newValue.replace(/\.(\d*)$/, ',$1');
                }
                else
                {
                  newValue = newValue.replace(/,(\d*)$/, '.$1');
                }
                ngModelCtrl.$setViewValue(newValue);
                ngModelCtrl.$render();
                element.select();
              }
            }); // end on keydown

            ngModelCtrl.$parsers.unshift(function(value)
            {
              if (typeof value !== 'string' || value === '')
              {
                return null;
              }
              value = value.replace(/,(\d*)$/, '.$1');
              var out = parseFloat(value,10);
              if (isNaN(out))
              {
                return undefined;
              }
              return out;
            }); // end $parser

            ngModelCtrl.$formatters.unshift(function(value)
            {
              if (typeof value !== 'string')
              {
                return value;
              }
              if (isNaN(parseFloat(value,10)))
              {
                return '';
              }
              if ($locale.NUMBER_FORMATS.DECIMAL_SEP === ',')
              {
                return value.replace(/\.(\d*)$/, ',$1');
              }
              return value.replace(/,(\d*)$/, '.$1');
            }); // end $formatter

            ngModelCtrl.$validators.number = function(modelValue, viewValue)
            {
              if (modelValue === undefined || modelValue === null || modelValue === '')
              {
                return true;
              }
              if (isNaN(modelValue))
              {
                return false;
              }
              return true;
            }; // end $validator number

            ngModelCtrl.$validators.range = function(modelValue, viewValue)
            {
              if ((maxAttr && modelValue > maxAttr) || (minAttr && modelValue < minAttr))
              {
                return false;
              }
              return true;
            }; // end $validator range

          };  // end link function
        } // end compile function
      };
    }
  ])

How to use: <input type="text" as-number />
Maybe this can be improved?

Edit: Just added the set pattern="[0-9]*" to add mobile browsers compatibility (iOS at least).

@zzal input[type=text] is problematic for mobile users, compared with [type=number], which has a better UI for entering numbers. I don't think we want to lose that, so it's kind of a non-starter

Good point.
In my use case though this is not a problem since the app is used in a controlled environment.

What is actually needed for the UI to help entering numbers? The keyboard layout?

@zzal Yes, the keyboard layout change in case of [type=number] is quite important to provide good UX for the user.

This issue is quite important to be fixed, as now with [type=number] we can't enable users to enter either comma or period as decimal separator, as in case of comma the model value is undefined. Angularjs should enable developers to setup how comma separators work or just provide a way to customize all the details.

What I would like to achieve is to enable users to enter comma and period in the input, and handle them based on the currently selected user locale, so for example:

  1. Current user locale decimal separator: period, then:
    Thousand separator is comma, remove all of them from input, use period as decimal separator (should be done by js by default).
  2. Current user locale decimal separator: comma, then:
    Thousand separator is period: remove all of them from input: convert comma to period to be used as decimal separator.

Basically the same thing as mentioned before.
I'm not sure if browser behavior is already limiting the customization in case of type="number" or not. Needs to be checked.

@zzal The directive does not catch the 1000-separator. A german 1.000,12 should be parsed to 1000.12 but instead is parsed to 1.00012

+1

@konsultaner That happens for browser that don't support input[number], right?

@Narretz I usually work with chrome, so no this happens in general I guess.

After following the suggestion from lgalfaso , I have successfully made the input[number] in IE11 can accept either Comma or Period as Decimal-Separator.

Roughly the directive will look like:

restrict:'A',
require:'?ngModel', // get a hold of NgModelController
link:function ($scope, $element, $attrs, $ngModel) {
    if ($ngModel) {
        $ngModel.$parsers.unshift(function (str) {
            if (str && (typeof str === 'string' || str instanceof String)) {
                // sanitize : replace Comma with Period/Dot
                return str.replace(',', '.');
            } else {
                // not a string, can't process it, give up, will be handled by next parser within pipeline
                return str;
            }
        });
    }
}

I think it's better to just force the user to use Period though, e.g. by rejecting Comma within keypress or keydown.
Browser's behavior for Comma is varies, even worse the behavior of the same browser can be different if the language settings are different.

@Thariq-Nugrohotomo I did some testing of your solution, it works in IE11 as you say, just want to give a heads up that this does not work as intended in Chrome, Win and Edge, Win.

  • Chrome rejects the comma input and therefore it does not reach the angular parser.

  • Edge accepts the input but does not send a value to AngularJS if the decimal number contains a comma.

The only solution that I can think of with these facts to work in all desktop browsers is to use input[type=text] instead and build manual parsing and validation of the number.

If mobile is needed I think the best full blown solution would be to fall back to using a input[type=number] if it is a mobile device to get a proper number keyboard when the user enters a number.

@robinwassen I haven't do any testing for Edge,
but as for Chrome, I just tell the client to switch their Chrome language settings to the one that supports comma as decimal-separator (e.g. french, german). They also need to make Chrome being shown in that selected language. This way, Chrome will accept the comma, even without adding custom parser.

Custom parser that I mentioned above is just for IE (tested in IE11).

This is a general problem with browsers that support input[number] since the value that AngularJS receives has already been converted by the browser to a number using the browser's locale settings.

I'm confused, can someone help me understand what should I do when:

  1. My users use a mix of browsers/language settings
  2. They just want the decimal separator to ALWAYS be "," (comma)

Why including the proper angular-locale_* is not enough?

@MatteoSp - Use

Old post, but maybe a solution.
The locale of my web app is fr_FR. I use the attribute lang="en_EN" on input type=number. Firefox display the float with a dot and Chrome display the float with a comma but I can use indifferently the comma and the dot, no pb of validation

Was this page helpful?
0 / 5 - 0 ratings