When writing a directive to verify that a "confirm password" input field matches a "new password" input field you can either pass in the model that stores the new password or the entire input. However both have a critical shortcoming. See below:
Passing in Model Approach
<form name="vm.passwordForm">
<input type="password" ng-model="vm.password.new" name="newPassword" required ng-minlength="6"/>
<input type="password" ng-model="vm.password.confirm" name="confirmPassword" required password-confirm="vm.password.new"/>
</form>
angular.module('passwordConfirm, [])
.directive('passwordConfirm', function () {
function link($scope, $element, $attrs, $ctrl) {
$ctrl.$validators.confirm = function confirm(modelValue, viewValue) {
var value = modelValue || viewValue;
if (typeof value === 'undefined' || value.length === 0) {
return true;
}
// $scope.passwordConfirm won't be set if that input is invalid
return value == $scope.passwordConfirm;
};
$scope.$watch("passwordConfirm", function () {
$ctrl.$validate();
});
}
return {
require: 'ngModel',
scope: {
passwordConfirm: "="
},
link: link
};
});
The problem with the above approach occurs when the new password field fails its own validation. Say, for example, that the user enters in "abc" as a new password. This is less than 6 characters so it would fail the ng-minlength validation which would, in turn, prevent the vm.password.new model from being set to "abc". If the user then entered in "abc" into the confirm password field, he would get an error telling him that the passwords don't match even though they do.
Pass in Input Approach
<form name="vm.passwordForm">
<input type="password" ng-model="vm.password.new" name="newPassword" required ng-minlength="6"/>
<input type="password" ng-model="vm.password.confirm" name="confirmPassword" required password-confirm="vm.passwordForm.newPassword"/>
</form>
angular.module('passwordConfirm, [])
.directive('passwordConfirm', function () {
function link($scope, $element, $attrs, $ctrl) {
$ctrl.$validators.confirm = function confirm(modelValue, viewValue) {
var value = modelValue || viewValue;
if (typeof value === 'undefined' || value.length === 0) {
return true;
}
return value == $scope.passwordConfirm.$viewValue;
};
// This doesn't work
$scope.$watch("passwordConfirm", function () {
$ctrl.$validate();
});
}
return {
require: 'ngModel',
scope: {
passwordConfirm: "="
},
link: link
};
});
This approach exposes the $viewValue property of the input field which doesn't have to go through validation. The confirm password field doesn't throw an error if the original password was invalid. It just cares about whether the two fields match. The problem, though, is that the $scope.$watch command doesn't work. I'm not sure why but it only fires once on page load and doesn't fire when the field is updated.
Any ideas which approach is better and how to get around each shortcoming?
UPDATE: I've discovered that setting ng-model-options="{allowInvalid: true}" on the new password field will update the model even when the password fails its validation, allowing the confirm password field to match it. I'm not sure this is the best approach, however, and I'm still curious as to why the $scope.$watch on the input field doesn't work.
Related
I will start out stating that I have searched google and SO and I have not found an answer for this specific situation. Yes, there are other posts that sound the same but are more based on a "MoreThan / LessThan" mentality. This does not following that mentality at all so please do not mark this as a duplicate referring to them.
Check out the Plunker Example
I am attempting to make sure the user does not enter an address that already exists else where on the page. To do this I need to validate all the address fields since different locations may have the same street address. I need the validator to set all the related fields to valid if any are invalid once the address has been fixed to not be a duplicate. Currently it only sets the last field modified to valid and leaves the rest as invalid.
Plunker example demonstrates what is happening. I have tried many different approaches such as iterating through all fields and setting them to prestine and untouched and then setting them to dirty and touched to trigger validations again but I am having no luck getting this working.
Validator
angular.directive('ruleFunc', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, $element, $attrs, $ngModel) {
var validatorName = $attrs.ruleName;
var validatorFunc = $attrs.ruleFunc;
if (!angular.isDefined(validatorName)) {
throw Error("rule-name attribute must be defined.");
}
if (!angular.isDefined(validatorFunc)) {
throw Error("rule-func attribute must be defined.");
}
// in real code I passing a function call with the model as the param
// this example demonstrated the issue I am having though
var expressionHandler = $parse(validatorFunc);
// had to use viewChangeListener because changes to the model
// were not showing up correctly in the actual implementation
$ngModel.$viewChangeListeners.push(function() {
var valid = expressionHandler($scope);
$ngModel.$setValidity(validatorName, valid);
});
});
Form
<form name="AddressForm" novalidate>
<h1>Address Form</h1>
<div style="margin:20px">
<input id="Street" type="text" name="Street" placeholder="Street" data-ng-model="ctrl.address.street" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.Street.$error.profileHasContact}}
<br />
<input id="City" type="text" name="City" placeholder="City" data-ng-model="ctrl.address.city" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.City.$error.profileHasContact}}
<br />
<input id="State" type="text" name="State" placeholder="State" data-ng-model="ctrl.address.state" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.State.$error.profileHasContact}}
<br />
<input id="Zip" type="text" name="Zip" placeholder="Zip" data-ng-model="ctrl.address.zip" rule-func="ctrl.checkVal()" rule-name="profileHasContact"> {{!AddressForm.Zip.$error.profileHasContact}}
<br />
<div ng-if="(AddressForm.Street.$error.profileHasContact
|| AddressForm.City.$error.profileHasContact
|| AddressForm.State.$error.profileHasContact
|| AddressForm.Zip.$error.profileHasContact)">Address already exists in Main Contacts</div>
<button type="submit">Submit</button>
</div>
I did find a post that was close enough that I could hack together a solution.
Form validation - Required one of many in a group
Here is the updated plunker
Updated Validator
directive('ruleFunc', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, $element, $attrs, $ngModel) {
var validatorName = $attrs.ruleName;
var validatorFunc = $attrs.ruleFunc;
var groupName = $attrs.ruleGroup;
if (!angular.isDefined(validatorName)) {
throw Error("rule-name attribute must be defined.");
}
if (!angular.isDefined(validatorFunc)) {
throw Error("rule-func attribute must be defined.");
}
if(angular.isDefined(groupName)){
// setup place to store groups if needed
if (!$scope.__ruleValidationGroups) {
$scope.__ruleValidationGroups = {};
}
var groups = $scope.__ruleValidationGroups;
// setip group if needed
if(!groups[groupName]){
groups[groupName] = {};
}
var group = groups[groupName];
// assign model to group
group[$attrs.ngModel] = {
model: $ngModel
}
}
function updateValidity(valid){
if(angular.isDefined(groupName)){
// set all models in group to same validity
for(var prop in group){
if(group.hasOwnProperty(prop)){
group[prop].model.$setValidity(validatorName, valid);
}
}
}
else
{
// set this model validity if not in group
$ngModel.$setValidity(validatorName, valid);
}
}
var expressionHandler = $parse(validatorFunc);
$ngModel.$viewChangeListeners.push(function() {
var valid = expressionHandler($scope);
updateValidity(valid);
});
}
};
}]);
I have an input box asking for a number and what I'm trying to do is to make sure the user does not type any letter or any digit that is not valid.
This is my code so far:
<form name="userForm" novalidate>
Calories Needed :
<input type="number" min="0" ng-model="user.calories">
</form>
I've been looking for a way using AngularJS but have had no luck. I've come across posts where they suggested creating a directive but I was looking for another technique. I'm still a beginner so I don't want to just use code w/o understanding what exactly is going on.
You're in luck I have a directive I use myself everywhere for this instance.
It's as easy as slapping <input valid-number /> on an input. Voila!
angular.module('yourApp')
.directive('validNumber', function () {
return {
require: '?ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
// make sure we're connected to a model
if (!ngModelCtrl) {
return;
}
ngModelCtrl.$parsers.push(function (val) {
// this is a test for whether it's undefined (from textbox)
// or null when using type="number"
if (val === undefined || val === null) {
val = '';
}
// here we try and clean it to make sure it's only numbers
var clean = val.toString().replace(/[^0-9]+/g, '');
// if a letter/etc got in there, set the model to the "cleaned" number value
if (val !== clean) {
ngModelCtrl.$setViewValue(clean);
ngModelCtrl.$render();
}
return clean;
});
// this is to test for "32" = SPACEBAR
// and "101" = e (Chrome for some reason let's E go through in type="number"
element.bind('keypress', function (e) {
var code = e.keyCode || e.which;
// Remove code === 101 part if you want 'e' to go through
if (code === 101 || code === 32) {
e.preventDefault();
}
});
}
};
});
You'll want to read up on Angular's form validation- tutorial
It's as simple as adding "required" to your input, and 'novalidate' to the form element:
<form name="testForm" novalidate>
<input type="number" required name="numberinput" class="form-control" ng-model="someNumber">
</form>
plunker
I have a directive that queries google maps api with the form field value (event_postcode). It needs to be a required field. Yet when the field is populated, and maps api (via manageEventsSrv.getPostcodeLocation()) returns a valid address, the forms submit button remains disabled, i.e. form is invalid.
My directive:
app.directive('geoLocatePostcode', ['manageEventsSrv', function(manageEventsSrv) {
return {
restrict: 'A',
require: 'ngModel',
link : function(scope, elem, attrs, ctrl){
var valid = false;
ctrl.$parsers.unshift(function(value) {
if(value.length>=3){
manageEventsSrv.getPostcodeLocation(value+',uk').then(function(result){
valid = true;
scope.point = new Parse.GeoPoint({latitude:result.lat,longitude:result.lng}); // parse.com geopoint
ctrl.$setValidity('locationNotFound', true);
}, function(result){
valid = false;
ctrl.$setValidity('locationNotFound', false);
});
} else {
ctrl.$setValidity('locationNotFound', true);
valid = false;
}
return valid ? value : undefined;
});
}
}
}]);
Form field:
<input type="text" placeholder="First 4 Characters Only" ng-model="event_postcode" maxlength="4" name="event_postcode" required geo-locate-postcode>
<span data-ng-show="createEventForm.event_postcode.$error.locationNotFound">Location not found</span>
<button type="submit" class="button" ng-disabled="createEventForm.$invalid">Create</button>
I assume i am not implementing "required" properly when using a custom directive.
Think i might have got to the bottom of this.... i needed to change
return valid ? value : undefined; to return valid ? value : false;
I was wondering if you would be able to help me out. I am trying to get validation to work for a password field. The password must comply to certain criteria to be valid and once each criteria is met it is checked off on the fly.
To do this I created 3 hidden fields each of which copies the value from the password field and validates it against one of the 3 criteria it requires. Ie one of the criteria requires the user to input a number, once they do this the text stating a number is required turns green. Same thing happens for the other two hidden fields. these hidden fields can be seen below:
<!--7-25 characters -->
<input required ng-model="password" ng-pattern="/.{7,25}/" name="length" type="text" />
<!--Contain at least 1 letter -->
<input required ng-model="password" ng-pattern="/[a-zA-Z]+/" name="letter" type="text" />
<!--Contain at least 1 number -->
<input required ng-model="password" ng-pattern="/[0-9]+/" name="number" type="text" />
For the actual password field I then wanted to create a directory which would only set that field as valid if the three hidden fields are also valid.
My directory looks like this (my form is named registration and to begin with im only testing it against the length hidden field):
app.directive('validPwd', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elem, attrs, ngModel) {
if(!ngModel) return;
scope.$watch(attrs.ngModel, function() {
validate();
});
var validate = function() {
// set validity
ngModel.$setValidity('validPwd', registration.length.$valid);
};
}
}
});
My issue with this is that my directive is giving me the error that it cannot read property length of undefined. Am I not referring to it correctly or what? I cant seem to figure it out.
Any help would be greatly appreciated.
That's a very convoluted way to validate your field. You shouldn't pollute your view with 3 hidden fields just to validate a 4th one. Why don't you simply check each condition inside a custom validator, as explained in the documentation?
app.directive('validPwd', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
var containsLetter = containsLetter(viewValue);
var containsDigit = containsDigit(viewValue);
var hasCorrectLength = hasCorrectLength(viewValue);
ctrl.$setValidity('containsLetter', containsLetter);
ctrl.$setValidity('containsDigit', containsDigit);
ctrl.$setValidity('hasCorrectLength', hasCorrectLength);
if (containsLetter && containsDigit && hasCorrectLength) {
return viewValue;
}
else {
return undefined;
}
});
}
};
});
So, the current code looks like this:
<input ng-pattern="validRegEx()" required>
$scope.validRegEx = function() {
// blah blah return regex
};
When I change an input on the form, I change the regex. The problem is data that was valid before (which should NOT be valid with the new RegEx) is still valid. How can I force the form to apply a regex to this field again, as opposed to just waiting for the user to blur the field again?
You could do it with a directive by watching for when the regex changes (or the conditions for it) and when it does you can re-run the validation by re-setting the value of the element.
something like this
angular.module('youappname', [])
.directive('reApply', function () {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, elm, attrs, ctrl) {
// you should change 'regex' in the following line to match the variable that when altered causes the regex to change..
scope.$watch('regex', function (value) {
if (ctrl.$viewValue !== ''){
ctrl.$setViewValue( ctrl.$viewValue );
}
});
}
};
});
and adding re-apply to your element
<input type="text" ng-pattern="validate()" re-apply="" name="userName" ng-model="user.name" required> <span class="error" ng-show="myForm.userName.$error.required">
demo at http://jsfiddle.net/Dztev/5/
This could also be done directly in the controller. This will watch the input stuff.name and if it changes, it will clear the scope.stuff.results field.
$scope.$watch("stuff.name", function(newValue, oldValue){
if(newValue != oldValue)
$scope.stuff.results = "";
});