AngularJS number input formatted view - angularjs

I want to use a formatted number input to show thousand seperator dots to user when he types big numbers. Here is the directive code that I used: http://jsfiddle.net/LCZfd/3/
When I use input type="text" it works, but when I want to use input type="number" it's weirdly cleaning by something when user typing big numbers.
What is problem about input[number]?

As written in the comments, input type="number" doesn't support anything but digits, a decimal separator (usually , or . depending on the locale) and - or e. You may still enter whatever you want, but the browser will discard any unknown / incorrect character.
This leaves you with 2 options:
Use type="text" and pattern validation like pattern="[0-9]+([\.,][0-9]+)*" to limit what the user may enter while automatically formatting the value as you do in your example.
Put an overlay on top of the input field that renders the numbers how you want and still allows the user to use the custom type="number" input controls, like demonstrated here.
The latter solution uses an additional <label> tag that contains the current value and is hidden via CSS when you focus the input field.

All these years later, there still isn't an HTML5 solution out of the box for this.
I am using <input type="tel"> or <input type="text"> ("tel" brings up a numeric keyboard in Android and iOS, which in some cases is a bonus.)
Then I needed a directive to:
filter out non-numeric characters
add thousand-separator commas as the user types
use $parsers and keyup to set elem.val() and $formatters to set the display...
...while behind the scenes, assign ng-model a floating point number
The directive example below does this, and it accepts negatives and floating point numbers unless you specify you want only positive or integers.
It's not the full solution I would like, but I think it bridges the gap.
HTML
<input type="text" ng-model="someNumber" number-input />
JAVASCRIPT
myApp.directive('numberInput', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.push(function(modelValue) {
return setDisplayNumber(modelValue, true);
});
// it's best to change the displayed text using elem.val() rather than
// ngModelCtrl.$setViewValue because the latter will re-trigger the parser
// and not necessarily in the correct order with the changed value last.
// see http://radify.io/blog/understanding-ngmodelcontroller-by-example-part-1/
// for an explanation of how ngModelCtrl works.
ngModelCtrl.$parsers.push(function(viewValue) {
setDisplayNumber(viewValue);
return setModelNumber(viewValue);
});
// occasionally the parser chain doesn't run (when the user repeatedly
// types the same non-numeric character)
// for these cases, clean up again half a second later using "keyup"
// (the parser runs much sooner than keyup, so it's better UX to also do it within parser
// to give the feeling that the comma is added as they type)
elem.bind('keyup focus', function() {
setDisplayNumber(elem.val());
});
function setDisplayNumber(val, formatter) {
var valStr, displayValue;
if (typeof val === 'undefined') {
return 0;
}
valStr = val.toString();
displayValue = valStr.replace(/,/g, '').replace(/[A-Za-z]/g, '');
displayValue = parseFloat(displayValue);
displayValue = (!isNaN(displayValue)) ? displayValue.toString() : '';
// handle leading character -/0
if (valStr.length === 1 && valStr[0] === '-') {
displayValue = valStr[0];
} else if (valStr.length === 1 && valStr[0] === '0') {
displayValue = '';
} else {
displayValue = $filter('number')(displayValue);
}
// handle decimal
if (!attrs.integer) {
if (displayValue.indexOf('.') === -1) {
if (valStr.slice(-1) === '.') {
displayValue += '.';
} else if (valStr.slice(-2) === '.0') {
displayValue += '.0';
} else if (valStr.slice(-3) === '.00') {
displayValue += '.00';
}
} // handle last character 0 after decimal and another number
else {
if (valStr.slice(-1) === '0') {
displayValue += '0';
}
}
}
if (attrs.positive && displayValue[0] === '-') {
displayValue = displayValue.substring(1);
}
if (typeof formatter !== 'undefined') {
return (displayValue === '') ? 0 : displayValue;
} else {
elem.val((displayValue === '0') ? '' : displayValue);
}
}
function setModelNumber(val) {
var modelNum = val.toString().replace(/,/g, '').replace(/[A-Za-z]/g, '');
modelNum = parseFloat(modelNum);
modelNum = (!isNaN(modelNum)) ? modelNum : 0;
if (modelNum.toString().indexOf('.') !== -1) {
modelNum = Math.round((modelNum + 0.00001) * 100) / 100;
}
if (attrs.positive) {
modelNum = Math.abs(modelNum);
}
return modelNum;
}
}
};
});
https://jsfiddle.net/benlk/4dto9738/

