I'm having an issue formatting an input field, while leaving the underlying scope variable non-formatted.
What I want to achieve is a text field to display currency. It should format itself on the fly, while handling wrong input. I got that working, but my problem is that I want to store the non-formatted value in my scope variable. The issue with input is that it requires a model which goes both ways, so changing the input field updates the model, and the other way around.
I came upon $parsers
and $formatters
which appears to be what I am looking for. Unfortunately they are not affecting each other (which might actually be good to avoid endless loops).
I've created a simple jsFiddle: http://jsfiddle.net/cruckie/yE8Yj/ and the code is as follows:
HTML:
<div data-ng-app="app" data-ng-controller="Ctrl">
<input type="text" data-currency="" data-ng-model="data" />
<div>Model: {{data}}</div>
</div>
JS:
var app = angular.module("app", []);
function Ctrl($scope) {
$scope.data = 1234567;
}
app.directive('currency', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attr, ctrl) {
ctrl.$formatters.push(function(modelValue) {
return modelValue.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ',');
});
ctrl.$parsers.push(function(viewValue) {
return parseFloat(viewValue.replace(new RegExp(",", "g"), ''));
});
}
};
});
Again, this is just a simple example. When it loads everything looks as it's supposed to. The input field is formatted and the variable is not. However, when changing the value in the input field it no longer formats itself - the variable however gets updated correctly.
Is there a way to ensure the text field being formatted while the variable is not? I guess what I am looking for is a filter for text fields, but I can't seen to find anything on that.
Best regards
Restrict. Angular allows us to set a property named restrict on the object we return on our directive definition. We can pass through a string with certain letters letting Angular know how our directive can be used. function MyDirective() { return { restrict: 'E', template: '<div>Hello world!
Note: When you create a directive, it is restricted to attribute and elements only by default. In order to create directives that are triggered by class name, you need to use the restrict option. The restrict option is typically set to: 'A' - only matches attribute name.
The directive scope uses prefixes to achieve that. Using prefixes helps establish a two-way or one-way binding between parent and directive scopes, and also make calls to parent scope methods. To access any data in the parent scope requires passing the data at two places – the directive scope and the directive tag.
Create New Directives In addition to all the built-in AngularJS directives, you can create your own directives. New directives are created by using the . directive function. To invoke the new directive, make an HTML element with the same tag name as the new directive.
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.
http://jsfiddle.net/KPeBD/2/
I've revised a little what Wade Tandy had done, and added support for several features:
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With