Using AngularJS directive to format input field while leaving scope variable unchanged
Here's a fiddle that shows how I implemented the exact same behavior in my application. I ended up using ngModelController#render
instead of $formatters
, and then adding a separate set of behavior that triggered on keydown
and change
events.
I've revised a little what Wade Tandy had done, and added support for several features:
- thousands separator is taken from $locale
- number of digits after decimal points is taken by default from $locale, and can be overridden by fraction attribute
- parser is activated only on change, and not on keydown, cut and paste, to avoid sending the cursor to the end of the input on every change
- Home and End keys are also allowed (to select the entire text using keyboard)
set validity to false when input is not numeric, this is done in the parser:
// This runs when we update the text field ngModelCtrl.$parsers.push(function(viewValue) { var newVal = viewValue.replace(replaceRegex, ''); var newValAsNumber = newVal * 1; // check if new value is numeric, and set control validity if (isNaN(newValAsNumber)){ ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false); } else{ newVal = newValAsNumber.toFixed(fraction); ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', true); } return newVal; });
You can see my revised version here - http://jsfiddle.net/KPeBD/64/
I have refactored the original directive, so that it uses $parses and $formatters instead of listening to keyboard events. There is also no need to use $browser.defer
See working demo here http://jsfiddle.net/davidvotrubec/ebuqo6Lm/
var myApp = angular.module('myApp', []); myApp.controller('MyCtrl', function($scope) { $scope.numericValue = 12345678; }); //Written by David Votrubec from ST-Software.com //Inspired by http://jsfiddle.net/KPeBD/2/ myApp.directive('sgNumberInput', ['$filter', '$locale', function ($filter, $locale) { return { require: 'ngModel', restrict: "A", link: function ($scope, element, attrs, ctrl) { var fractionSize = parseInt(attrs['fractionSize']) || 0; var numberFilter = $filter('number'); //format the view value ctrl.$formatters.push(function (modelValue) { var retVal = numberFilter(modelValue, fractionSize); var isValid = isNaN(modelValue) == false; ctrl.$setValidity(attrs.name, isValid); return retVal; }); //parse user's input ctrl.$parsers.push(function (viewValue) { var caretPosition = getCaretPosition(element[0]), nonNumericCount = countNonNumericChars(viewValue); viewValue = viewValue || ''; //Replace all possible group separators var trimmedValue = viewValue.trim().replace(/,/g, '').replace(/`/g, '').replace(/'/g, '').replace(/\u00a0/g, '').replace(/ /g, ''); //If numericValue contains more decimal places than is allowed by fractionSize, then numberFilter would round the value up //Thus 123.109 would become 123.11 //We do not want that, therefore I strip the extra decimal numbers var separator = $locale.NUMBER_FORMATS.DECIMAL_SEP; var arr = trimmedValue.split(separator); var decimalPlaces = arr[1]; if (decimalPlaces != null && decimalPlaces.length > fractionSize) { //Trim extra decimal places decimalPlaces = decimalPlaces.substring(0, fractionSize); trimmedValue = arr[0] + separator + decimalPlaces; } var numericValue = parseFloat(trimmedValue); var isEmpty = numericValue == null || viewValue.trim() === ""; var isRequired = attrs.required || false; var isValid = true; if (isEmpty && isRequired) { isValid = false; } if (isEmpty == false && isNaN(numericValue)) { isValid = false; } ctrl.$setValidity(attrs.name, isValid); if (isNaN(numericValue) == false && isValid) { var newViewValue = numberFilter(numericValue, fractionSize); element.val(newViewValue); var newNonNumbericCount = countNonNumericChars(newViewValue); var diff = newNonNumbericCount - nonNumericCount; var newCaretPosition = caretPosition + diff; if (nonNumericCount == 0 && newCaretPosition > 0) { newCaretPosition--; } setCaretPosition(element[0], newCaretPosition); } return isNaN(numericValue) == false ? numericValue : null; }); } //end of link function }; //#region helper methods function getCaretPosition(inputField) { // Initialize var position = 0; // IE Support if (document.selection) { inputField.focus(); // To get cursor position, get empty selection range var emptySelection = document.selection.createRange(); // Move selection start to 0 position emptySelection.moveStart('character', -inputField.value.length); // The caret position is selection length position = emptySelection.text.length; } else if (inputField.selectionStart || inputField.selectionStart == 0) { position = inputField.selectionStart; } return position; } function setCaretPosition(inputElement, position) { if (inputElement.createTextRange) { var range = inputElement.createTextRange(); range.move('character', position); range.select(); } else { if (inputElement.selectionStart) { inputElement.focus(); inputElement.setSelectionRange(position, position); } else { inputElement.focus(); } } } function countNonNumericChars(value) { return (value.match(/[^a-z0-9]/gi) || []).length; } //#endregion helper methods }]);
Github code is here [https://github.com/ST-Software/STAngular/blob/master/src/directives/SgNumberInput]