You need to add the step attribute to your number input.
<input type="number" step="0.01" />
This will allow floating points.
http://jsfiddle.net/LCZfd/1/
Also, I'd recommend reviewing the bug thread on number inputs in Firefox. You may want to consider not using this input type, as it was just finally supported in this release of FF.
https://bugzilla.mozilla.org/show_bug.cgi?id=344616
http://caniuse.com/input-number

You cannot use values with , because type=number only takes numbers, adding a comma makes it a string.
See http://jsfiddle.net/LCZfd/5
You're better off making your own controls if you want commas. One with a true value (the number) and a display value (the string).

you can try this, I modified the directive I saw here...
How do I restrict an input to only accept numbers? ...
here's the modified directive I made... This directive uses the keyup event to modify the input on the fly...
.directive('numericOnly', function($filter) {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
element.bind('keyup', function (inputValue, e) {
var strinput = modelCtrl.$$rawModelValue;
//filter user input
var transformedInput = strinput ? strinput.replace(/[^,\d.-]/g,'') : null;
//remove trailing 0
if(transformedInput.charAt(0) <= '0'){
transformedInput = null;
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}else{
var decimalSplit = transformedInput.split(".")
var intPart = decimalSplit[0];
var decPart = decimalSplit[1];
//remove previously formated number
intPart = intPart.replace(/,/g, "");
//split whole number into array of 3 digits
if(intPart.length > 3){
var intDiv = Math.floor(intPart.length / 3);
var strfraction = [];
var i = intDiv,
j = 3;
while(intDiv > 0){
strfraction[intDiv] = intPart.slice(intPart.length-j,intPart.length - (j - 3));
j=j+3;
intDiv--;
}
var k = j-3;
if((intPart.length-k) > 0){
strfraction[0] = intPart.slice(0,intPart.length-k);
}
}
//join arrays
if(strfraction == undefined){ return;}
var currencyformat = strfraction.join(',');
//check for leading comma
if(currencyformat.charAt(0)==','){
currencyformat = currencyformat.slice(1);
}
if(decPart == undefined){
modelCtrl.$setViewValue(currencyformat);
modelCtrl.$render();
return;
}else{
currencyformat = currencyformat + "." + decPart.slice(0,2);
modelCtrl.$setViewValue(currencyformat);
modelCtrl.$render();
}
}
});
}
};
you use it like this ...
<input type="text" ng-model="amountallocated" id="amountallocated" numeric-only />

Related

IP address validation while the user is typing in AngularJS

