Angular.js validation. Programatically set form fields properties - angularjs

I'm trying to set up a form validation systems with angular as follows.
An error class is assigned when the field is both $invalid && $dirty.
This works great for individual fields. Here is an example field.
<div class="control-group" ng-class="getErrorClasses(schoolSignup.first_name)">
<label class="control-label" for="first_name">First Name</label>
<div class="controls">
<input type="text" class="input-xlarge" id="first_name" name="first_name" required ng-maxlength="30" ng-model="school.first_name">
<span class="help-inline" ng-show="showError(schoolSignup.first_name, 'required')">This field is required</span>
<span class="help-inline" ng-show="showError(schoolSignup.first_name, 'maxlength')">Maximum of 30 character allowed</span>
</div>
</div>
schoolSignup is the name of the form.
ng-class="getErrorClasses(schoolSignup.first_name)"> is defined in the controller as
$scope.getErrorClasses = function(ngModelContoller) {
return {
error: ngModelContoller.$invalid && ngModelContoller.$dirty
};
};
And on the error help items ng-show="showError(schoolSignup.first_name, 'required')" is defined as:
$scope.showError = function(ngModelController, error) {
return ngModelController.$dirty && ngModelController.$error[error];
};
This all works fine.
However, what I want to do is make it so if the user clicks the submit button and all the individual fields that are required are invalid then show the required error message next to the appropriate fields.
This will require setting all the individual fields to $dirty and the $error['required'] value to true based on this system.
I have set up a directive on the submit button called can-save as follows which has access to the form.
<button class="btn btn-primary btn-large" type="submit" can-save="schoolSignup">Register</button>
-
.directive('canSave', function () {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
var valid = false;
element.click(function(){
if(!valid)
{
// show errors
return false;
}
return true;
});
scope.$watch(function() {
return form.$dirty && form.$valid;
}, function(value) {
valid = !!value;
});
}
});
This all works except for making the errors show as desired.
Any ideas on how to achieve this would be appreciated.

For anyone interested, I've amended my directive to loop through the errors and set each to $dirty and then run $rootScope.$digest():
.directive('canSave', function ($rootScope) {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
element.click(function () {
if(form.$dirty && form.$valid) {
return true;
} else {
// show errors
angular.forEach(form.$error, function (value, key) {
var type = form.$error[key];
angular.forEach(type, function (item) {
item.$dirty = true;
item.$pristine = false;
});
});
$rootScope.$digest();
return false;
}
});
}
});

Related

AngularJS - Validate when ng-model value not in Select options

I have a situation where the ng-model value may sometimes not be present in the ng-options list of a Select dropdown (ID of a user that is no longer with the company saved in a record, for example). AngularJS's behavior is that it adds an empty option to the dropdown, but does not mark it as Invalid or Error, which makes it difficult to validate. I found a solution here that uses a directive to handle this situation upon loading the page. Because I get the user Id and the list using an API call, after researching I added a $watch to make sure the data was loaded before checking, but I can't seem to get it to work. Can someone help me figure out what's wrong with my code? It seems the code inside the $validators function does not get called properly or all the time. What I need is to be able to show a message when the user ID is not in the select list, but when the user selects a name from the dropdown, the message should go away.
Here's the relevant part of my code:
<div ng-app="RfiApp" ng-controller="RfiControllerRef" class="col-sm-12">
<form class="pure-form pure-form-aligned" name="frm" method="post"
autocomplete="off">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-md-2" for="bsa_analyst_user_id">BSA Analyst</label>
<div class="col-md-10">
<select class="form-control" id="bsa_analyst_user_id" name="bsa_analyst_user_id" ng-model="rfi.bsa_analyst_user_id" ng-options="s.value as s.name for s in BsaAnalysts | orderBy: 'name'" valid-values="BsaAnalysts" required>
<option value="">--Select--</option>
</select>{{frm.bsa_analyst_user_id.$error}} - {{frm.bsa_analyst_user_id.$invalid}} - {{rfi.bsa_analyst_user_id}}
<div class="text-danger"
ng-show="frm.bsa_analyst_user_id.$invalid"
ng-messages="frm.bsa_analyst_user_id.$error">
<div ng-message="required">BSA Analyst is required</div>
<div ng-message="validValues">BSA Analyst in database not valid or not in BSA Unit selected</div>
</div>
</div>
</div>
</div>
</form>
</div>
.JS Code:
RfiApp.directive('validValues', function () {
return {
scope: {
validValues: '=',
model: '=ngModel'
},
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModel) {
scope.$watch('[model,validValues]', function (newVals) {
if (newVals[0] && newVals[1]) {
var values = angular.isArray(scope.validValues)
? scope.validValues
: Object.keys(scope.validValues);
ngModel.$$runValidators(newVals[0], values, function () { });
ngModel.$validators.validValues = function (modelValue) {
var result = values.filter(function (obj) {
return obj.value == modelValue;
});
var ret = result.length > 0;
return ret;
}
}
});
}
}
});
$scope.GetDropdownValues = function () {
var a = 1;
DropdownOptions.get(
function (response) {
//... other code ommitted
$scope.UserAnalysts = response.UserAnalystsList;
},
function () {
showMessage('error', "Error");
}
);
};
$scope.GetExistingRfi = function () {
NewRfiResource.get({ rfiId: $scope.rfiId },
function (response) {
$scope.rfi = response.rfi;
$scope.ButtonText = "Save";
},
function (response) {
showMessage('error', response.data.Message);
$scope.GetNewRfi();
}
);
};
$scope.GetDropdownValues();
if ($scope.rfiId != "0") {
$scope.GetExistingRfi();
}
else {
$scope.GetNewRfi();
}
Thanks!

