Why is ng-maxlength not re-evaluated? - angularjs

I have a form, that has one input field and three check boxes. Depending on which check box is selected the max length on the field needs to change. I have a input field defined like this
<input placeholder="ID" type="text" id="form_ID" name="searchId" autofocus
data-ng-model="vm.searchCriteria.searchId" data-ng-required="vm.isSearchIdRequired"
data-ng-minlength="1" data-ng-maxlength="{{searchIdMaxLength}}"
data-ng-class="{'input-error': vm.isSearchIdValid}">
and one of the checkboxes
<input type="checkbox" id="checkbox1" class="hidden-field"
data-ng-model="vm.searchCriteria.searchIdInSrId" data-ng-checked="vm.searchCriteria.searchIdInSrId"
data-ng-change="processSearchIdOptionsChange()">
So everytime user changes which checkbox is/are selected processSearchIdOptionsChange gets called, and searchIdMaxLength changes it's value. This is all working fine and I can see the value being changed on the $scope. But, my initial max length is still being applied. Following error pops up after initial max number of chars is reached. Why?
<span class="error" data-ng-show="(searchForm.$dirty && searchForm.searchId.$error.maxlength)">Too long!</span>

This is the intended behaviour of ng-maxlength. Verified from source : https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js?source=c#L523
The value is parsed only once and cached :
var maxlength = int(attr.ngMaxlength);
If you want to observe the change you need to create your own directive which uses something like
scope.$watch(attr.namespaceMaxLength,function(){
// clear old validator. Add new one.
})

After a lot of trial and error here is the directive that does what I need. Please if you have any suggestions or improvements share them, I have only 7 days of angular under my belt, and javascript is something that i have a cursory knowledge of.
(function () {
'use strict';
angular.module('commonModule')
.directive('srMaxlength', ['$window', srMaxlength]);
function srMaxlength($window) {
// Usage:
// use if you need to switch max length validation dynamically based on
// Creates:
// removes old validator for max length and creates new one
var directive = {
require: 'ngModel',
link: link,
restrict: 'A'
};
return directive;
function link(scope, element, attrs, ctrl) {
attrs.$observe("srMaxlength", function (newval) {
var maxlength = parseInt(newval, 10);
var name = "srMaxLengthValidator";
for (var i = ctrl.$parsers.length - 1; i >= 0; i--) {
if (ctrl.$parsers[i].name !== undefined && ctrl.$parsers[i].name == name) {
ctrl.$parsers.splice(i, 1);
}
}
for (var j = ctrl.$formatters.length - 1; j >= 0; j--) {
if (ctrl.$formatters[j].name !== undefined && ctrl.$formatters[j].name == name) {
ctrl.$formatters.splice(j, 1);
}
}
ctrl.$parsers.push(maxLengthValidator);
ctrl.$formatters.push(maxLengthValidator);
//name the function so we can find it always by the name
maxLengthValidator.name = name;
function maxLengthValidator(value) {
if (!ctrl.$isEmpty(value) && value.length > maxlength) {
ctrl.$setValidity('maxlength', false);
return undefined;
} else {
ctrl.$setValidity('maxlength', true);
return value;
}
}
});
}
}
})();

Related

preventing maxLength on a input type number

