Created
June 26, 2015 21:53
-
-
Save jholland918/d3e74f894cc9ac782c7e to your computer and use it in GitHub Desktop.
AngularJS form validation example validating an interrelated collection of controls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// http://plnkr.co/edit/HoG0XH | |
(function () { | |
'use strict'; | |
var app = angular.module('app', []); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
angular | |
.module('app') | |
.controller('controller1', controller1); | |
controller1.$inject = ['$location']; | |
function controller1($location) { | |
var data = { | |
'title': 'More Snacks Please', | |
'description': 'Add beef jerky to the breakroom snacks.', | |
'reviewers': [{ | |
'id': 1, | |
'name': 'John Smith', | |
'office': 'Branch', | |
'route': '1' | |
}, { | |
'id': 2, | |
'name': 'Amy Jones', | |
'office': 'Corporate', | |
'route': '2' | |
}, { | |
'id': 3, | |
'name': 'Lucy Laflamme', | |
'office': 'Corporate', | |
'route': '3' | |
}] | |
}; | |
var vm = this; | |
vm.data = data; | |
vm.isValidReviewers = null; | |
vm.routeChanged = routeChanged; | |
vm.lastRouteChanged = null; | |
activate(); | |
function activate() { } | |
function routeChanged(name) { | |
vm.lastRouteChanged = name; | |
} | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html ng-app="app"> | |
<head> | |
<script data-require="[email protected]" data-semver="1.3.0" src="https://code.angularjs.org/1.3.0/angular.js"></script> | |
<link rel="stylesheet" href="style.css" /> | |
<script src="app.js"></script> | |
<script src="controller1.js"></script> | |
<script src="reviewersValidator.js"></script> | |
<script src="routeOrderValidator.svc.js"></script> | |
<script src="uniqueRouteValidator.svc.js"></script> | |
<script src="isZeroValidator.js"></script> | |
</head> | |
<body ng-controller="controller1 as vm"> | |
<h1>Request Form</h1> | |
<form name="form1"> | |
<label> | |
Title: | |
<input type="text" name="title" ng-model="vm.data.title" /> | |
</label> | |
<label> | |
Description: | |
<textarea type="text" name="description" ng-model="vm.data.description"></textarea> | |
</label> | |
<h3>Reviewers</h3> | |
<ul ng-repeat="reviewer in vm.data.reviewers"> | |
<li> | |
<label> | |
Name: | |
<input type="text" name="name_{{$index}}" ng-model="reviewer.name" /> | |
</label> | |
<label> | |
Office: | |
<select name="office_{{$index}}" ng-model="reviewer.office"> | |
<option>Branch</option> | |
<option>Corporate</option> | |
</select> | |
</label> | |
<label> | |
Routing Order: | |
<input type="text" name="route_{{$index}}" ng-model="reviewer.route" ng-model-options="{ allowInvalid: true }" is-zero-validator /> | |
<p ng-show="form1.route_{{$index}}.$error.uniqueRoute" class="error">Not a unique routing order!</p> | |
<p ng-show="form1.route_{{$index}}.$error.routeOrder" class="error">Branch employees must be first in the routing order!</p> | |
<p ng-show="form1.route_{{$index}}.$error.isZero" class="error">The test isZero validator is angry!</p> | |
</label> | |
</li> | |
</ul> | |
<div> | |
<div reviewers-validator name="reviewers" ng-model="vm.data.reviewers" ng-model-options="{ allowInvalid: true }" class="reviewers-validator"> | |
<p ng-show="form1.reviewers.$error.uniqueRoute" class="error">Duplicate routing orders found.</p> | |
<p ng-show="form1.reviewers.$error.routeOrder" class="error">Invalid routing order, Branch employees must appear first in routing order.</p> | |
</div> | |
</div> | |
</form> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
'use strict'; | |
angular | |
.module('app') | |
.directive('isZeroValidator', isZeroValidator); | |
function isZeroValidator() { | |
return { | |
require: 'ngModel', | |
link: link | |
}; | |
function link($scope, element, attrs, ngModel) { | |
ngModel.$validators.isZero = function (value) { | |
return (value !== "0"); | |
}; | |
} | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
angular | |
.module('app') | |
.directive('reviewersValidator', reviewersValidator); | |
reviewersValidator.$inject = ['uniqueRouteValidator', 'routeOrderValidator']; | |
function reviewersValidator(uniqueRouteValidator, routeOrderValidator) { | |
// Usage: | |
// <div reviewers-validator | |
// name="reviewers" | |
// ng-model="vm.data.reviewers" | |
// ng-model-options="{ allowInvalid: true }" > | |
// <p ng-show="form1.reviewers.$error.uniqueRoute" class="error">Error text.</p> | |
// <p ng-show="form1.reviewers.$error.routeOrder" class="error">Error text.</p> | |
// </div> | |
// | |
// Note 1: | |
// Look at the allowInvalid option in ngModelOptions and consider if you want to | |
// set allowInvalid to true for this directive. Using allowInvalid: true is recommended. | |
// ngModelOptions documentation: https://docs.angularjs.org/api/ng/directive/ngModelOptions | |
// | |
// Note 2: | |
// You might also want to use 'ng-model-options="{ allowInvalid: true }"' on the form | |
// controls you wish to validate in this directive because they may have individual | |
// validators of their own that will set their model to undefined when invalid. | |
// | |
// Alternatively, you could validate against the form control's $viewValue data, | |
// which would require you to parse the FormController properties to extract the | |
// $viewValue data for each form control. | |
var directive = { | |
require: ['ngModel', '^form'], | |
link: link, | |
restrict: 'EA' | |
}; | |
return directive; | |
function link(scope, element, attrs, controllers) { | |
var ngModel = controllers[0]; | |
var form = controllers[1]; | |
scope.$watch(attrs.ngModel, function () { | |
ngModel.$validate(); | |
}, true); | |
ngModel.$validators.routeOrder = function () { | |
return routeOrderValidator.validate(form, scope.$eval(attrs.ngModel)); | |
}; | |
ngModel.$validators.uniqueRoute = function () { | |
return uniqueRouteValidator.validate(form, scope.$eval(attrs.ngModel)); | |
}; | |
} | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
angular | |
.module('app') | |
.service('routeOrderValidator', routeOrderValidator); | |
routeOrderValidator.$inject = ['$filter']; | |
function routeOrderValidator($filter) { | |
this.validate = validate; | |
var fieldNamePrefix = 'route_'; | |
var validationErrorKey = 'routeOrder'; | |
function validate(form, collection) { | |
if (!collection) { | |
return true; | |
} | |
var invalidItems = getInvalidItems(collection); | |
if (!invalidItems || invalidItems.length === 0) { | |
setValid(form); | |
return true; | |
} | |
setInvalid(form, invalidItems); | |
return false; | |
} | |
function getInvalidItems(reviewers) { | |
reviewers.forEach(function (reviewer, index) { | |
reviewer.originalIndex = index; | |
}); | |
var sorted = $filter('orderBy')(reviewers, 'route'); | |
var misordered = []; | |
var branchCount = 0; | |
sorted.forEach(function (reviewer, index) { | |
if (reviewer.office == 'Branch') { | |
if (index !== branchCount) { | |
misordered.push(reviewer.originalIndex); | |
} | |
branchCount++; | |
} | |
}); | |
return misordered; | |
} | |
function setValid(form) { | |
var ctrls = getModelControllers(form); | |
ctrls.forEach(function (ctrl) { | |
ctrl.$setValidity(validationErrorKey, true); | |
}); | |
} | |
function setInvalid(form, itemIndexes) { | |
itemIndexes.forEach(function (index) { | |
form[fieldNamePrefix + index].$setValidity(validationErrorKey, false); | |
}); | |
} | |
function getModelControllers(form) { | |
var ctrls = []; | |
Object.getOwnPropertyNames(form).forEach(function (key) { | |
if (key.indexOf(fieldNamePrefix) === 0 && form[key].hasOwnProperty('$setValidity')) { | |
ctrls.push(form[key]); | |
} | |
}); | |
return ctrls; | |
} | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
label { | |
display: block; | |
margin-bottom:6px; | |
} | |
input,textarea,select { | |
display: block; | |
margin-bottom:6px; | |
} | |
ul { | |
list-style-type: none; | |
} | |
ul li { | |
background-color:#F0F0F0; | |
padding: 6px; | |
} | |
.error { | |
color: red; | |
} | |
.reviewers-validator.ng-invalid { | |
color:red; | |
border:solid 2px red; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function () { | |
'use strict'; | |
angular | |
.module('app') | |
.service('uniqueRouteValidator', uniqueRouteValidator); | |
function uniqueRouteValidator() { | |
this.validate = validate; | |
var fieldNamePrefix = 'route_'; | |
var validationErrorKey = 'uniqueRoute'; | |
function validate(form, collection) { | |
//console.log('collection', collection); | |
if (!collection) { | |
return true; | |
} | |
var invalidItems = getInvalidItems(collection); | |
if (!invalidItems || invalidItems.length === 0) { | |
setValid(form); | |
return true; | |
} | |
setInvalid(form, invalidItems); | |
return false; | |
} | |
function getInvalidItems(reviewers) { | |
var routes = []; | |
reviewers.forEach(function (reviewer) { | |
routes.push(reviewer.route); | |
}); | |
var counts = {}; | |
routes.forEach(function (route) { | |
counts[route] = (counts[route] || 0) + 1; | |
}); | |
var duplicates = []; | |
reviewers.forEach(function (reviewer, index) { | |
if (counts[reviewer.route] > 1) { | |
duplicates.push(index); | |
} | |
}); | |
return duplicates; | |
} | |
function setValid(form) { | |
var ctrls = getModelControllers(form); | |
ctrls.forEach(function (ctrl) { | |
ctrl.$setValidity(validationErrorKey, true); | |
}); | |
} | |
function setInvalid(form, itemIndexes) { | |
itemIndexes.forEach(function (index) { | |
form[fieldNamePrefix + index].$setValidity(validationErrorKey, false); | |
}); | |
} | |
function getModelControllers(form) { | |
var ctrls = []; | |
Object.getOwnPropertyNames(form).forEach(function (key) { | |
if (key.indexOf(fieldNamePrefix) === 0 && form[key].hasOwnProperty('$setValidity')) { | |
ctrls.push(form[key]); | |
} | |
}); | |
return ctrls; | |
} | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment