angularjs 1.2 - validation directive update on $watch - javascript

I have the following directive that I'm using for validation on a multi-select to allow for (dynamic) length validation of the number of selected items.
(function() {
'use strict';
angular
.module('myModule')
.directive('listLength', listLength);
listLength.$inject = ['$parse'];
function listLength($parse) {
var directive = {
require: 'ngModel',
restrict: 'A',
link: link
};
function link(scope, elem, attr, ngModel) {
var length = 0;
var exp = $parse(attr.listLength);
if (exp.constant) {
// Single value, no need to watch
length = exp(scope);
} else {
// We have an expression, need to watch for changes
scope.$watch(attr.listLength, function(newVal, oldVal) {
length = newVal;
});
}
//For DOM -> model validation
ngModel.$parsers.unshift(function(value) {
if (!angular.isUndefined(value) && value !== "") {
var valid = value.length === length;
ngModel.$setValidity('listLength', valid);
return valid ? value : undefined;
}
return value;
});
//For model -> DOM validation
ngModel.$formatters.unshift(function(value) {
if (!angular.isUndefined(value) && value !== "") {
var valid = value.length === length;
ngModel.$setValidity('listLength', valid);
}
return value;
});
}
return directive;
}
})();
In order to have it update properly, I need to trigger the validation to be run when the expression (attr.listLength) updates, but it's not obvious to me how to achieve this. I tried setting ngModel.$dirty to true but it still does not update.

Try change the watch parameter like that:
scope.$watch("listLength", function(newVal, oldVal) {
length = newVal;
});

Looks like the only way to do this in Angular 1.2 is to use ngModel.$setViewValue, which triggers all the validation functions. So if I do
scope.$watch(attr.listLength, function(newVal, oldVal) {
length = newVal;
ngModel.$setViewValue(ngModel.$viewValue); // Set view value to itself...
});
It then triggers the validation functions. In angularjs 1.3+ this can be done with ngModel.$validate()

Related

Angular directive: Allow just numbers

Here is a sample angular directive to prevent typing non-numeric keys (StackOverflow answer).
I want to write something like this fiddle to use the is-number directive in several inputs. Please consider that since I have various different directives in my inputs, I cannot use the same template as suggested in the update of mentioned answer above.
var $scope;
var app = angular.module('myapp', []);
app.controller('Ctrl', function($scope) {
$scope.myNnumber1 = 1;
$scope.myNnumber2 = 1;
});
app.directive('isNumber', function () {
return {
require: 'ngModel',
link: function (scope, element) {
scope.$watch(element.ngModel, function(newValue,oldValue) {
newValue = String(newValue);
newValue = newValue.replace('۰', '0').replace('۱', '1').replace('۲', '2').replace('۳', '3').replace('۴', '4').replace('۵', '5').replace('۶', '6').replace('۷', '7').replace('۸', '8').replace('۹', '9');
var arr = String(newValue).split("");
if (arr.length === 0) return;
if (arr.length === 1 && (arr[0] == '-' || arr[0] === '.' )) return;
if (arr.length === 2 && newValue === '-.') return;
if (isNaN(newValue)) {
element.ngModel = oldValue;
}
});
}
};
Update:
Please consider that I need to do some processes to convert non English numbers and so on. I created a new fiddle here based on the the Angular_10's answer. Now, every thing is fine except the cursor position while typing Persian numbers. When I type A Persian Number, it is replaced with English equivalent number, but the cursor suddenly jumps to the end.
OK ! Looking at your requirement I've took liberty and wrote more customised directive.
Here is the fiddle for the same
Problem
The example from which you referred and made changes to the given directive is causing the issue.
Your $scope variable names are wrong in HTML/JS ($scope.myNnumber1 = 1;
$scope.myNnumber2 = 1; in JS and in HTML it was ng-model="myNumber1")
You are accessing element ng-model and trying to modify it through directive which is bad practice and also the root cause for directive to not to work.As you are not changing the ng-model value but in turn modifying HTML element value which angular will not recognise.
More over using $watch in directive is not always preferable for performance sake.
Solution
app.directive('isNumber', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attr, ctrl) {
function inputValue(val) {
if (val) {
var numeric = val.replace(/[^- 0-9]/g, '');
if (numeric !== val) {
ctrl.$setViewValue(numeric );
ctrl.$render();
}
return numeric;
}
return undefined;
}
ctrl.$parsers.push(inputValue);
}
};
});
When controller communication is required from directive we can pass Controller as 4 param in the link function.From that Ctrl param we can modify/view things from controller scope.
Using some basic regex expression to find out what is the entered input and set it in the controller scope object view value.
ctrl.$setViewValue(numeric); //to set the value in the respective ngModdel
ctrl.$render(); //to display the changed value
More about $setViewValue
I finally used the below directive. This directive converts persian number and do not let no numbers to be typed in the text box. Special thanks to Angular_10. I awarded 50 bounties to him for his help.
app.directive('fixPersianAndNoNumberInput', function ($filter) {
return {
require: 'ngModel',
restrict: 'EA',
link: function (scope, element, attr, controller) {
function inputValue(val) {
if (val) {
let numeric = parseInt(String(val).replace('۰', '0').replace('۱', '1').replace('۲', '2').replace('۳', '3').replace('۴', '4').replace('۵', '5').replace('۶', '6').replace('۷', '7').replace('۸', '8').replace('۹', '9').replace(' ', '000').replace(/[^- 0-9]/g, ''));
if (numeric !== val) {
controller.$setViewValue(numeric);
controller.$render();
let value;
let updateOn, debounce;
if (controller.$options) {
if (controller.$options.getOption) {
updateOn = controller.$options.getOption('updateOn');
debounce = controller.$options.getOption('debounce');
} else {
updateOn = controller.$options.updateOn;
debounce = controller.$options.debounce;
}
}
if (updateOn === 'blur' || debounce) {
value = controller.$viewValue;
for (let i = controller.$parsers.length - 1; i >= 0; i--) {
value = controller.$parsers[i](value);
}
} else {
value = controller.$$rawModelValue;
}
for (let j = controller.$formatters.length - 1; j >= 0; j--) {
value = controller.$formatters[j](value);
}
controller.$viewValue = value;
controller.$render();
}
return numeric;
}
return undefined;
}
controller.$parsers.push(inputValue);
controller.$formatters.push((value) => {
if ([undefined, null, ''].indexOf(value) === -1) {
return $filter('currency')(value, '', 0);
}
return value;
});
}
};
});

