I have an angularjs application and have to do form validation with custom business rules.
The problem is that my validation rules for a particular input field is dependent on other fields, and I don't know how to trigger the validation other than when the actual modelvalue changes.
The case is a dynamic list of employees each with a dynamic list of times of day to be entered. One rule is that these times must not overlap, which means one value can be invalid due to another value being changed and vice-versa.
I also have to show an error message for each field.
The form content is generated from the datamodel with a few layers of nested repeaters.
I have made a custom directive that contains the different validation rules and it triggers nicely when that field changes.
I'm using ngMessages to show the appropriate errormessage based on what business rule is violated.
The question is, how do I trigger validation on all other fields, when one particular field is changed? Preferably I should just trigger validation of all fields for the employee, whos value is being changed, since the values for one employee doesn't affect validation of other employees.
The fiddle here has a simplified version of my case, where the "overlap" rule just checks if two numbers are the same.
The html:
<form name="demoForm">
<div ng-repeat="employee in list">
<div ng-bind="employee.name"></div>
<div ng-repeat="day in employee.days" ng-form="employeeForm">
<input ng-model="day.hours" name="hours" custom-validate="{day: day, days: employee.days}" ng-model-options="{allowInvalid:true}" />
<span ng-messages="employeeForm.hours.$error">
<span ng-message="number">Should be a number.</span>
<span ng-message="businessHours">The number is outside business hours.</span>
<span ng-message="max">The number is too large.</span>
<span ng-message="overlap">The number must be unique for each employee.</span>
</span>
</div>
<br/>
</div>
</form>
The validation directive:
angular.module('app').directive('customValidate', [validator]);
function validator() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
data: '=customValidate'
},
link: linkFunc,
};
function linkFunc(scope, element, attrs, ctrl) {
ctrl.$validators.number = function(value) {
return value === "" || Number.isInteger(+value);
}
ctrl.$validators.businessHours = function(value) {
// imagine other validation data here
return value === "" || (value >= 1 && value <= 10);
}
ctrl.$validators.overlap = function(value) {
if (value === "") {
return true;
}
// find all other entries with identical value excluding self
var identical = scope.data.days.filter(function(x) {
return x !== scope.data.day && +x.hours === +value;
});
return identical.length === 0;
};
}
}
Fiddle here:
http://jsfiddle.net/maxrawhawk/dvpjdjbv/
The answer:
This little piece of code in the end of the directives link function:
scope.$watch('data', function(){
ctrl.$validate();
}, true);
Watch the data related to validation given from the markup with the most important detail 'true' as third parameter, making $watch check for object equality.
Updated fiddle:
http://jsfiddle.net/maxrawhawk/dvpjdjbv/12/
Related
I have 2 date fields: departure / arrival.
Both have validation on them using a directive.
Here is what the directive looks like for the "arrival" date range:
(function(angular) {
'use strict';
function dateRangeToValidatorDirective () {
return {
restrict: 'A',
require : 'ngModel',
link : function (scope, element, attrs, ngModelCtrl) {
function validateToDateRange (value) {
var valid = true;
if (scope.$eval(attrs.dateRangeValue) && value) {
var arrivalDate = Date.parse(value);
var departureDate = Date.parse(scope.$eval(attrs.dateRangeValue));
valid = arrivalDate >= departureDate;
if (valid) {
ngModelCtrl.$setValidity('toDateRange', true);
ngModelCtrl.$setValidity('fromDateRange', true); // Why doesn't this work?
}
else {
ngModelCtrl.$setValidity('toDateRange', false);
}
}
return value;
}
ngModelCtrl.$parsers.push(validateToDateRange);
}
}
}
angular
.module('components.shared')
.directive('dateRangeToValidator', dateRangeToValidatorDirective)
})(window.angular);
HTML usage:
<input type="text" name="arrivalDate" maxlength="10" required
date-picker date-range-to-validator
date-range-value="$ctrl.newFlight.departureDate"
placeholder='MM/DD/YYYY'
ng-model="$ctrl.newFlight.arrivalDate"
id="nf_arrivalDate" size="10" />
<div class="input-error"
ng-show="$ctrl.newFlight.departureDate.length &&
$ctrl.newFlight.arrivalDate.length &&
newFlight.arrivalDate.$dirty &&
newFlight.arrivalDate.$invalid">
Arrival Date cannot precede Departure Date!
</div>
The "departure" date range is the same with a few minor differences.
(no need to fill up the screen with the same code)
This works except in the following use case(s):
Step 1. Set departure date: 12/28/2017
Step 2. Set arrival date: 12/27/2017
===> Error is shown "Arrival Date cannot precede Departure Date!"
Step 3. Modify departure date: 12/26/2017
Actual: "Arrival Date" error message does not go away.
Expected: "Arrival Date" error message goes away.
Question: How do I get one directive that is on one element to affect the $valid state of another element that is using another directive?
Please notice my comment in the if (valid) block where I am asking why doesn't this work? If I set the validity of the model controller in one directive - why can't I get at the same model controller from another directive? I assume that this is case because that isn't working...
I ended up adding a couple of controller functions that would get invoked depending on the date field that changed using ng-change. First I needed to expose the form to my controller; hence you will notice that my form name changed. Once I could access the form from my controller, changing the validity of the element was easy.
function checkDepartureDate() {
if (ctrl.newFlightForm.arrivalDate.$valid &&
ctrl.newFlight.departureDate.length &&
ctrl.newFlightForm.departureDate.$invalid) {
ctrl.newFlightForm.departureDate.$setValidity("fromDateRange", true);
}
}
}
<input type="text" name="arrivalDate" maxlength="10" required
date-picker date-range-to-validator
date-range-value="$ctrl.newFlight.departureDate"
placeholder='MM/DD/YYYY'
ng-model="$ctrl.newFlight.arrivalDate"
ng-change="$ctrl.checkDepartureDate()"
id="nf_arrivalDate" size="10" />
I'm creating a site in which I have objects that all have a 'tags' property, which is a list of strings.
I'm creating a search functionality that filters all elements in a list. If the user enters '#something here', then I want to ONLY match the user input to the tags of each property. If the user just enters a string in the search box, then I want to search all object properties.
I have a form defined like so:
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input data-ng-model="$root.searchText" type="text" class="form-control" placeholder="#hashtag or just a string">
</div>
</form>
I know that the default filter can be used in the way I want it to if I define the data-ng-model with the field I want. So if I wanted to only search tags, I'd do data-ng-model='$root.searchText.tags', and user input will only match that. If I want to search all, then I'd do data-ng-model='$root.searchText.$'.
But how can I make the model switch based on whether or not a user types in a string with '#' at the beginning or not?
I've tried creating a custom filter, but that got confusing. There should be some kind of conditional statement that either sets the model to be $root.searchText.tags or $root.searchText.$, but that's more difficult that I thought. Does anyone know how to structure that conditional statement?
I have a solution for you, although it might not be the best workaround.
You watch the search filter and update the lists based on your logic:
1.filter by tags if searchText starts with '#'
2.fitler by properties values if searchText do not starts with '#'
$scope.$watch('model.search', function(search){
var tagFilter,results;
if(search.startsWith('#')) { //handle tag filter logic
tagFilters = search.split('#');
console.log(tagFilters);
results = _.filter($scope.model.lists, function(obj){
return _.intersection(obj.tags, tagFilters).length > 0;
});
} else { //handle property filter logic
results = _.filter($scope.model.lists, function (obj) {
return _.values(obj).some(function (el) {
return el.indexOf(search) > -1;
});
});
}
console.log(results);
$scope.model.displayItems = results;
}, true);
plnkr
Is there a way to create a custom fieldtype in angular schema form that is able to sum other fields on the form? I've looked at directives/decorators but am not sure how to reference the fields to be summed?
The easiest way is probably to let the user supply the keys of the fields you'd like to sum up the and watch for changes with a $watchGroup
So an example of how a form definition could look:
{
type: "sum",
sumFields: [
"key.to.field1",
"key.to.field2",
"key.to.field3"
]
}
And then you need a directive in your field type sum to actually sum things up. (WARNING, untested code)
<div class="form-control-group">
<input disabled sum-it-up="form.sumFields" type="text">
</div>
angular.module('myModule').directive('sumItUp',function(){
return {
scope: true,
link: function(scope, element, attr) {
var keys = scope.$eval(attrs.sumItUp);.map(function(key){
// Whatever you put in model is always available on scope
// as model. Skipped ObjectPath for simplification.
return 'model.'+key
})
scope.$watchGroup(keys, function(values) {
element.value(values.reduce(function(sum, value){
return sum+value
},0))
})
}
}
})
You could also do a fieldset type of thing and loop through each item in the items property of the form and go from there. The sfSelect service might also be useful. Hope that helps!
I'm trying to create a form of many rows of inputs using ng repeat. Each row has many inputs, with the values the user enters for each row adding up to the a value specific to each row e.g
Value = 20 input 1 = 10, input 2 = 10,
Value = 24 input 1 = 14, input 2 = 10,
From here the user can only submit the form if each line is fully completed and correct. Therefore i want to use ng-show="entireForm.$valid" to display the submit button.
Is this possible? I have been stuck on this problem for a number of days and am completely confused as I am new Angular. As of the minute I have created the view and setup the validator with no logic inside. Due to the amount of opinion out there I'm unsure if this is even possible with ng-repeat
MM.app.directive('lineitemvalidator', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.lineItemValidator = function(modelValue,viewValue) {
var totalInputHrs = parseFloat(scope.childForm.bill.$viewValue) + parseFloat(scope.childForm.drawDown.$viewValue)
+ parseFloat(scope.childForm.carryForward.$viewValue) + parseFloat(scope.childForm.writeOff.$viewValue) + parseFloat(scope.childForm.roundUp.$viewValue);
if((totalInputHrs==parseFloat(scope.lineItem.billableTime))){
/*scope.childForm.bill.$valid=true;
scope.childForm.drawDown.$valid=true;
scope.childForm.carryForward.$valid=true;
scope.childForm.writeOff.$valid=true;
scope.childForm.roundUp.$valid=true;
scope.childForm.drawDown.$setValidity("drawDown", true);
scope.childForm.bill.$setValidity("bill", true);
scope.childForm.carryForward.$setValidity("carryForward", true);
scope.childForm.writeOff.$setValidity("writeOff", true);
scope.childForm.roundUp.$setValidity("roundUp", true);
scope.childForm.$setValidity("childForm",true,scope.childForm);
*/
console.log("Form valid should = true");
console.log(scope.childForm);
return true;
console.log("Form valid = "+scope.childForm.$valid);
}else{
/*scope.childForm.bill.$valid=false;
scope.childForm.drawDown.$valid=false;
scope.childForm.carryForward.$valid=false;
scope.childForm.writeOff.$valid=false;
scope.childForm.roundUp.$valid=false;
scope.childForm.drawDown.$setValidity("drawDown", false);
scope.childForm.bill.$setValidity("bill", false);
scope.childForm.carryForward.$setValidity("carryForward", false);
scope.childForm.writeOff.$setValidity("writeOff", false);
scope.childForm.roundUp.$setValidity("roundUp", false);
scope.childForm.$setValidity("childForm",true,scope.childForm);
*/
console.log("Form valid = "+scope.childForm.$valid);
return false;
console.log("Form valid = "+scope.childForm.$valid);
}
}
}
};
You will have to use ng-form for this scenario. Each form will have some input and validations specific to its input fields. The parent form get automatically marked as $valid or $invalid if any of the child form validation fails.
The setup would look something like this:
<form name='entireForm'>\
<ng-form name='formChild' ng-repeat='item in items'>
<input name='input1' ng-model='item.field1'/>
<input name='input2' ng-model='item.field2'/>
</ng-form>
</form>
Each ng-form can be validated individually and the overall state of the form entireForm is valid only when all children are valid.
I have a directive designed to impose date range restrictions on a date input field (earliest and latest). Here is the directive below, I am using the momentjs library to do my date comparison:
.directive('startDate', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
console.log(arguments);
var compareStartDates = function (value) {
var startDateCompare = moment((attrs.startDate ? scope.$eval(attrs.startDate) : '1901-01-01'), 'YYYY-MM-DD');
if (startDateCompare && startDateCompare.isValid() && value && value.match(/\d{4}\-?\d{2}\-?\d{2}/g)) {
var valueMoment = moment(value, 'YYYY-MM-DD');
if (valueMoment && valueMoment.isValid() && valueMoment < startDateCompare) {
ctrl.$setValidity('startdate', false);
ctrl.$error['startdate'] = true;
return undefined;
}
}
ctrl.$setValidity('startdate', true);
return value;
};
ctrl.$parsers.unshift(compareStartDates);
}
};
})
JSFiddle: http://jsfiddle.net/2ug4X/
Look at the fiddle above and do the following:
1) enter "A" in the text box, the pattern error triggers.
2) click the "CLICK ME" text, which updates teh value of the model on the scope, notice the error clears
3) enter "1800-01-01" in the text box, the date restriction error triggers
4) enter "2000-01-01" in the text box which is a valid date, should clear the startdate error but it doesn't. Any idea why this is happening?
I'd expect updating the ng-model bound variable like so
scope.sample.open_date = '2000-01-01';
would clear the error on the input element like the pattern error clears.
Found this after searching more on Stack: AngularJS custom validation not firing when changing the model programatically
it seems my error was not also pushing the compare function to the ctrl.$formatters like so:
ctrl.$formatters.unshift(compareStartDates);
this did the trick!