Knockout Validation Extender
Testing a prototype validation mechanism using Knockout JS and extenders.
by David Kyle
HTML
<script src="http://knockoutjs.com/downloads/knockout-3.2.0.js"></script>
<script src="https://code.jquery.com/ui/1.11.1/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.11.1/themes/mint-choc/jquery-ui.css">
<script src="http://momentjs.com/downloads/moment.js"></script>
<main id="form-section" class="main">
<h1>Welcome to an Example</h1>
<form enctype="multipart/form-data" method="post" id="input-form">
<h2>Validation Form</h2>
<div class="form-liner">
<p>Welcome, please fill out the form completely before continuing on to the next step.</p>
<p>Fields marked with a <mark class="validation-inactive">*</mark> are required.</p>
<section class="inner-liner">
<span class="form-item">
<label for="_firstName">First Name:<mark data-bind="css: FirstName.ValidationState.MessageClass()" >*</mark></label>
<input type="text" name="firstName" id="_firstName" tabindex="1" data-bind="textInput: FirstName, css: FirstName.ValidationState.IsValid() ? '' : 'item-error'" />
<span class="validation-message validation-active" data-bind="text: FirstName.ValidationState.ValidationMessage"></span>
</span>
<span class="form-item">
<label for="_lastName">Last Name:<mark data-bind="css: LastName.ValidationState.MessageClass()">*</mark></label>
<input type="text" name="lastName" id="_lastName" tabindex="2" data-bind="textInput: LastName, css: LastName.ValidationState.IsValid() ? '' : 'item-error'" />
<span class="validation-message validation-active" data-bind="text: LastName.ValidationState.ValidationMessage"></span>
</span>
<span class="form-item">
<label>Gender:</label>
<span class="radio-list vertical">
<!-- ko template: {name: 'gender-option-template', foreach: AvailableGenders, as: 'item'} -->
<!-- /ko -->
</span>
</span>
<span class="form-item">
<label for="_birthDate">Birth Date:</label>
<input type="text"...
CSS
.main {
width: 100%;
margin: 0px auto;
padding: 4px 0px;
color: #121212;
font-family: Candara, Arial, Sans-Serif;
font-size: small;
display: block;
}
.main H1 {
font-size: 130%;
font-weight: bold;
margin: 0px auto 5px 5px;
color: #565656;
}
.main H2 {
display: block;
width: 50%;
margin: 6px auto 10px auto;
padding: 2px;
text-align: center;
font-weight: bold;
border: outset 1px #dedede;
background: #453326 url("https://code.jquery.com/ui/1.11.1/themes/mint-choc/images/ui-bg_gloss-wave_25_453326_500x100.png") 50% 50% repeat-x;
border-radius: 3px;
font-size: 122%;
color: white;
}
.main INPUT[type='text'] {
border: solid 1px #ababab;
border-radius: 2px;
width: 160px;
}
.main INPUT[type='text']:HOVER {
background: #619226 url("https://code.jquery.com/ui/1.11.1/themes/mint-choc/images/ui-bg_highlight-soft_20_619226_1x100.png") 50% top repeat-x;
border: solid 1px #add978;
}
.main MARK {
background-color: inherit;
}
.main INPUT[type='submit'], .main INPUT[type='reset'], .main INPUT[type='button'] {
padding: 3px 5px;
border: 1px solid #695444;
background: #1c160d url("https://code.jquery.com/ui/1.11.1/themes/mint-choc/images/ui-bg_gloss-wave_20_1c160d_500x100.png") 50% 50% repeat-x;
box-shadow: 2px 1px 2px rgba(190, 190, 190, .8);
margin: 0 4px;
cursor: pointer;
color: #9bcc60;
}
.main INPUT[type='submit']:HOVER, .main INPUT[type='reset']:HOVER, .main INPUT[type='button']:HOVER {
background: url("https://code.jquery.com/ui/1.11.1/themes/mint-choc/images/ui-bg_gloss-wave_30_44372c_500x100.png") repeat-x scroll 50% 50% #44372C;
box-shadow: 4px 1px 3px rgba(50, 50, 50, .8);
}
.main INPUT[type='submit']:ACTIVE, .main INPUT[type='reset']:ACTIVE, .main INPUT[type='button']:ACTIVE {
box-shadow: 2px 0px 2px rgba(50, 50, 50, .8);
background-color: rgba(76, 151, 183, .8);
}
.main .form-liner {
margin: 0px auto;
...
JavaScript
//An initial state to control bio rendering and not show form errors on the first load.
var initialState = true;
$(function () {
//Set focus on the first element
$("[tabindex='1']", $("form")).focus();
$(document).on("click", "#_submit", function(e, data) {
alert(viewModel.IsValid());
return false; //For this fiddle, prevent submission
});
$(document).on("click", "#_clear", function(e, data) {
e.preventDefault();
console.log("Cleared");
viewModel.Clear();
});
$(".datepicker").datepicker({ showAnim: "puff", changeYear: true, yearRange: ("1930:" + new Date().getFullYear()), maxDate: new Date() });
ko.applyBindings(viewModel);
});
var GenderOptions = {
Unspecified: 0,
Male: 1,
Female: 2
};
GenderOptions.GetName = function(value) {
switch(value) {
case 0:
return 'Unspecified';
case 1:
return 'Male';
case 2:
return 'Female';
default:
return 'Unspecified';
} // end switch
};
var ValidationItemViewModel = function () {
var self = this;
self.IsValid = ko.observable(true);
self.PropertyName = ko.observable("");
self.MessageClass = ko.pureComputed(function() {
return self.IsValid() ? "validation-inactive" : "validation-active";
}, self);
self.ValidationMessage = ko.pureComputed(function () {
if (!self.IsValid()) {
return self.PropertyName() + " is required.";
} else {
return "";
} // end if/else
}, self);
};
ko.extenders.withName = function (target) {
target.Name = function() {
return GenderOptions.GetName(target());
};
return target;
};
ko.extenders.required = function (target, data) {
//add some sub-observables to our observable
target.ValidationState = new ValidationItemViewModel();
target.ValidationState.PropertyName(data.fieldName);
target.ValidationMessage =...