Skip to content

Instantly share code, notes, and snippets.

@andybooth
Created June 5, 2011 18:02
Show Gist options
  • Select an option

  • Save andybooth/1009233 to your computer and use it in GitHub Desktop.

Select an option

Save andybooth/1009233 to your computer and use it in GitHub Desktop.
Knockout Validation
(function () {
function isArray(o) { return o.isArray || Object.prototype.toString.call(o) === '[object Array]'; }
function isObject(o) { return o != null && typeof o === 'object'; }
function values(o) { var r = []; for (var i in o) { r.push(o[i]); } return r; }
function hasAttribute(node, attr) { if (node.hasAttribute) return (node.hasAttribute(attr)); else return (!!node.getAttribute(attr)); }
function isValidatable(o) { return o.rules && o.isValid && o.isModified; }
function insertAfter(node, newNode) { node.parentNode.insertBefore(newNode, node.nextSibling); }
function extend(o, p, q) { if (!p) return o; for (var i in p) { if (isObject(p[i])) { if (!o[i]) o[i] = {}; extend(o[i], p[i]); } else { o[i] = p[i]; } } if (q) extend(o, q); return o; }
var configuration = {
registerExtenders: false,
messagesOnModified: false,
messageTemplate: null,
insertMessages: false,
parseInputAttributes: false
};
ko.validation = {
configure: function (options) {
extend(configuration, options);
ko.validation.registerExtenders();
ko.validation.registerValueBindingHandler();
},
group: function (obj) { // array of observables or viewModel
var group = isArray(obj) ? obj : values(obj);
var observables = ko.utils.arrayFilter(group, function (item) {
if (ko.isObservable(item)) {
item.extend({ validatable: true });
return true;
}
return false;
});
var result = ko.dependentObservable(function () {
var errors = [];
ko.utils.arrayForEach(observables, function (observable) {
var error = observable.isValid();
if (error)
errors.push(error);
});
return errors;
});
result.showAllMessages = function () {
ko.utils.arrayForEach(observables, function (observable) {
observable.isModified(true);
});
};
return result;
},
formatMessage: function (message, params) {
return message.replace('{0}', params);
},
addRule: function (observable, rule) {
observable.extend({ validatable: true });
observable.rules.push(rule);
return observable;
},
addExtender: function (name, validator, message) {
ko.extenders[name] = function (observable, params) {
return ko.validation.addRule(observable, {
validator: validator,
message: message,
params: params
});
};
},
registerExtenders: function () { // root extenders optional, use 'validation' extender if would cause conflicts
if (configuration.registerExtenders) {
for (var i in ko.validation.validators) {
ko.validation.addExtender(i, ko.validation.validators[i], ko.validation.messages[i]);
}
}
},
insertValidationMessage: function (element) {
var span = document.createElement('SPAN');
span.className = 'validationMessage';
insertAfter(element, span);
return span;
},
registerValueBindingHandler: function () { // parse html5 input validation attributes where value binder, optional feature
var init = ko.bindingHandlers.value.init;
ko.bindingHandlers.value.init = function (element, valueAccessor, allBindingsAccessor, viewModel, options) {
init(element, valueAccessor, allBindingsAccessor);
var config = extend({}, configuration, options.validation);
if (config.parseInputAttributes) {
setTimeout(function () {
ko.utils.arrayForEach(['required', 'min', 'max', 'maxLength', 'pattern'], function (attr) {
if (hasAttribute(element, attr)) {
ko.validation.addRule(valueAccessor(), {
validator: ko.validation.validators[attr],
message: ko.validation.messages[attr],
params: element.getAttribute(attr) || true
});
}
});
}, 0);
}
if (config.insertMessages && isValidatable(valueAccessor())) {
var validationMessageElement = ko.validation.insertValidationMessage(element);
if (config.messageTemplate) {
ko.renderTemplate(config.messageTemplate, {
field: valueAccessor()
}, null, validationMessageElement, 'replaceNode');
} else {
ko.applyBindingsToNode(validationMessageElement, {
validationMessage: valueAccessor()
});
}
}
};
},
messages: { // allows localization
required: 'This field is required.',
min: 'Please enter a value greater than or equal to {0}.',
max: 'Please enter a value less than or equal to {0}.',
minLength: 'Please enter at least {0} characters.',
maxLength: 'Please enter no more than {0} characters.',
pattern: 'Please check this value.'
},
validators: {
required: function (val, required) {
return required && val && val.length > 0;
},
min: function (val, min) {
return !val || val >= min;
},
max: function (val, max) {
return !val || val <= max;
},
minLength: function (val, minLength) {
return val && val.length >= minLength;
},
maxLength: function (val, maxLength) {
return !val || val.length <= maxLength;
},
pattern: function (val, regex) {
return !val || val.match(regex) != null;
}
}
};
ko.bindingHandlers.validationMessage = { // individual error message, if modified or post binding
update: function (element, valueAccessor) {
valueAccessor().extend({ validatable: true });
ko.bindingHandlers.text.update(element, function () {
return !configuration.messagesOnModified || valueAccessor().isModified() ? valueAccessor().isValid : null;
});
}
};
ko.bindingHandlers.validationOptions = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, options) {
return { 'controlsDescendantBindings': true }
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, options, descendantBindingContext) {
var localOptions = extend({}, options, { validation: valueAccessor() });
ko.applyBindingsToDescendants(viewModel, element, localOptions);
}
};
ko.extenders.validation = function (observable, rules) { // allow single rule or array
ko.utils.arrayForEach(isArray(rules) ? rules : [rules], function (rule) {
ko.validation.addRule(observable, rule);
});
return observable;
};
ko.extenders.validatable = function (observable, enable) {
if (enable && !isValidatable(observable)) {
observable.rules = ko.observableArray();
observable.isValid = ko.dependentObservable(function () {
for (var i = 0; i < observable.rules().length; i++) {
var r = observable.rules()[i];
if (!r.validator(observable(), r.params || true)) // default param is true, eg. required = true
return ko.validation.formatMessage(r.message, r.params);
}
return null;
});
observable.isModified = ko.observable(false);
observable.subscribe(function (newValue) {
observable.isModified(true);
});
}
return observable;
}
})();
@ericmbarnard
Copy link

