AngularJs validator with a form of many dynamically created inputs - angularjs

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.

Related

Date Range AngularJS Form validation

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" />

AngularJS form validation triggered by other fields

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/

How to validate a field based on another field in the same form using bootstrap and angularJS?

I have a form. In order to validate the fields (on the client side) I'm using a bit of HTML5 and ng-message to display the error.
Two of the fields behave differently from the rest of the fields.
If one of these 2 fields is filled, the other should be filled as well.
On the other side, if none of the two is filled, the form should accept these 2 fields as valid.
I want a direct feedback in the form, as soon as the user enter something in one of the two field, the other should display the corresponding message.
The 2 fields in the code bellow are fWebsiteURLand fWebsiteName.
What is the best approach to implement such logic in my directive/form ?
Here is portion of my partial (using jade and bootstrap 2.x.x).
At the moment, only a basic required validation is made for each field, making them mandatory for the all form.
Thanks for your help.
form.form-horizontal(name="scheduleForm")
.control-group(ng-class="{error: scheduleForm.fProjectName.$invalid}")
label.control-label.muted(for='fProjectName') Project
.controls
input.input-xxlarge(name="fProjectName", type='text', placeholder='Project name', ng-model="schedule.projectName", required)
small.help-inline(ng-messages="scheduleForm.fProjectName.$error")
div(ng-message="required")
| The project name is required.
.control-group(ng-class="{error: scheduleForm.fWebsiteName.$invalid}")
label.control-label.muted(for='fWebsiteName') Website name
.controls
input.input-xlarge(name="fWebsiteName", type='text', placeholder='Website name', ng-model="schedule.website.name", required)
small.help-inline(ng-messages="scheduleForm.fWebsiteName.$error")
div(ng-message="required")
| The website name is required if you entered a website link.
.control-group(ng-class="{error: scheduleForm.fWebsiteURL.$invalid}")
label.control-label.muted(for='fWebsiteURL') Website URL
.controls
input.input-xlarge(name="fWebsiteURL", type='text', placeholder='Website link', ng-model="schedule.website.url", required)
small.help-inline(ng-messages="scheduleForm.fWebsiteURL.$error")
div(ng-message="required")
| The website link is required if you entered a website name.
.control-group
.controls
button.btn.btn-primary(type='button', name='submit', ng-disabled="scheduleForm.$invalid", ng-click="update()") Save
button.btn(type='button', name='Cancel', ng-click="cancel()") Cancel
Here is a plunk that illustrates a simple example when whether one field is valid or not depends on the presence of the other:
http://plnkr.co/edit/sr3qDWLk7jpEc520qiKs?p=preview
Basically you create a validate function:
var checkAndSet = function(first, second, scope){
if ( !_.isUndefined(first) && first !== ''){
var firstFilledSecondNot = (_.isUndefined(second) || second === '');
scope.firstFilledSecondNot = firstFilledSecondNot;
} else {
scope.firstFilledSecondNot = false;
}
};
This function just straightforwardly checks if when the first input is filled in, the second one is not. The tricky part is you put the validation result into your scope to be used by the hidden input:
<input name="fSecondInput" data-ng-model="firstFilledSecondNot" type="hidden" data-ui-validate="{firstInputFilledInButSecondNot: 'isFirstInputFilledInButSecondNot($value)'}">
As you can see, this input has the same name as your "real" second input, and has as model the scope
firstFilledSecondNot
variable you exposed in the function in step 1. Then you have
a
data-ui-validate="{firstInputFilledInButSecondNot: 'isFirstInputFilledInButSecondNot($value)'}"
block which just takes this value and returns it. After the validation cycle has passed, you'd have
someForm.fSecondInput.$error.firstInputFilledInButSecondNot
object in your scope, which is what you wanted in the first place.
You could have just used the two watches and then put the result of each in your scope.someForm.fSecondInputObject, but I find it uglier:
var checkAndSet = function(first, second, scope){
if ( !_.isUndefined(first) && first !== ''){
var firstFilledSecondNot = (_.isUndefined(second) || second === '');
scope.someForm.fSecondInput.$error.firstFilledSecondNot = firstFilledSecondNot;
} else {
scope.someForm.fSecondInput.$error.firstFilledSecondNot = false;
}
};
Second plunkr here:
http://plnkr.co/edit/W1UW7E2D9DDuOJtL6Jea?p=preview
Based on the comment from #baba i think i found a solution to my problem, here is the logic i implemented.
I created a custom directive responsible to do the validation. This validator is called coBothOrNoneValidator. It require 2 parameters, the formand the other-field. This last parameter is the field being compare to.
Here is my jade partial:
form.form-horizontal(name="scheduleForm")
.control-group(ng-class="{error: scheduleForm.fProjectName.$invalid}")
label.control-label.muted(for='fProjectName') Project
.controls
input.input-xxlarge(name="fProjectName", type='text', placeholder='Project name', ng-model="schedule.projectName", required)
small.help-inline(ng-messages="scheduleForm.fProjectName.$error")
div(ng-message="required")
| The project name is required.
.control-group(ng-class="{error: scheduleForm.fWebsiteName.$invalid}")
label.control-label.muted(for='fWebsiteName') Website name
.controls
input.input-xlarge(name="fWebsiteName", type='text', placeholder='Website name', ng-model="item.website.name", co-both-or-none-validator, form="scheduleForm", other-field="fWebsiteURL")
small.help-inline(ng-messages="scheduleForm.fWebsiteName.$error")
div(ng-message="bothOrNone")
| The website name is required if you entered a website link.
.control-group(ng-class="{error: scheduleForm.fWebsiteURL.$invalid}")
label.control-label.muted(for='fWebsiteURL') Website URL
.controls
input.input-xlarge(name="fWebsiteURL", type='text', placeholder='Website link', ng-model="item.website.url", co-both-or-none-validator, form="scheduleForm", other-field="fWebsiteName")
small.help-inline(ng-messages="scheduleForm.fWebsiteURL.$error")
div(ng-message="bothOrNone")
| The website link is required if you entered a website name.
.control-group
.controls
button.btn.btn-primary(type='button', name='submit', ng-disabled="scheduleForm.$invalid", ng-click="update()") Save
button.btn(type='button', name='Cancel', ng-click="cancel()") Cancel
Finally, here is my directive co-both-or-none-validator:
angular.module('app').directive("coBothOrNoneValidator", function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, el, attrs, ctrl) {
var validateBothOrNone = function(value){
var form = scope[attrs.form];
var theOther = form[attrs.otherField];
var isNotValid = (value && (_.isUndefined(theOther.$modelValue) || theOther.$modelValue === ''))
ctrl.$setValidity('bothOrNone', !isNotValid);
theOther.$setValidity('bothOrNone', !isNotValid);
return value;
};
ctrl.$parsers.unshift(validateBothOrNone);
}
}
})
Here is a very basic demo
Any idea to improve this code is welcome