I'm beginner in angularjs. I create a costume directive which validate IP address on keyperss event of textbox. It's work good but, I think there will be more efficient way implementation with use of regx or any other thing like parser or filter etc.
please help me improve this.
//Declare Main and sub modules
angular.module('CompanyApp', []);
//Initialize variables when DOM is ready
angular.module('CompanyApp').run(['$rootScope', function ($rootScope) { } ]);
(function () {
'use strict';
angular.module('CompanyApp').controller('TestCtrl', ['$scope', '$rootScope', TestCtrl]);
function TestCtrl($scope, $rootScope) {
$scope.ipAddress = '';
}
})()
angular.module('CompanyApp').directive("ipValidator", function () {
var directive = {};
directive.restrict = 'A'
directive.compile = function (element, attributes) {
var link = function (scope, element, attrs) {
element.bind("keypress", function (e) {
var key = e.key;
var code = e.keyCode;
var txt = element.val();
txt += key;
var len = txt.length;
var lastchar = txt.charAt(len-2);
var txtList = txt.split('.');
if ((len == 1 ||lastchar==='.')&& code == 46) {
e.preventDefault();
}
else if (!(code <= 57 && code >= 46) || len >= 16 || txtList.length > 4) {
e.preventDefault();
}
else {
txtList.forEach(function (value) {
var intValue = parseInt(value);
if (intValue > 255 || intValue < 0) {
e.preventDefault();
}
});
}
});
}
return link;
}
return directive;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.2/angular.min.js"></script>
<div name="divCompanyContainer" ng-app="CompanyApp" ng-controller="TestCtrl" ng-init="Init()">
<input type="text" ng-model="ipAddress" ip-validator placeholder="Ip address" /> <br />
</div>
First, lets talk about regular expression. Then, the algorithm we could use.
Regular expressions and IP
(Note: the IP regexps came from Regular-expressions.info - How to Find or Validate an IP Address.)
The whole regular expression for an IP address could be:
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
You can see that the three first bytes are both checked by the same pattern below (surrounded by ^ and $):
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.)$
The fourth byte is different:
^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
Simple algorithm
(Note: I'm only considering the keypress event.)
Since the IP is validated incrementally, we could assume at each validation that a part is already correct.
The algorithm would be, for each key press:
Get an array of entered bytes separated by a dot .
If the array contains more than four items, the user entered an extra dot after a valid IP, input is rejected, return false
If the penultimate byte is an empty string, the user tried to enter a second dot, input is rejected, return false
If the last byte is an empty string, that's okay because the user just entered a dot, return true
The last byte is not empty, let's check it with the relevant regexp; if it matches its regexp then return true else return false.
Below a working snippet you could adapt.
var first3BytesRg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$/;
var fourthByteRg = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
document.getElementById("ip").onkeypress = preventInvalid;
function preventInvalid(e) {
return validateIP(e.target.value + String.fromCharCode(e.which));
}
function validateIP(ip) {
var splitted = ip.split(".");
var nb = splitted.length;
if (nb > 4) return false;
if (splitted[nb - 2] == "") return false;
if (splitted[nb - 1] == "") return true;
if (nb < 4) {
return first3BytesRg.test(splitted[nb - 1]);
}
return fourthByteRg.test(splitted[nb - 1]);
}
IP <input type="text" id="ip" />

How to add thousand separating commas for numbers in angularJS?

I simply want to convert a string of numbers to a number which will be displayed using thousand separated commas.
var value = "123456";
I want to display "123,465" in a grid.
I have looked some documentation on this but everything is about displaying it in HTML.
I want to display this in a dynamic grid.
function numberRenderer (params) {
return new Number (params.value);
}
I want to format the number so that I can convert that into a string for display.
Use a filter ...
HTML usage
{{ number_expression | number : fractionSize}}
Js usage
$filter('number')(number, fractionSize)
I appreciated the answer from #jbrown, but I was also hoping to find some type of solution to add commas to an input field as the user enters numbers. I ended up finding this directive which proved to be exactly what I needed.
HTML
<input type="text" ng-model="someNumber" number-input />
JAVASCRIPT
myApp.directive('numberInput', function($filter) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.push(function(modelValue) {
return setDisplayNumber(modelValue, true);
});
// it's best to change the displayed text using elem.val() rather than
// ngModelCtrl.$setViewValue because the latter will re-trigger the parser
// and not necessarily in the correct order with the changed value last.
// see http://radify.io/blog/understanding-ngmodelcontroller-by-example-part-1/
// for an explanation of how ngModelCtrl works.
ngModelCtrl.$parsers.push(function(viewValue) {
setDisplayNumber(viewValue);
return setModelNumber(viewValue);
});
// occasionally the parser chain doesn't run (when the user repeatedly
// types the same non-numeric character)
// for these cases, clean up again half a second later using "keyup"
// (the parser runs much sooner than keyup, so it's better UX to also do it within parser
// to give the feeling that the comma is added as they type)
elem.bind('keyup focus', function() {
setDisplayNumber(elem.val());
});
function setDisplayNumber(val, formatter) {
var valStr, displayValue;
if (typeof val === 'undefined') {
return 0;
}
valStr = val.toString();
displayValue = valStr.replace(/,/g, '').replace(/[A-Za-z]/g, '');
displayValue = parseFloat(displayValue);
displayValue = (!isNaN(displayValue)) ? displayValue.toString() : '';
// handle leading character -/0
if (valStr.length === 1 && valStr[0] === '-') {
displayValue = valStr[0];
} else if (valStr.length === 1 && valStr[0] === '0') {
displayValue = '';
} else {
displayValue = $filter('number')(displayValue);
}
// handle decimal
if (!attrs.integer) {
if (displayValue.indexOf('.') === -1) {
if (valStr.slice(-1) === '.') {
displayValue += '.';
} else if (valStr.slice(-2) === '.0') {
displayValue += '.0';
} else if (valStr.slice(-3) === '.00') {
displayValue += '.00';
}
} // handle last character 0 after decimal and another number
else {
if (valStr.slice(-1) === '0') {
displayValue += '0';
}
}
}
if (attrs.positive && displayValue[0] === '-') {
displayValue = displayValue.substring(1);
}
if (typeof formatter !== 'undefined') {
return (displayValue === '') ? 0 : displayValue;
} else {
elem.val((displayValue === '0') ? '' : displayValue);
}
}
function setModelNumber(val) {
var modelNum = val.toString().replace(/,/g, '').replace(/[A-Za-z]/g, '');
modelNum = parseFloat(modelNum);
modelNum = (!isNaN(modelNum)) ? modelNum : 0;
if (modelNum.toString().indexOf('.') !== -1) {
modelNum = Math.round((modelNum + 0.00001) * 100) / 100;
}
if (attrs.positive) {
modelNum = Math.abs(modelNum);
}
return modelNum;
}
}
};
});
AngularJS Directive was found from: AngularJS number input formatted view
https://jsfiddle.net/benlk/4dto9738/
Very appreciative of what Anguna posted. The only thing it was missing for me was handling the decimal places like currency. I wanted it to automatically add 2 decimal places to the displayed value. However, this should only occur on initial display and then again when leaving a field. I updated the code to handle that scenario.
var app = angular.module("myApp", []);
app.directive('currencyInput', function ($filter) {
return {
require: 'ngModel',
link: function (scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.push(function (modelValue) {
var displayValue = setDisplayNumber(modelValue, true);
displayValue = setDecimal(displayValue);
return displayValue;
});
// it's best to change the displayed text using elem.val() rather than
// ngModelCtrl.$setViewValue because the latter will re-trigger the parser
// and not necessarily in the correct order with the changed value last.
// see http://radify.io/blog/understanding-ngmodelcontroller-by-example-part-1/
// for an explanation of how ngModelCtrl works.
ngModelCtrl.$parsers.push(function (viewValue) {
setDisplayNumber(viewValue);
return setModelNumber(viewValue);
});
// occasionally the parser chain doesn't run (when the user repeatedly
// types the same non-numeric character)
// for these cases, clean up again half a second later using "keyup"
// (the parser runs much sooner than keyup, so it's better UX to also do it within parser
// to give the feeling that the comma is added as they type)
elem.bind('keyup focus', function () {
setDisplayNumber(elem.val());
});
elem.bind('blur', function () {
// Add Decimal places if they do not exist
var valStr = elem.val().toString();
valStr = setDecimal(valStr);
elem.val(valStr);
});
function setDisplayNumber(val, formatter) {
var valStr, displayValue;
if (typeof val === 'undefined') {
return 0;
}
valStr = val.toString();
displayValue = valStr.replace(/,/g, '').replace(/[A-Za-z]/g, '');
displayValue = parseFloat(displayValue);
displayValue = (!isNaN(displayValue)) ? displayValue.toString() : '';
// handle leading character -/0
if (valStr.length === 1 && valStr[0] === '-') {
displayValue = valStr[0];
} else if (valStr.length === 1 && valStr[0] === '0') {
displayValue = '';
} else {
displayValue = $filter('number')(displayValue);
}
// handle decimal
if (!attrs.integer) {
if (displayValue.indexOf('.') === -1) {
if (valStr.slice(-1) === '.') {
displayValue += '.';
} else if (valStr.slice(-2) === '.0') {
displayValue += '.0';
} else if (valStr.slice(-3) === '.00') {
displayValue += '.00';
}
} // handle last character 0 after decimal and another number
else {
if (valStr.slice(-1) === '0') {
displayValue += '0';
}
}
}
if (attrs.positive && displayValue[0] === '-') {
displayValue = displayValue.substring(1);
}
if (typeof formatter !== 'undefined') {
return (displayValue === '') ? 0 : displayValue;
} else {
elem.val((displayValue === '0') ? '' : displayValue);
}
}
function setModelNumber(val) {
var modelNum = val.toString().replace(/,/g, '').replace(/[A-Za-z]/g, '');
modelNum = parseFloat(modelNum);
modelNum = (!isNaN(modelNum)) ? modelNum : 0;
if (modelNum.toString().indexOf('.') !== -1) {
modelNum = Math.round((modelNum + 0.00001) * 100) / 100;
}
if (attrs.positive) {
modelNum = Math.abs(modelNum);
}
return modelNum;
}
function setDecimal(val) {
// Add Decimal places if they do not exist
var valStr = val.toString();
// If no decimal then add it
if (valStr.indexOf('.') === -1) {
valStr += '.00';
}
else {
var decimalDigits = valStr.length - (valStr.indexOf('.') + 1);
var missingZeros = 2 - decimalDigits;
for (var i = 1; i <= missingZeros; i++) {
valStr += '0';
}
}
return valStr;
}
}
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js"></script>
<div ng-app="myApp">
<input type="text" ng-model="myModelValue" currency-input />
</div>

AngularJS directive with ui-mask

I am creating a directive to validate the age of a user and I am using it in combination with ui-mask. The html for my input is as such:
<input id="dateOfBirth" name="dateOfBirth" type="text" class="form-control" ng-model="owner.dateOfBirth" placeholder="mm/dd/yyyy" required ui-mask="99/99/9999" minimum-age="18">
and the directive (copied from the blacklist directive example here on stackoverflow):
angular.module('Pensco.AEW.Directives')
.directive('minimumAge', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var minimumAge = parseInt(attr.minimumAge, 10),
minDate = new Date(),
monthsToSubtract = 12 * minimumAge;
// subtract the months from today
minDate.setMonth(minDate.getMonth() - monthsToSubtract);
//For DOM -> model validation
ngModel.$parsers.unshift(function(value) {
var valid = isValid(value);
ngModel.$setValidity('minimumAge', valid);
return value;
});
//For model -> DOM validation
ngModel.$formatters.unshift(function(value) {
var valid = isValid(value);
ngModel.$setValidity('minimumAge', valid);
return value;
});
function cleanMask(value) {
var parts;
if (!value) { return; }
parts = value.split('/');
parts[0] = checkLength(parts[0], 2, 'm');
parts[1] = checkLength(parts[1], 2, 'd');
parts[2] = checkLength(parts[2], 4, 'y');
return parts.join('/');
}
function checkLength(value, len, character) {
if (value.length > len) {
value = value.slice(0, - 1);
} else if (value.length < len) {
value = value + character;
}
return value;
}
function isValid(value) {
var cleanedValue = cleanMask(value),
dob = new Date(cleanedValue);
// if the date is valid then compare it to calculated min date
if (isDate(dob)) {
return dob <= minDate;
}
return false;
}
function isDate(value) {
return value instanceof Date && isFinite(value)
}
}
};
});
The issue is that value is that when the user types a number such as 0, value is set to "0mm/dd/yyyy" as it appears that ui-mask has not removed the placeholder character yet to make it "0m/dd/yyyy". ui-mask has its priority set to 100, so I set the priority of this directive to -1000 thinking it might have the mask finish processing before processing my directive. After my directive is completed it appears that ui-mask finishes processing and removes the placeholder character.
I added a couple of methods to clean and validate the dates, which works, and yet, I have to believe there is a cleaner way to handle it. Any ideas on how I can my directive to fire after ui-mask is done processing?
I found a issue in your code.In checklength methos throw TypeError: Cannot read property 'length' of undefined
I have change the code to
function checkLength(value, len, character) {
if(value!=undefined)
{
if (value.length > len) {
value = value.slice(0, - 1);
} else if (value.length < len) {
value = value + character;
}
}
return value;
}
Check this Working Sample which is working correctly.

