Skip to content

Instantly share code, notes, and snippets.

@infocynic
Last active September 22, 2015 18:45
Show Gist options
  • Save infocynic/cfbc8b4ef8911411b627 to your computer and use it in GitHub Desktop.
Save infocynic/cfbc8b4ef8911411b627 to your computer and use it in GitHub Desktop.
Adds Knockout 3 bindings for rendering ASP.Net MVC input and validation markup for indexed primitive and complex types.
// Assigns an ASP.Net MVC-style name attribute to a form field that is part of a list.
// Used inside a foreach: binding.
// Example for an indexed primitive type (string, int, etc.) :
// <div data-bind="foreach: Items">
// <input type="text" data-bind="value: SomeValue, aspnetIndexedName: 'Items'" />
// </div>
//
// The above will name the input fields "Items[0]", "Items[1]", etc. For a complex type,
// a property can be specified in addition to the name:
// <div data-bind="foreach: Items">
// <input type="text" data-bind="value: FirstName, aspnetIndexedName: { name: 'Items', property: 'FirstName' }" />
// </div>
// or using a shorthand name.property form:
// <input type="text" data-bind="value: FirstName, aspnetIndexedName: 'Items.FirstName' }" />
//
// The above will name the input fields "Items[0].FirstName", "Items[1].FirstName", etc.
//
// The numeric index value defaults to the $index property of the binding context, but can be specified
// in the parameters:
// <div data-bind="foreach: Items">
// <input type="text" data-bind="value: FirstName, aspnetIndexedName: { name: 'Items', property: 'FirstName', index: Line }" />
// </div>
//
// For this to work properly with the ASP.Net MVC model binder, the index values must start at 0 and increase by 1;
// the sequence cannot omit any values.
//
// To render validation errors, the ASP.Net MVC model state must be available through the viewmodel. One way to
// do this is:
// function MyViewModel {
// var self = this;
// ...
// // Serialize the model state to capture the validation messages.
// self.modelState = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(ViewData.ModelState));
// }
;(function($) {
ko.bindingHandlers.aspnetIndexedName = {
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var name, property, index;
var bindings = allBindings().aspnetIndexedName;
// If just a string, parse it as "name.property". For primitive values, property is empty.
if (typeof bindings === "string") {
var parts = bindings.split(".");
name = parts[0];
property = parts[1];
index = bindingContext.$index();
}
else {
name = bindings.name;
property = bindings.property;
index = bindings.index || bindingContext.$index();
}
var fullname = name + '[' + index + ']';
if (property)
fullname = fullname + '.' + property;
$(element).attr({ name: fullname, id: fullname });
// Is there a modelstate error?
var modelStateProperty = bindings.modelState || 'modelState';
var modelState = bindingContext.$root[modelStateProperty];
if (modelState && modelState[fullname] && modelState[fullname].Errors.length) {
$(element).addClass("input-validation-error");
}
}
}
// Populates an ASP.Net MVC validation control that is rendered within a foreach: binding. The
// data-valmsg-for attribute is given the name of the input control, and the validation
// message is inserted.
// The validation messages must be serialized into the viewmodel.
//
// Example markup:
// <div data-bind="foreach: Items">
// <input type="text" data-bind="value: FirstName, aspnetIndexedName: 'Items.FirstName' }" />
// <span class="control-label" data-bind="aspnetIndexedValidation: 'Items.FirstName'}" />
// </div>
//
// Example view model:
// function MyViewModel {
// var self = this;
// ...
// // Serialize the model state to capture the validation messages.
// self.modelState = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(ViewData.ModelState));
// }
ko.bindingHandlers.aspnetIndexedValidation = {
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var name, index, property;
var bindings = allBindings().aspnetIndexedValidation;
// If parameters are just a string, it's the name
if (typeof bindings === "string") {
var parts = bindings.split(".");
name = parts[0];
property = parts[1];
index = bindingContext.$index();
}
else {
name = bindings.name;
property = bindings.property;
index = bindings.index || bindingContext.$index();
}
var fullname = name + '[' + index + ']';
if (property)
fullname = fullname + '.' + property;
$elem = $(element);
$elem.attr({ 'data-valmsg-for': fullname });
$elem.attr({ 'data-valmsg-replace': true });
var modelStateProperty = bindings.modelState || 'modelState';
// Insert initial error message
var modelState = bindingContext.$root[modelStateProperty];
if (modelState && modelState[fullname] && modelState[fullname].Errors.length) {
$elem.text(modelState[fullname].Errors[0].ErrorMessage);
$elem.addClass("field-validation-error");
}
else {
$elem.addClass("field-validation-valid")
}
},
};
})(jQuery);
@infocynic
Copy link
Author

Renamed file so it can be picked up with a ScriptBundle -{version}; added closure for jQuery.

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