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!
Related
I want to validate a control against multiple fields. And I want to display an error indicating which field(s) caused validation to fail.
Run the following code. Change the first value to a high number (E.G. 5) and validation will pass. Change the first value to a low number (2) and validation will fail.
In the case of "2" there should be 2 errors: model3, model4 because those models are higher values than 2. Similarly for all other fields.
The validation works fine. I'm having trouble displaying the correct error messages based on the particular validation rule that failed.
Note, any field changes must re-fire validation just like it does now. You should run this snippet in full page view.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.model = {number: "1"};
$scope.model2 = {number: "2"};
$scope.model3 = {number: "3"};
$scope.model4 = {number: "4"};
});
app.directive('theGreatest', function(){
return {
require: 'ngModel',
restrict: 'A',
link: function($scope, $element, $attr, ngModel) {
var compareCollection;
// watch the attribute to get the date we need to compare against
$attr.$observe('theGreatest', function (val) {
console.log('compareCollection set to: ', val);
compareCollection = JSON.parse(val);
ngModel.$validate();
});
ngModel.$validators.theGreatest = function(modelValue, viewValue) {
console.log('validating...', modelValue);
console.log('compareDate: ', compareCollection);
var pass = true;
_.map(compareCollection, function(compare){
console.log('comparing value: ', compare);
if(modelValue < compare){
pass = false;
}
});
console.log('validation pass', pass);
return pass;
};
}
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js"></script>
<section ng-app="app" ng-controller="MainCtrl">
<div>first: <input type="text" ng-model="model.number" the-greatest="{{[model2.number, model3.number, model4.number]}}" />
(change me to a high number)
</div>
<div>second: <input ng-model="model2.number" type="text" /></div>
<div>third: <input ng-model="model3.number" type="text" /></div>
<div>fourth: <input ng-model="model4.number" type="text" /></div>
<div>validation passed if you see a value here: {{model.number}}</div>
<div>The following errors are not implemented correctly. The intention is to show what I am want to accomplish</div>
<div ng-if="!model.number">ERROR: first is less than model 2</div>
<div ng-if="!model.number">ERROR: first is less than model 3</div>
<div ng-if="!model.number">ERROR: first is less than model 4</div>
<div ng-if="!model.number">ERROR: first is required</div>
</section>
You need to send ErrorFlags array into directive and while you are validating mark those flags as false when validation fails.
HTML:
<section ng-app="app" ng-controller="MainCtrl">
<div>first: <input type="text" ng-model="model.number" the-greatest="{{[model2.number, model3.number, model4.number]}}" error-flags="errorFlags" />
(change me to a high number)
</div>
<div>second: <input ng-model="model2.number" type="text" /></div>
<div>third: <input ng-model="model3.number" type="text" /></div>
<div>fourth: <input ng-model="model4.number" type="text" /></div>
<div>validation passed if you see a value here: {{model.number}}</div>
<div>The following errors are not implemented correctly. The intention is to show what I want to accomplish</div>
<div ng-if="!errorFlags[0]">ERROR: first is less than model 2</div>
<div ng-if="!errorFlags[1]">ERROR: first is less than model 3</div>
<div ng-if="!errorFlags[2]">ERROR: first is less than model 4</div>
<div ng-if="!model.number">ERROR: first is required</div>
</section>
AngularJS Code:
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.model = {number: "1"};
$scope.model2 = {number: "2"};
$scope.model3 = {number: "3"};
$scope.model4 = {number: "4"};
$scope.errorFlags = [true, true , true];
});
app.directive('theGreatest', function(){
return {
require: 'ngModel',
restrict: 'A',
scope: {
errorFlags:"="
},
link: function(scope, element, attrs, ngModel) {
var compareCollection;
// watch the attribute to get the date we need to compare against
attrs.$observe('theGreatest', function (val) {
console.log('compareCollection set to: ', val);
compareCollection = JSON.parse(val);
ngModel.$validate();
});
ngModel.$validators.theGreatest = function(modelValue, viewValue) {
console.log('validating...', modelValue);
console.log('compareDate: ', compareCollection);
scope.errorFlags = [true, true, true];
console.log("Before Validation Flags", scope.errorFlags);
var pass = true;
var loopVariable = 0;
_.map(compareCollection, function(compare){
console.log('comparing value: ', compare);
if(modelValue < compare){
pass = false;
scope.errorFlags[loopVariable] = false;
}
loopVariable++;
});
console.log("after Validation Flags", scope.errorFlags);
console.log('validation pass', pass);
return pass;
};
}
};
});
The required directive shows the red error message that works!
The uniqueschoolclassnumberValidator directive shows NOT the red error message!
From the server I always return exists => true, but I also tried it with false.
What do I wrong? The custom directive is triggered for sure!
Directive
'use strict';
angular.module('TGB').directive('uniqueschoolclassnumberValidator', function (schoolclassCodeService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$asyncValidators.unique = function (schoolclass) {
var schoolclassNumber = "0a";
var schoolyearId = 1;
return schoolclassCodeService.exists(schoolyearId, schoolclassNumber);
};
}
};
});
Service
this.exists = function (schoolyearId, schoolclassNumber) {
var path = 'api/schoolyears/' + schoolyearId + '/schoolclasses/' + schoolclassNumber;
return $http.get(path).then(function (response) {
if (response.data == true) {
$q.reject("schoolclass number has already been taken");
}
else {
return $q.resolve();
}
});
};
Html
<form name="myForm">
<div class="col-sm-8">
<input type="text" unique-schoolclasnumber-Validator name="myInput"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 300, 'blur': 0} }"
ng-model="schoolclassNumber" class="form-control"
required placeholder="Enter schoolclass">
</div>
<div ng-messages="myForm.myInput.$error" style="color:red" role="alert">
<div ng-message="required">You did not enter anything.</div>
<div ng-message="unique">That schoolclass number already exists.</div>
</div>
</form>
In your service's exists method there should be return keyword before $q.reject:
if (response.data == true) {
return $q.reject("schoolclass number has already been taken");
}
else {
return $q.resolve();
}
Directive should be named uniqueSchoolclassnumberValidator instead of uniqueschoolclassnumberValidator (AngularJS change dash-delimited format to camelCase).
There is also a misspell in html code, in word "class". It should be unique-schoolclassnumber-Validator instead of unique-schoolclasnumber-Validator.
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
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>
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;
}
});
}
});