/ko-observable-validation

Knockout validated observable - mixin for observables that provides hooks to add validation to an observble. Built on the premise, that view should be dumb.

Primary LanguageTypeScript

ko-observable-validation

What this is

ko-observable-validation is a very small library for knockout.js which provides validation hook mixin for observables. It is built on two simple and similar premises:

  • Views should be dumb (according Robert Cecil Martin).
  • Validation is part of business logic, as such it belongs in the Model \ ViewModel, newer in the View.

That is, your VM valdiation should be testable without any HTML.

What this does

You get a single extension function for your ko object, validateObservable, which accepts an observable and validation function, extends the obsevable in place with validationMessages observable array and hooks up the validation function as computed. As a result, changes to the value of the extended observable or observables used in validation function will retrigger validation.

Inside of validation function you are free to use your own code for validation or laverage one of many agnostic validation libraries for JS out there.

 var threshold = ko.observable(10);

 var subj = ko.validateObservable(ko.observable(1), function (val) {
            //value convertion is handled by ko bindings and extenders used in views (see below),
            //but validation is only handled here
            if (typeof val !== 'number') {
                return ["Number is required"];
            }
            
            if (val >= threshold()) {
                return ["Value must be lower than " + threshold()];
            }

            //more validation with generic reusable functions

            return [];
        });
        
        
        subj(8);
        console.log(subj.validationMessages().length); //0

        threshold(5);
        console.log(subj.validationMessages()[0]); //Value must be lower than 5

        subj(undefined);
        console.log(subj.validationMessages()[0]); //Number is required

        subj(2);
        console.log(subj.validationMessages().length); //0

After initial extension, observable is automatically considered valid, and the first validation is triggered only after its value changes, or you manually trigger it with .revalidate(). After it is triggered first time for whatever reason, validation function is now wrapped in computed. You also have ability to set valid state manually.

 var threshold = ko.observable(5);

 var subj = ko.validateObservable(100, function (val) {  
 // you can pass in a non-observable value, it will be wrapped in observable
            if (typeof val !== 'number') {
                return ["Number is required"];
            }
            
            if (val >= threshold()) {
                return ["Value must be lower than " + threshold()];
            }

            return [];
        });
        
        console.log(subj.validationMessages().length); //0, despite threshold being exceeded

        subj.revalidate();
        console.log(subj.validationMessages()[0]); //Value must be lower than 5

        threshold(120);
        console.log(subj.validationMessages().length); //0, revalidated, as the validation functiion was wrapped in computed after first execution

        threshold(5);
        console.log(subj.validationMessages()[0]); //Invalid again

        subj.forceStateValid();
        console.log(subj.validationMessages().length); //0, manually set valid

How to use this

Mainly we use it as follows:
html input element gets the value. Value is a string; input may have a widget like dropdown list;
value converter extender parses string to desired data type. It adds validation messages if value is not parsable; does not handle actuall validation;
value is written into vm observable. At this point, validation function evaluates it an produces validation messages if there are problems;

View:

    <input type="number" data-bind="value: foo.extend({integerInput:'Please enter integer'})">
    <div data-bind="foreach: foo.validationMessages">
        <div data-bind="text: $data">
    </div>

Value converter extenders are not part of this library, but they typically look like this:

// a typical knockout observable extender, as given here http://knockoutjs.com/documentation/extenders.html
ko.extenders.integerInput = function (target, option) {
    target.validationMessages = target.validationMessages || ko.observableArray();

    var result = ko.pureComputed({
        read: target,
        write: function(newValue){
            if(newValue.trim() === ""){
                target(null);
                return;
            }

            var intRegex = /^\d+$/;

            if(newValue.match(intRegex) == false){
                target.validationMessages(option || "Only integer allowed");
                return;
            }

            newValue = parseInt(newValue);

            if(target.revalidate && target() === newValue){
                target.revalidate();
                return;
            }
            target(newValue);

        }
    }).extend({notify: 'always'});

    result.validationMessages = target.validationMessages;

    // return new computed
    return result;
}

VM:

    var vm = {};

    vm.foo =  ko.validateObservable(100, function (val) {  
            if (typeof val !== 'number') {
                return ["Number is required"];
            }
            
            if (val >= 5) {
                return ["Value must be lower than 5"];
            }

            return [];
        });

Dev info

Developed in TypeScript (if you are new to TS - don't worry, just add lib\ko-obsevable-validation.js to your project). Tested with knockout 3.4.1 against evergreen browsers (latest Chrome, Edge, FireFox) and IE11.