Using AngularJS directive to format input field while leaving scope variable unchanged

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
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:
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]
Indeed the $parsers and $formatters are "independent" as you say (probably for loops, again as you say). In our application we explicitly format with the onchange event (inside the link function), roughly as:
element.bind("change", function() {
...
var formattedModel = format(ctrl.$modelValue);
...
element.val(formattedModel);
});
See your updated fiddle for the detailed and working example: http://jsfiddle.net/yE8Yj/1/
I like binding to the onchange event, because I find it annoying to change the input as the user is typing.
The fiddle is using an old version of angular(1.0.7).
On updating to a recent version, 1.2.6, the $render function of the ngModelCtrl is never called, meaning if the model value is changed in the controller,
the number is never formatted as required in the view.
//check if new value is numeric, and set control validity
if (isNaN(newValAsNumber)){
ngModelCtrl.$setValidity(ngModelCtrl.$name+'Numeric', false);
}
Here is the updated fiddle http://jsfiddle.net/KPeBD/78/
Based on the answer from Wade Tandy here is a new jsfiddle with following improvements:
decimal numbers possible
thousands separator and decimal separator based on locals
and some other tweaks ...
I have also replaces all String.replace(regex) by split().join(), because this allows me to use variables in the expression.
http://jsfiddle.net/KPeBD/283/

