Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind unobtrusive validation with custom KnockoutJS binding

Using MVC 4 with KnockoutJS. Can I bind unobtrusive validation with a custom knockout binding? I am currently rebinding the validation using a template with afterRender. I would love to have it automatically added with the binding. Like this:

ko.bindingHandlers.egtZipRep = {
    init: function (element, valueAccessor, allBindingsAccessor, context) {
      $(element).inputmask("99999", { "placeholder": " " });
      egtUniqueNameBinding(element, ++ko.bindingHandlers['uniqueName'].currentIndex);

      applyValidationRules(element); // Is it possible to do this here?

      ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor, context);
    }
};

I've tinkered around with it all day. I can't do it without being extremely inefficient.

The way I currently do it, is below. Maybe I should just be happy with it. But I'm guessing people have tried this before.

self.ReferenceAfterRender = function (element) {
    bindUnobtrusiveValidation(element);
}

// Bind validation on new content
function bindUnobtrusiveValidation(element) {
   // Bind to fields - must be called everytime new field is created
   $.validator.unobtrusive.parseDynamicContent(element);
}

$.validator.unobtrusive.parseDynamicContent = function (selector) {
// Use the normal unobstrusive.parse method
$.validator.unobtrusive.parse(selector);

// Get the relevant form
var form = $(selector).first().closest('form');

// Get the collections of unobstrusive validators, and jquery validators
// and compare the two
var unobtrusiveValidation = form.data('unobtrusiveValidation');
var validator = form.validate();

if (typeof (unobtrusiveValidation) != "undefined") {
  $.each(unobtrusiveValidation.options.rules, function (elname, elrules) {
    if (validator.settings.rules[elname] === undefined) {
      var args = {};
      $.extend(args, elrules);
      args.messages = unobtrusiveValidation.options.messages[elname];
      $('[name=' + elname + ']').rules("add", args);
    } else {
      $.each(elrules, function (rulename, data) {
        if (validator.settings.rules[elname][rulename] === undefined) {
          var args = {};
          args[rulename] = data;
          args.messages = unobtrusiveValidation.options.messages[elname][rulename];
          $('[name=' + elname + ']').rules("add", args);
        }
      });
    }
  });
 }
like image 632
Ricka Avatar asked Mar 07 '13 01:03

Ricka


1 Answers

Interesting question! Here's one pure KnockoutJS + VanillaJS solution. There may be some wrinkles, cross browser stuff (I'm looking at you, IE!), and rough edges. Let me know in comments or suggest an update to the answer if you like.


ViewModel & Validation Rules:
The validation rules should be close to the ViewModel's properties, much like attributes in .NET. The documentation for KnockoutJS suggests using extenders for this purpose. Usage would look like this:

self.name = ko.observable("Bob-Martin");
self.name = self.name.extend({ regex: { pattern: "^[^0-9]*$", message: "No digits plz!" } })
self.name = self.name.extend({ regex: { pattern: "^[^-]*$", message: "No dashes plz!" } });

Code for the Extender:
The extender from the documentation is nice and simple. Here's an alternative that handles multiple validation errors (though it needs some work for multiple rules with the same message):

ko.extenders.regex = function(target, options) {
    options = options || {};
    var regexp = new RegExp(options.pattern || ".*");
    var message = options.message || "regex is mad at you, bro!";

    // Only create sub-observable if it hasn't been created yet
    target.errors = target.errors || ko.observableArray();

    function validate(newValue) {
        var matched = regexp.test(newValue);

        if (!matched && target.errors.indexOf(message) == -1) {
            target.errors.push(message);
        }
        else if (matched && target.errors.indexOf(message) >= 0) {
            // TODO: support multiple extender instances with same 
            // message yet different pattern.
            target.errors.remove(message);
        }
    }

    validate(target()); //initial validation
    target.subscribe(validate); //validate whenever the value changes
    return target; //return the original observable
};

Template for validation messages:
To make the View DRY and validation unobtrusive I'd define a template for validation errors like this:

<script type="text/html" id="validation">
    <span data-bind="foreach: $data" class="errors">
        <span data-bind='text: $data'> </span>    
    </span>
</script>

View:
The actual view can be very simple:

<p>Name: <input data-bind='valueWithValidation: name' /></p>

Unobtrusive and DRY, because there is no markup here with validation messages. (If you want special markup for your validation though, you could just use a value binding and create seperate markup for name.errors.)

Custom binding:
And the custom binding would just do, in order:

  1. Inject the template after the input field.
  2. Apply the correct template binding with the name observable as data.
  3. Pass the rest on to the value and valueUpdate bindings.

Here is the binding (that may need some refactoring, and jQuery/javascript lovin' though):

ko.bindingHandlers.valueWithValidation = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        // Interception! Add validation markup to the DOM and
        // apply the template binding to it. Some of this code
        // can be more elegant, especially if you use jQuery or
        // a similar library.
        var validationElement = document.createElement("span");
        element.parentNode.insertBefore(validationElement, element.nextSibling);
        ko.applyBindingsToNode(validationElement, { template: { name: 'validation', data: valueAccessor().errors } });

        // The rest of this binding is handled by the default
        // value binding. Pass it on!
        ko.applyBindingsToNode(element, { value: valueAccessor(), valueUpdate: 'afterkeydown' });
    }
};

Demo:
To see all this in action, have a peek at this jsfiddle.

like image 160
Jeroen Avatar answered Nov 15 '22 19:11

Jeroen