Andy,

Is this still Active, and have you and Steve worked on anything with it recently?

I was thinking about making some contributions, but I wasn't sure if this was the most mature version of the validation plugin for 1.3.

@andybooth
Copy link
Author

Hi Eric

This remains the most up-to-date version of the Knockout validation prototype I started. There have been no changes recently. Would be great if you found it useful to build upon.

Andy

@tfsjohan
Copy link

Is there any documentation, blog post or forum post about this plugin? Looks pretty sweet!

@andybooth
Copy link
Author

This validation prototype relates to the discussion thread at https://groups.google.com/d/topic/knockoutjs/28TNX4eQ9sI/discussion and the sample at http://jsfiddle.net/andybooth/2GUyX.

@tfsjohan
Copy link

Thanks. Really like this!

One thing I do miss or haven't found out is to be able to set individual validation messages on the html element, but to keep all rules in the model. The main reason I wan't this is to make my models more reusable and put design and copy in the html, but logic in js.

Any ideas on how to implement this thru a element attribute or a ko custom binding?

// Johan

@ericmbarnard
Copy link

Andy,

Took a bit longer than I was hoping, but I've updated this on my fork at https://gist.github.com/1301497

There were some breaking changes between the Knockout 1.3 CTP that you have in the fiddle and what the latest is in the Knockout repo (looks like Steve is doing several pull requests tonight as well, so hopefully this all still works...)

  1. I re-wrote the validationOptions binding as the ko.applyBindingsToDescendants method in the CTP no longer seems to exist. I instead went with embedding a with binding, and so hopefully its a little more extensible going forward.
  2. I added a lot of comments to make it easier for me to remember what logic was doing in certain places... and hopefully give some examples to folks as they read the source.
  3. I changed how rules are stored in the plugin so that it more closely resembles (hopefully this change isn't too opinionated) implementing a custom bindingHandler.
     validator: function( val, mustEqualVal ){
         return val === mustEqualVal;
     }, 
     message: 'This field must equal {0}'
    };
    
    

Anyways, I also tweaked a few other things to give some easier syntax when defining bindings (but left full backward compatibility as much as I can tell), you can see the fiddle here: http://jsfiddle.net/ericbarnard/KHFn8/

Finally, I did put together a repo w/ a Unit Test suite here: https://github.com/ericmbarnard/Knockout-Validation
You started this project, so if you want to start your own repo or pull the bits out of mine and start one... I will get rid of mine and issue pull requests to yours.

Thanks for getting this started!

@andybooth
Copy link
Author

Eric,

Thanks for adding momentum and improvements to the plugin. Using the "with" binding for the cascading validation options is especially cool.

Going forward, if it meets your requirements and you have little bits of time to work on it, please do circulate https://github.com/ericmbarnard/Knockout-Validation and the JsFiddle on the KnockoutJS Google Group as the new home of the plugin.

Andy

@ericmbarnard
Copy link

Andy,

Thanks a ton! My company is looking at leveraging this in our existing application, so I should be able to keep it up to date. Thanks for all the work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment