Knockout.js - Form validation and reset to last valid value

by nitronic

HTML

<script src="https://github.com/SteveSanderson/knockout/raw/master/build/output/knockout-latest.js"></script>
<script src="https://github.com/downloads/SteveSanderson/knockout/jquery.tmpl.js"></script>
<div id="bookform">
    <h3>Title</h3>
    <p data-bind="css: { error: title.hasError }">
        <input data-bind='value: title, valueUpdate: "afterkeydown", event: { blur: function() { updateLastValidValue(title); }}' />
        <span data-bind='visible: title.hasError, text: title.validationMessage'> </span>
    </p>
    <h3>Author</h3>
    <p data-bind="css: { error: author.hasError }">
        <input data-bind='value: author, valueUpdate: "afterkeydown", event: { blur: function() { updateLastValidValue(author); }}' />
        <span data-bind='visible: author.hasError, text: author.validationMessage'> </span>
    </p>
    <h3>Pages</h3>
    <p data-bind="css: { error: pages.hasError }">
        <input data-bind='value: pages, valueUpdate: "afterkeydown", event: { blur: function() { updateLastValidValue(pages); }}' />
        <span data-bind='visible: pages.hasError, text: pages.validationMessage'> </span>
    </p>
    <p>
        <input type="button" value="Save" data-bind="disable: hasErrors"></input>
        <input type="button" value="Undo" data-bind="enable: hasErrors, click: resetToValidValues"></input>
    </p>
</div>

CSS

h3 {
  font-size: 120%;
  font-weight: bold;  
  margin-top: 5px;
}

.error {
 color: red;   
}

#bookform {
  margin: 10px;
  padding: 20px 20px 25px 20px;  
  background-color: #eee;
}

input {
  width: 250px;   
}

JavaScript

ko.extenders.requiredField = function(target, message) {   
    target.hasError = ko.observable();
    target.validationMessage = ko.observable();
    target.lastValidValue = ko.observable();
 
    function validate(newValue) {
       target.hasError(newValue ? false : true);
       target.validationMessage(newValue ? "" : message || "* required");
    }
    
    function setInitialValueIfValid() {
        if (!target.hasError()) {
            target.lastValidValue(target());
        }  
    }
 
    validate(target());
    setInitialValueIfValid();
    target.subscribe(validate);
 
    return target;
};
 
function BookViewModel(title, author, pages) {
    var self = this;
    self.title = ko.observable(title).extend({ requiredField: "Please enter a title" });
    self.author = ko.observable(author).extend({ requiredField: "Please enter an author" });
    self.pages = ko.observable(pages).extend({ requiredField: "" });
    
    self.hasErrors = ko.computed(function() { return self.title.hasError() || self.author.hasError() || self.pages.hasError(); });
    
    self.updateLastValidValue = function(observable) {
        if (!observable.hasError()) {
            observable.lastValidValue(observable());
        }
    };
    
    self.resetToValidValues = function() {
        self.title(self.title.lastValidValue());
        self.author(self.author.lastValidValue());
        self.pages(self.pages.lastValidValue());
    };
}

var viewModel = new BookViewModel("Windows Presentation Foundation 4.5 - EinfĂĽhrung und Praxis", "Norbert Eder", 400);
ko.applyBindings(viewModel);