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">
 &nbsp;
 <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 =...