Call AngularJS directive from controller on click

Quick question, so I'm using tg-dynamic-directive to loop through a json file and output following tree. (See image attached)
So the problem is, when the "tree" gets very long there are some serious performance problems because the browser needs to render a lot of items. (I'm talking about 1000 or longer). So what I'm trying to do is initially only load first 2 levels and the rest will be collapsed. When a user clicks expand arrow of each element I need to render its children. (If that makes sense). So basically run tg-dynamic-directive again.
When page starts rendering and function that returns template with the html is fired I have this to check if its first 2 levels:
$scope.getView = function (item) {
// Check if item is defined
if(typeof item != 'undefined') {
// Load course, label and module first!
if(item.type == 'course' || item.type == 'label' || item.type == 'module' || item.type == 'course_assessment' || item.type == 'module_assessment') {
// Return Template
return 'nestable_item.html';
} else {
// Otherwise return null
return null;
}
} else {
return null;
}
};
Then what I need to do is call that directive again when expand arrow is clicked.
This is the directive:
angular.module('tg.dynamicDirective', [])
.directive('tgDynamicDirective', ['$compile',
function($compile) {
'use strict';
function templateUrlProvider(getView, ngModelItem) {
if (getView) {
if (typeof getView === 'function') {
var templateUrl = getView(ngModelItem) || '';
if (templateUrl) {
return templateUrl;
}
} else if (typeof getView === 'string' && getView.length) {
return getView;
}
}
return '';
}
return {
restrict: 'E',
require: '^ngModel',
scope: true,
template: '<div ng-include="templateUrl"></div>',
link: function(scope, element, attrs, ngModel) {
scope.$watch(function() {
var ngModelItem = scope.$eval(attrs.ngModel);
var getView = scope.$eval(attrs.tgDynamicDirectiveView);
scope.ngModelItem = ngModelItem;
return templateUrlProvider(getView, ngModelItem);
}, function(newValue, oldValue) {
scope.templateUrl = newValue;
});
}
};
}
]);
My question is how can I fire tg-dynamic-directive again when expand arrow is clicked from the controller.
Use $rootScope.broadcast("XXXXXX"); and catch it
$rootScope.$on("XXXXXX", function() {
// function call
})
in directive and call the function you want.
or
use document.createElement("tg-dynamic-directive");

Using an AngularJS directive, how do I bind to keyup event on input field and revert model to previous value if it fails a regular expression check?

Doing a quick POC in AngularJS to only allow specific input into a text box.
The goal is to check the value every time the user types a new character, if it fails the regular expression check, we need to either reject the character or roll it back to the previous value.
The way I see it, here are my 2 options:
1. Bind to keypress event, check what the new value would be against a regex, and return false if it fails, preventing the character from being accepted into the text box
2. Bind to keyup event, check what the new value is against a regex, and if it fails, revert it to the previous value
How can I accomplish this from my directive?
var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
app.directive("currencyInput", function () {
return {
restrict: "A",
scope: {
},
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
$(element).bind('keypress', function (event) {
// TODO: Get what new value would be
var newValue = "...";
return currencyRegEx.test(newValue);
});
$(element).bind('keyup', function (event) {
var newValue = $(this).val();
if (!currencyRegEx.test(newValue)) {
// TODO: Revert to previous value
}
});
}
}
});
<input type="text" class="form-control" ng-model="item.paymentAmount" currency-input />
EDIT w/ SOLUTION
Here is the current solution we have in place in order to prevent non-digit input and rollback invalid currency value.
First, we created a new property "scope.prevValue" to hold the last valid value entered by the user. Then, on "keypress" we check to make sure the user typed a digit, comma, or period. Finally, on "keyup", we check the new value against the currency regex and rollback if needed.
var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
var digitRegex = /^[0-9]*$/;
app.directive("currencyInput", function () {
return {
restrict: "A",
scope: {},
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
scope.prevValue = '';
$(element).on('keypress', function(event) {
var validAlphaChars = ['.', ','];
var enteredCharacter = String.fromCharCode(event.charCode != null ? event.charCode : event.keyCode);
if (validAlphaChars.indexOf(enteredCharacter) < 0 && !digitRegex.test(enteredCharacter)) {
return false;
}
});
$(element).on('keyup', function (event) {
var newValue = $(element).val();
if (newValue.length > 0 && !currencyRegEx.test(newValue)) {
$(element).val(scope.prevValue);
return false;
} else {
scope.prevValue = $(element).val();
}
});
}
});
EDIT w/ SOLUTION #2 (using Steve_at_IDV's approach on accepted answer)
var currencyRegEx = /^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?|(\.\d{1,2}))\)$/;
app.directive("currencyInput", function () {
return {
restrict: "A",
scope: {},
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function (value) {
if (value.length > 0 && value != '.' && !currencyRegEx.test(value)) {
var prevValue = ngModelCtrl.$modelValue;
ngModelCtrl.$setViewValue(prevValue)
ngModelCtrl.$render();
return prevValue;
}
return value;
});
}
}
});
This would be a good time to use ngModelCtrl.$parsers instead of binding to keypresses manually. Try something like this in your link function:
ngModelCtrl.$parsers.push( function (value) {
// do some validation logic...it fails
if (validationFails) {
var prevValue = ctrl.$modelValue;
ctrl.$setViewValue(prevValue); // set view
ctrl.$render(); // render view
return prevValue; // set model
}
// otherwise we're good!
return value;
} );
Here is a Plunker which demonstrates. The input field will reject a lowercase z from being entered.
See the $parsers section of https://docs.angularjs.org/api/ng/type/ngModel.NgModelController for more info.
Firstly, I think you shouldn't modify the input of the user. I personnaly find it bad on a UX point of view. It's better to indicate that the input is in an error state by bordering in red for example.
Secondly, there is a directive that can fit your need, ng-pattern.
<input type="text"
class="form-control"
ng-model="item.paymentAmount"
ng-pattern="currencyRegEx" />
Some similar questions :
Angularjs dynamic ng-pattern validation
How to only allow the numbers 0-5 in <input> fields with AngularJS?