I took me a while to figure this one out, but can someone post a cleaner method for limiting the number of digits in an input type='number'. One of the issues is that errors are thrown if $scope.variable = null....meaning nothing in the input field.
<input type="number" model='modalName' ng-change="monitorLength('modalName',16)">
JS:
$scope.monitorLength = function (model,maxLength) {
if ($scope[model] != null) { // prevent error on empty input field
var len = $scope[model].toString() ; // convert to a string
if (len.length > maxLength) { //evaluate string length
$scope[model] = parseInt(len.substring(0, maxLength));
// convert back to number or warning is thrown on input value not being a number
}
}
}
I then needed to expand up on this to account for number only, preventing any non-digit characters include '.' and ',' symbols:
var reg = new RegExp(/^\d+$/) ;
$scope.monitorLength = function (modal,maxLength) {
if ($scope[modal] != null) {
var len = $scope[modal].toString() ;
if (len.length > maxLength) {
$scope[modal] = parseInt(len.substring(0, maxLength));
} else if (!reg.test(len)) {
$scope[modal] = parseInt(len.substring(0, len.length-2));
}
}
}
Is there way to extract the ng-modal that was responsible for calling the ng-change? so the call would only have to be: ng-change="monitorLength(10)". And then in the function somehow dynamically retrieve the calling ng-modal?
<input type="number" max="99" onkeypress="if (this.value.length >= 2) return false;"/>
OR
<!--maxlength="10"-->
<input type="number" onKeyPress="if(this.value.length==10) return false;" />
this is a cleaner method for limiting the number, using ngMaxlength for that:
<input type="number" model='modalName' ng-maxlength="16">
You can find more attributes and info here
Is there way to extract the ng-modal that was responsible for calling the ng-change?
Yes. You can define a directive and require the ngModelController.
.directive('maxNum', function(){
return {
require: '^ngModel',
link: function($scope, elem, attrs){
// here you can add formatters/parsers to the ngModel
// to affect the change on the ngModel.$viewValue.
}
}
})
As #rolinger stated on the other answer, using the built in directives will not prevent the use from entering non-valid characters, they simply mark the model as being invalid.

How to revalidate a form with multiple dependent fields?