Directly access variable by parameter - make a generic function

I'm trying to achieve a kind of generic remaining function. I have a bunch of fields in my form, for example: name and address. These fields have a limit of characters size, my goal is to make a generic function to limit it's size when reached. So I made a sample function that works:
First on the page:
label(for='name') Name {{remaining()}} of {{totalChars}}
Second, the code to handle it:
$scope.totalChars = 10;
$scope.remaining = function() {
var count = 0;
if ($scope.mName) {
count = $scope.mName.length;
if (count > $scope.totalChars) {
$scope.mName = $scope.mName.trim().substr(0, $scope.mName.length - 1);
count = $scope.mName.length;
}
}
return count;
//return calculateChars($scope.mName, $scope.totalChars);
};
When I type some input value into name field, angular stops typing when 10 chars are reached as well. But I've remade the function to turn it in a generic way and try to use it for any field I want to, but doesn't work as expected:
$scope.totalChars = 10;
$scope.remaining = function() {
return calculateChars($scope.mName, $scope.totalChars);
};
...
function calculateChars(obj, size) {
var count = 0;
if (obj && obj !== 'undefined') {
count = obj.length;
if (count > size) {
$scope.obj = obj.trim().substr(0, obj.length - 1);
console.log('Result: ' + $scope.obj);
count = obj.length;
}
}
return count;
}
The calculateChars works partiality fine, the problem is $scope.obj = obj.trim().substr(0, obj.length - 1); because angularjs doesn't know what "obj" is and doesn't stop typing when 10 chars are reached, even counting the amount correctly.
I don't know how to make the first approach works for any case without duplicate any code for any text field I want to.
Thanks in advance!
It sounds like you're looking for a directive. Here's an example of a directive named remaining that, given a model and a "max length" attribute, displays how many characters are remaining. It also prevents the user from typing more than the max number of characters; this can be removed by getting rid of the first branch of the if (remaining < 0) check in the link function.
app.directive('remaining', function() {
return {
template: "{{remaining}} of {{maxLen}}",
scope: {
maxLen: '#max',
model: '=ngModel'
},
link: function(scope, elem, attrs) {
scope.$watch('model', function(val) {
if (val == null || val == undefined) return;
var maxLen = parseInt(scope.maxLen, 10);
var remaining = maxLen - val.length;
if (remaining < 0)
scope.model = val.substr(0, maxLen);
else
scope.remaining = remaining;
});
}
};
});
Here's a jsFiddle to demonstrate: http://jsfiddle.net/BinaryMuse/JanZm/. Note I'm using version 1.1.x of AngularJS to get access to the ngTrim directive, which tells Angular not to strip off whitespace in text fields.

Resources