decimal validation directive in angularjs

I wanted to create directive in angular that would display error message if entered value is not in valid format.
What I finally came with is:
http://plnkr.co/edit/l2CWu8u6sMtSj3l0kdvd?p=preview
app.directive('kbDecimalValidation', function ($parse, $rootScope, $compile) {
return {
restrict: 'E',
scope: {
inputFieldRef: '=?',
kbModel: '=ngModel',
kbRequired: '#required',
inputName: '#'
},
template: '<span ng-form="kbDecimalValidationForm">' +
'<input ng-model="kbModel" ng-required="kbRequired" ' +
'size="6"' +
'ng-pattern="/^[0-9]+(\\.[0-9][0-9]?)?$/" ' +
'/>' +
'<div ng-show="!kbDecimalValidationForm[inputName].$valid && kbDecimalValidationForm[inputName].$error.required"' +
'style="color: red; font-weight: bold">Field is required</div>' +
'<div ng-show="!kbDecimalValidationForm[inputName].$valid && kbDecimalValidationForm[inputName].$error.pattern"' +
'style="color: red; font-weight: bold">Bad format format,<br />allowed: "0.00"' +
'</div>' +
'</span>',
replace: true,
priority: 50,
controller: function($scope){
$scope.$watch(
'kbDecimalValidationForm[inputName]',
function (value) {
$scope.inputFieldRef = value;
});
},
compile: function (tElement, tAttrs, transclude) {
if($.tempKbDecimalValidationGUID == undefined){
$.tempKbDecimalValidationGUID = 0;
}
var guidInputName = 'XXX' + ++$.tempKbDecimalValidationGUID + 'XXX';
$(tElement).find('input').attr('name', guidInputName); //it is here to force angular to assign value to: $scope.kbDecimalValidationForm[guidInputName]
//there is no expression in name, so angular won't add it to $$watchers
return {
pre: function preLink($scope, iElement, iAttrs, controller) {
//$(iElement).find('input').attr('name', iAttrs.inputName); //it doesn't work if there is expression in inputName,
// expression will be evaluated later (after linkFunction)
// and the name assigned here will be updated (re-parsed by angular watch)
},
post: function postLink($scope, iElement, iAttrs, controller) {
$scope.kbDecimalValidationForm[iAttrs.inputName] = $scope.kbDecimalValidationForm[guidInputName]; //rewrite value to make it available by parsed name
$(iElement).find('input').attr('name', iAttrs.inputName); //assign parsed name - GUID didn't contain expression, so it is not in $$watchers,
// so it won't be replaced by angular
}
}
}
};
});
but I'm sure it is not propper way to do it. I expirience a lot of problems with it. Can somebody tell me what is the propper way to achieve it?
PS: The problem I'm facing right now with the above directive is: when I use it in ng-repeat, and reorder repeated source the directive does not work correctly. I suspect the problem is with my "hacking coding" (the tempKbDecimalValidationGUID, and $scope.kbDecimalValidationForm variables)
For Angular 1.2.x, you will have to use the ngModel.$parsers and $formatters pipelines for validation. Angular 1.3 has the dedicated $validators and even $asyncValidators pipelines. So the outline of a validation solution for 1.2.x would be:
.directive("kbDecimalValidation", function() {
function parseDecimal(value) {
// implement the conversion from a string to number, e.g. (simpistic):
var val = parseFloat(value);
// return a number (for success), null (for empty input), or a string (describing the error on error)
}
function formatDecimal(value) {
// format a number to a string that will be displayed; the inverse of parseDecimal()
// throw error if something goes wrong
}
return {
restrict: "A",
require: "ngModel",
link: function(scope, elem, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
var val = parseDecimal(value);
if( typeof val === "string" ) {
// an error occured
ngModel.$setValidity("kbDecimal", false);
// return undefined!
}
else {
ngModel.$setValidity("kbDecimal", true);
return val;
}
});
ngModel.$formaters.push(function(value) {
if( value == null || typeof value === "number" ) {
ngModel.$setValidity("kbDecimal", true);
try {
return formatDecimal(value);
}
catch(e) {
ngModel.$setValidity("kbDecimal", false);
return "";
}
}
else {
ngModel.$setValidity("kbDecimal", false);
return "";
}
});
}
};
})
Many details will need work, but hopefully you get the idea. The parseDecimal()/formatDecimal() functions could even go to a dedicated Angular service, if they become too complex, or need to be reusable.
About the display of error messages
A quick and dirty way is to use DOM manipulation through the elem argument of link(). E.g.:
link: function(scope, elem, attrs, ngModel) {
...
scope.$watch(
function() { return ngModel.$error.kbDecimal; },
function(newval) {
var container = elem.parent();
// append or remove the message as necessary
...
}
);
}
Another way, less quick but more componentized is to make 2 more directives. One will be placed on the <span ng-form> element (the container), another will display the messages. The HTML would be like:
<span ng-form="..." validation-container>
<input ... kb-decimal-validation />
<validation-messages></validation-messages>
</span>
Both kbDecimalValidation and validationMessages will require the validationContainer; the controller of the validationContainer will have a method, called by the kbDecimalValidation, to get notified about the $error object. It will also expose a copy of the $error object. The validationMessages will $watch that object and display or hide the appropriate messages.