I'm fairly new to Angular. I have a form where the user need to assign port numbers to 9 different port input fields (context: it's a form for a server environment configuration). The validation requirement is that no port number can be assigned twice, so each of the 9 port numbers needs to be unique.
For that, I have a custom validation directive called "srb-unique-port", which I assign to my input fields.
Directive:
(function () {
'use strict';
angular
.module('account')
.directive('srbUniquePort', [srbUniquePort]);
function srbUniquePort() {
return {
restrict: 'A',
require: 'ngModel',
scope: true,
link: function (scope, element, attrs, ngModel) {
ngModel.$validators.srbUniquePort = function (val) {
if (val == null || val == undefined || val == "" || val==0) return true;
var fieldName = attrs.name;
var configuration = scope.$eval(attrs.srbUniquePort);
var portFieldsToCheck = [
"myRestServicePort",
"myRestServicePortSSL",
"alfrescoPortHttp",
"alfrescoPortHttps",
"alfrescoPortTomcatShutdown",
"alfrescoPortAJP",
"alfrescoPortMySql",
"alfrescoPortJOD",
"alfrescoPortVti"
];
for (var i = 0; i < portFieldsToCheck.length; i++) {
if (fieldName!=portFieldsToCheck[i] && configuration[portFieldsToCheck[i]] == val) {
return false;
}
}
return true;
}
}
}
}
})();
HTML form (excerpt, just showing 2 of the 9 fields):
...
<md-input-container>
<label for="company" translate>COMPANY.CONFIGURATION.DBLIB_WEB_SRVC_PORT</label>
<input ng-model="vm.configuration.dblibWebSrvcPort" name="dblibWebSrvcPort" srb-unique-port="vm.configuration">
<div ng-messages="configurationForm.dblibWebSrvcPort.$error">
<div ng-message when="srbUniquePort">
<span translate>COMPANY.CONFIGURATION.VALIDATION.PORT_NOT_UNIQUE</span>
</div>
</div>
</md-input-container>
<md-input-container>
<label for="company" translate>COMPANY.CONFIGURATION.DBLIB_WEB_SRVC_PORT_SSL</label>
<input ng-model="vm.configuration.dblibWebSrvcPortSLL" name="dblibWebSrvcPortSLL" srb-unique-port="vm.configuration">
<div ng-messages="configurationForm.dblibWebSrvcPortSLL.$error">
<div ng-message when="srbUniquePort">
<span translate>COMPANY.CONFIGURATION.VALIDATION.PORT_NOT_UNIQUE</span>
</div>
</div>
</md-input-container>
...
It basically works for the field that I am current entering a value into. But the problem is that when I change the value of one input field, I need to re-validate all other depending fields as well. But I am not sure what the best way is in order to not run into an endless loop here, since all fields have the "srb-unique-port" assigned.
I already looked on StackOverflow and found this very similar question:
Angular directive with scope.$watch to force validation of other fields
with this plunker sample code:
http://plnkr.co/edit/YnxDDAUCS2K7KyXT1AXP?p=preview
but the example provided there is different: it's only about a password and a password repeat field, where only one field has the validation directive assigned.
So it differs from my case.
I tried to add this in my above code:
scope.$watch(ngModel, function (newValue, oldValue) {
ngModel.$validate();
});
but this causes endless loops (why does the ngModel frequently change here without any further action other than a validation which should always result to the same?).
This is the solution I ended up with. Looks a bit hacked to me, but it works.
(function () {
'use strict';
angular
.module('account')
.directive('srbUniquePort', [srbUniquePort]);
function srbUniquePort() {
return {
restrict: 'A',
require: 'ngModel',
scope: true,
link: function (scope, element, attrs, ngModel) {
function hasAValue(field) {
return !!field;
}
ngModel.$validators.srbUniquePort = function (val) {
var fieldName = attrs.name;
var configuration = scope.$eval(attrs.srbUniquePort);
var portFieldsToCheck = [
"dblibWebSrvcPort",
"dblibWebSrvcPortSLL",
"myRestServicePort",
"myRestServicePortSSL",
"alfrescoPortHttp",
"alfrescoPortHttps",
"alfrescoPortTomcatShutdown",
"alfrescoPortAJP",
"alfrescoPortMySql",
"alfrescoPortJOD",
"alfrescoPortVti"
];
configuration[fieldName] = val;
if (scope.$parent.configuration == undefined) {
scope.$parent.configuration = JSON.parse(JSON.stringify(configuration));
}
scope.$parent.configuration[fieldName] = val;
// compare each port field with each other and in case if equality,
// remember it by putting a "false" into the validityMap helper variable
var validityMap = [];
for (var i = 0; i < portFieldsToCheck.length; i++) {
for (var j = 0; j < portFieldsToCheck.length; j++) {
if (portFieldsToCheck[i] != portFieldsToCheck[j]) {
var iFieldHasAValue = hasAValue(scope.$parent.configuration[portFieldsToCheck[i]]);
var jFieldHasAValue = hasAValue(scope.$parent.configuration[portFieldsToCheck[j]]);
var valHasAValue = hasAValue(val);
if (iFieldHasAValue && jFieldHasAValue
&& scope.$parent.configuration[portFieldsToCheck[i]] == scope.$parent.configuration[portFieldsToCheck[j]]
) {
validityMap[portFieldsToCheck[i]] = false;
validityMap[portFieldsToCheck[j]] = false;
}
}
}
}
// in the end, loop through all port fields and set
// the validity here manually
for (var i = 0; i < portFieldsToCheck.length; i++) {
var valid = validityMap[portFieldsToCheck[i]];
if (valid == undefined) valid = true;
ngModel.$$parentForm[portFieldsToCheck[i]].$setValidity("srbUniquePort", valid);
}
// ending with the standard validation for the current field
for (var i = 0; i < portFieldsToCheck.length; i++) {
if (fieldName != portFieldsToCheck[i] && configuration[portFieldsToCheck[i]] == val) {
return false;
}
}
return true;
}
}
}
}
})();

Update view with formatted value, model with numeric value