Passing Information to Directive to Match Passwords

I'm trying to add an errors to my floating placeholder labels when certain conditions are met in my controller
However, I'm not sure the best way to go about this and my current implementing doesn't seem to be detecting the attribute change in the directive (custom-error stays set to "test").
Here's what I've got right now:
HTML:
<input type="password" float-placeholder
custom-error="test" placeholder="Confirm password"
required name="passwordSecond" id="passwordSecond"
ng-model="vs.PasswordSecond" />
Directive:
angular.module('myApp').directive('floatPlaceholder', function ($window) {
return {
restrict: 'A',
scope: {
customError: '#'
},
link: function (scope, element, attrs) {
element.after("<label class='floating-placeholder'>" + attrs.placeholder + "</label>");
var label = angular.element(ele.parent()[0].getElementsByClassName('floating-placeholder'));
element.on('blur', function() {
if (ele.val().length > 0) {
if (scope.customError) {
label.text(attrs.placeholder + ' - ' + scope.customError);
}
}
}
}
};
});
Controller:
angular.module('myApp').controller('SignupController', function factory() {
_this.confirmPassword = () => {
if(_this.PasswordFirst !== _this.PasswordSecond){
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
}
}
});
I'm using Angular 1.6
Validator Directive which Matches Passwords
To have a form match password inputs, create a custom directive that hooks into the ngModelController API ($validators):
app.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
For more information, see AngularJS Developer Guide - Forms - Modifying Built-in Validators
The DEMO
angular.module("app",[])
.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<form name="form1">
<input type="password" name="password1" required
placeholder="Enter password"
ng-model="vm.password1" />
<br>
<input type="password" name="password2" required
placeholder="Confirm password"
ng-model="vm.password2"
match-with="vm.password1"
ng-model-options="{updateOn: 'blur'}" />
<br>
<p ng-show="form1.password2.$error.match">
Passwords don't match
</p>
<input type="submit" value="submit" />
</form>
</body>
Had a look at your code. Have you defined the scope variables in the SignUpController
_this.PasswordFirst and _this.PasswordSecond
Also this line in your controller
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
good suggestion would be to implement this in the directive as attributes can be accessed correctly in the directive
(I'm basing this on you saying 'custom-error stays set to "test"')
custom-error is looking for a variable of "test", not a string value of "test". Have you tried setting a variable test in your controller and updating that?

Async validation with AngularJS

There are a couple of things going wrong. First, how do I catch the error in the template, because the has-error class doesn't get applied, even though the response is 404 and the proper code is executed.
Second, this only works the first time around. If I leave the field and then enter it again, each time I press a key I get a TypeError: validator is not a function exception (as you can see in the code, I execute this only on blur). Where am I going wrong?
The service call
this.CheckIfUnique = function(code) {
var deferred = $q.defer();
$http.get("/Api/Codes/Unique/" + code).then(function() {
deferred.resolve();
}, function() {
deferred.reject();
});
return deferred.promise;
};
The directive
var uniqueCode = [ "CodeService", function(codeService) {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ctrl) {
element.bind("blur", function(e) {
if (!ctrl || !element.val()) return;
var currentValue = element.val();
ctrl.$asyncValidators.uniqueCode = codeService.CheckIfUnique (currentValue);
});
}
};
}];
codeModule.directive("uniqueCode",uniqueCode);
The HTML
<div class="form-group" ng-class="{'has-error' : ( codeForm.submitted && codeForm.code.$invalid ) || ( codeForm.code.$touched && codeForm.code.$invalid ) }">
<label class="col-md-4 control-label" for="code">Code</label>
<div class="col-md-8">
<input class="form-control" id="code" name="code" ng-model="newCode.code" ng-required="true" type="text" unique-code />
<span class="help-block" ng-show="( codeForm.submitted && codeForm.code.$error.required ) || ( codeForm.code.$touched && codeForm.code.$error.required)">Please enter a code</span>
<span class="help-block" ng-show="codeForm.code.$pending.code">Checking if the code is available</span>
<span class="help-block" ng-show="( codeForm.submitted && codeForm.code.$error.uniqueCode ) || ( codeForm.code.$touched && codeForm.code.$error.uniqueCode)">This code already exist</span>
</div>
</div>
The MVC controller
public async Task<ActionResult> Unique(string code)
{
if (string.IsNullOrWhiteSpace(code))
{
return new HttpStatusCodeResult(HttpStatusCode.NotFound);
}
return _db.Codes.Any(x => x.Code = code)
? new HttpStatusCodeResult(HttpStatusCode.NotFound)
: new HttpStatusCodeResult(HttpStatusCode.Accepted);
}
EDIT:
Just to clarify, the API gets called only when I leave the field, the exception gets thrown on key down (and only after the first time I leave the field)
EDIT 2:
In case someone misses the comment, dfsq's answer works and if you add ng-model-options="{ updateOn: 'blur' }" to the input it'll validate on blur only.
You didn't provide proper validator function. It should use anonymous function that returns promise object (from your service). You also don't need blur event:
var uniqueCode = ["CodeService", function(codeService) {
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attrs, ctrl) {
ctrl.$asyncValidators.uniqueCode = function(value) {
return codeService.CheckIfUnique(value);
};
}
};
}];
codeModule.directive("uniqueCode", uniqueCode);
In addition, you should clean up service method to not use redundant deferred object:
this.CheckIfUnique = function(code) {
return $http.get("/Api/Codes/Unique/" + code);
};
About the other part of your question, the message is not showing because:
For the "Checking the code" message you have codeForm.code.$pending.code and it should be codeForm.code.$pending.uniqueCode
Here is the plunker working:
http://plnkr.co/edit/FH8GBhOipgvvUzrFYlwx?p=preview