ngRequired and custom directive in angular

I can't seem to get the smart float directive found in the angular docs example working when used together with ngRequired.
You can confirm this by going to the angular docs page
https://docs.angularjs.org/guide/forms
Edit the plunker on the smart float example, and add ng-required="false".
Make sure to check that the validity of the entire form, not the individual control.
The form itself is always marked as invalid if there is no input on the control.
I am using version 1.3.0-rc3
In order to allow empty values for smartFloat directive you should use $validators for validity check of the float value in conjunction with $parsers. Also note that returning undefined (or not returning any value) from any of the parsers will mark the whole form is invalid.
Allow empty values
var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/;
app.directive('smartFloat', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// Parse
ctrl.$parsers.unshift(function (viewValue) {
if (!viewValue) {
return ''; // <-- Don't return undefined, but empty string instead
} else if (FLOAT_REGEXP.test(viewValue)) {
return parseFloat(viewValue.replace(',', '.'));
}
});
// Validate
ctrl.$validators.float = function (viewValue) {
if (viewValue === '') {
ctrl.$setValidity('float', true); // <-- Handle empty value as valid
return true;
} else if (FLOAT_REGEXP.test(viewValue)) {
ctrl.$setValidity('float', true);
return true;
} else {
ctrl.$setValidity('float', false);
return false;
}
};
}
};
});
Live example see here.

Categories

Resources