angularjs input field directive isn't clearing errors when scope changes value

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!

Force validation of entire form in AngularJS upon editing any portion of the form?

I have a form in which the validity depends upon the relationship between multiple textboxes. For example, if there are three textboxes, then the form is valid only if each textbox's integer value is greater than the previous textbox's integer value.
I'd like to set up this form so that if the user edits any of the textboxes, the entire form revalidates.
I've tried setting up ng-change=revalidate() on all the textboxes, with the following:
$scope.revalidate = function() {
var formData = $parse('signals');
var dataCopy = angular.copy(formData($scope));
formData.assign($scope, dataCopy);
};
I hoped that copying and reassigning the form's data would trigger revalidation, but it doesn't seem to work. How would I achieve this?
I solved this by creating a directive. In that directive, I set up a $watch on the concatenated values of all the textboxes. Then when that $watch sees a change in any of the textboxes, it revalidates the element. Since this directive is applied to all my textboxes, the entire form revalidates when any one of the textboxes is edited.
If someone has a more elegant solution than this, let me know.
link: function(scope, elm, attrs, ctrl) {
// when any of the intervals for this signal change, revalidate this interval
scope.$watch(
// loop through all the intervals for this signal, concatenate their values into one string
function() {
var intervals = [],
child = scope.$parent.$$childHead;
while (child !== null) {
console.log(child);
intervals.push(child.interval.end);
child = child.$$nextSibling;
}
return intervals.join();
},
function() {
validate(ctrl.$viewValue);
}
);
function validate(intervalDateTimeFromView) {
var valid = false;
// if this interval ends before or at the same time as the previous interval
if (scope.$$prevSibling && Number(intervalDateTimeFromView) <= Number(scope.$$prevSibling.interval.end))
{
ctrl.$setValidity('overlappingInterval', false);
return undefined;
} else {
ctrl.$setValidity('overlappingInterval', true);
return intervalDateTimeFromView;
}
}
ctrl.$parsers.unshift(validate);
ctrl.$formatters.unshift(validate);
}
It's not perfect, but it's what I'm working on at the moment:
$element.bind('blur', function() {
formCtrl[inputName].$dirty = true;
$scope.$emit('validate-refresh');
});
$scope.$on('validate-refresh', function() {
var control = formCtrl[inputName];
if (control.$dirty) {
control.$setViewValue(control.$viewValue);
}
}

Resources