Use ngMessages with Angular 1.2

Does anyone know if there is a fork of Angular 1.2 that supports ngMessages?
I'd love to use this but I have a requirement for IE8.
Thanks in advance for your help.
Here is my directive I use:
/**
* Ui-messages is similar implementation of ng-messages from angular 1.3
*
* #author Umed Khudoiberdiev <info#zar.tj>
*/
angular.module('uiMessages', []).directive('uiMessages', function () {
return {
restrict: 'EA',
link: function (scope, element, attrs) {
// hide all message elements
var messageElements = element[0].querySelectorAll('[ui-message]');
angular.forEach(messageElements, function(message) {
message.style.display = 'none';
});
// watch when messages object change - change display state of the elements
scope.$watchCollection(attrs.uiMessages, function(messages) {
var oneElementAlreadyShowed = false;
angular.forEach(messageElements, function(message) {
var uiMessage = angular.element(message).attr('ui-message');
if (!oneElementAlreadyShowed && messages[uiMessage] && messages[uiMessage] === true) {
message.style.display = 'block';
oneElementAlreadyShowed = true;
} else {
message.style.display = 'none';
}
});
});
}
};
});
I've used ui-messages instead of ng-messages to avoid conflicts.
<div ui-messages="form.name.$error">
<div ui-message="minlength">too short</div>
<div ui-message="required">this is required</div>
<div ui-message="pattern">pattern dismatch</div>
</div>
I don't know for sure if a fork exists but it would be easy enough to roll your own ng-message (or something that serves the same purpose). I think the following would do it:
Controller
app.controller("Test", function ($scope) {
$scope.messages = {
"key1": "Message1",
"key2": "Message2",
"key3": "Message3"};
$scope.getMessage = function (keyVariable) {
return $scope.messages[keyVariable.toLowerCase()];
};
$scope.keyVariable = 'key1';
});
HTML (example)
ENTER A KEY: <input type="text" ng-model="keyVariable" />
<h1 ng-bind="getMessage(keyVariable)" ng-show="getMessage(keyVariable) != ''"></h1>
See It Working (Plunker)
I've updated pleerock's answer to handle element directives having for and when attributes like ngMessages and ngMessage. You can find the same in this github repo
angular.module('uiMessages', []).directive('uiMessages', function() {
return {
restrict: 'EA',
link: function(scope, element, attrs) {
// hide all message elements
var messageElements = element.find('ui-message,[ui-message]').css('display', 'none');
// watch when messages object change - change display state of the elements
scope.$watchCollection(attrs.uiMessages || attrs['for'], function(messages) {
var oneElementAlreadyShowed = false;
angular.forEach(messageElements, function(messageElement) {
messageElement = angular.element(messageElement);
var message = messageElement.attr('ui-message') || messageElement.attr('when');
if (!oneElementAlreadyShowed && messages[message] && messages[message] === true) {
messageElement.css('display', 'block');
oneElementAlreadyShowed = true;
} else {
messageElement.css('display', 'none');
}
});
});
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js"></script>
<form name="userForm" ng-app="uiMessages" novalidate>
<input type="text" name="firstname" ng-model="user.firstname" required />
<ui-messages for="userForm.firstname.$error" ng-show="userForm.firstname.$dirty">
<ui-message when="required">This field is mandatory</ui-message>
</ui-messages>
<br />
<input type="text" name="lastname" ng-model="user.lastname" required />
<div ui-messages="userForm.lastname.$error" ng-show="userForm.lastname.$dirty">
<div ui-message="required">This field is mandatory</div>
</div>
</form>

Call async service in AngularJS custom validation directive

I have a directive for custom validation (verify a username doesn't already exist). The validation uses the $http service to ask the server if the username exists, so the return is a promise object. This is working fantastic for validation. The form is invalid and contains the myform.$error.usernameVerify when the username is already taken. However, user.username is always undefined, so it's breaking my ng-model directive. I think this is probably because the function in .success is creating it's own scope and the return value isn't used on the controllers $scope. How do I fix this so the ng-model binding still works?
commonModule.directive("usernameVerify", [
'userSvc', function(userSvc) {
return {
require: 'ngModel',
scope: false,
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(checkForAvailability);
ctrl.$formatters.unshift(checkForAvailability);
function checkForAvailability(value) {
if (value.length < 5) {
return value;
}
// the userSvc.userExists function is just a call to a rest api using $http
userSvc.userExists(value)
.success(function(alreadyUsed) {
var valid = alreadyUsed === 'false';
if (valid) {
ctrl.$setValidity('usernameVerify', true);
return value;
}
ctrl.$setValidity('usernameVerify', false);
return undefined;
});
}
}
}
}
]);
Here is my template:
<div class="form-group" ng-class="{'has-error': accountForm.username.$dirty && accountForm.username.$invalid}">
<label class=" col-md-3 control-label">Username:</label>
<div class="col-md-9">
<input name="username"
type="text"
class="form-control"
ng-model="user.username"
ng-disabled="user.id"
ng-minlength=5
username-verify
required />
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.required">Username is required.</span>
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.minlength">Username must be at least 5 characters.</span>
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.usernameVerify">Username already taken.</span>
</div>
</div>
Angular has a dedicated array of $asyncValidators for precisely this situation:
see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get({url:'/api/users/' + value}).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};
In order to get this to work, I needed to add "return value;" outside of the asynchronous call. Code below.
commonModule.directive("usernameVerify", [
'userSvc', function(userSvc) {
return {
require: 'ngModel',
scope: false,
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(checkForAvailability);
ctrl.$formatters.unshift(checkForAvailability);
function checkForAvailability(value) {
if (value.length < 5) {
return value;
}
userSvc.userExists(value)
.success(function(alreadyUsed) {
var valid = alreadyUsed === 'false';
if (valid) {
ctrl.$setValidity('usernameVerify', true);
return value;
}
ctrl.$setValidity('usernameVerify', false);
return undefined;
});
// Below is the added line of code.
return value;
}
}
}
}
]);

Resources