I have a need for input boxes to display values formatted based on the user's locale but the model must store the value in en-US locale and all of this happens on blur. I've got the formatting of the fields working when the user clicks off of them but I cannot figure out how to set the model value. In my code formatedValue is being set correctly for the user to view but how do I update the model value to be "valueToFormat"? I've tried
scope.$modelValue = valueToFormat;
and it works when watching it thru the debugger but as soon as the view is rendered the value reverts to the $viewValue. How can I accomplish this?
element.bind('blur', function () {
var val = ctrl.$modelValue;
parse(val);
})
ctrl.$formatters.push(function(value) {
if(!value) {
return value;
}
var valueToFormat = getActualValue(value, decimalDelimiter, thousandsDelimiter, decimals, '%') || '0';
return viewMask.apply(prepareNumberToFormatter(valueToFormat, decimals));
});
function parse(value) {
if(!value) {
return value;
}
var valueToFormat = getActualValue(value, decimalDelimiter, thousandsDelimiter, decimals) || '0';
var formatedValue = viewMask.apply(prepareNumberToFormatter(valueToFormat, decimals));
var actualNumber = parseFloat(modelMask.apply(valueToFormat));
ctrl.$viewValue = formatedValue;
ctrl.$render();
return valueToFormat;
}
You should be able to use a filter to solve this issue. For example this is a date filter that we use. This is with typescript so a little tweaking will be necessary for straight java-script but it gets the point across.
app.filter('MyDateFilter', ['$filter', function ($filter: ng.IFilterService)
{
return function (value:any, format)
{
if (typeof value == "string")
{
var stringValue = <string>value;
//If there is no / or - then assume its not a date
if (stringValue.indexOf('/') == -1 && stringValue.indexOf('-') == -1)
return value;
}
var parsedDate = Date.parse(value);
if (isNaN(parsedDate))
return value;
else
return $filter('date')(value, format);
}
}]);
And then in the html.
<div ng-repeat="value in values" >
{{value | MyDateFilter:"MM/dd/yyyy" }}
</div>
Since it seems like you want to change an input display value. here is a solution we use for that. I created a custom directive and inside of there you can manipulte the $render. So:
app.directive("myDateDisplayInput", ['$filter', function DatepickerDirective(filter: ng.IFilterService): ng.IDirective
{
return {
restrict: 'A',
require: 'ngModel',
link: (scope: ng.IScope, element, attrs, ngModelCtrl: ng.INgModelController) =>
{
ngModelCtrl.$render = () =>
{
var date = ngModelCtrl.$modelValue ?
filter('date')(ngModelCtrl.$modelValue, "MM/dd/yyyy") :
ngModelCtrl.$viewValue;
jqueryElement.val(date);
};
}
};
}
This will format the value to be what you want. If you only want this to happen on blur then you add
ng-model-options="{ updateOn: 'blur' }"
<input type="text" my-date-display-input ng-model-options="{ updateOn: 'blur' }" ng-model="unformattedDate"/>
Some pieces might be missing but this should get the idea across.

when the validity of an input depends on the value of another input - or how to trigger validation in Angularjs?

As demonstrative in the following code sample, the input named amountOrPercet can be interpreted as an amount or as a percentage value, depending on the state of the mode radio button.
<input type="radio" name="mode" value="amt" ng-model="mode"/> Amount
<input type="radio" name="mode" value="pct"ng-model="mode"/> Percent
<input type="text" name="amountOrPercent" ng-model="amountOrPercent" check-percent/>
I have put together an attribute directive to invalidate amountOrPercent for values greater than 100, in case it must be interpreted as a percentage:
myApp.directive('checkPct', function () {
return {
require: 'ngModel',
link: function (scope, elem, attr, ngModel) {
ngModel.$parsers.unshift(function (value) {
var valid = scope.mode != 'pct' || value <= 100;
ngModel.$setValidity('checkPct', valid);
return valid ? value : undefined;
});
ngModel.$formatters.unshift(function (value) {
ngModel.$setValidity('checkPct', scope.mode != 'pct' || value <= 100);
return value;
});
}
};
});
When the value of mode it kept unchanged, The validation works as expected. However when the value of mode is changed, the value of amountOrPercent is not re-validated, unless user changes the value of amountOrPercentage.
I know that I can use a watch to do some action based on changes in mode, but my question is how I can trigger the validation on amountOrPercent, when mode is changed. - Thank you
You can add watcher for mode and revalidate value when it changes:
scope.$watch('mode', function(){
var valid = scope.mode != 'pct' || ngModel.$modelValue <= 100;
ngModel.$setValidity('checkPct', valid);
}

How to make a percent formatted input work on latest AngularJS?

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.
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>
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
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
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);
}
);
}

Resources