Skip to content

Instantly share code, notes, and snippets.

@dgwaldo
Last active February 28, 2016 07:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dgwaldo/5a842416d2db54502d77 to your computer and use it in GitHub Desktop.
Save dgwaldo/5a842416d2db54502d77 to your computer and use it in GitHub Desktop.
Client Side Collection Editing with Unobtrusive Validation
//This is our Partial that utilizes the EditorTemplate.
@model Domain.Models.Themes.Asset
@Html.EditorFor(m=>m, "Asset")
//Note: Just to show what script you need to load an in what order to get client side unobtrusive validation working in MVC5.
//Most likely you'll want to use the bundler to load these.
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
<script src="~/Scripts/jquery.loadTemplate-1.5.0.min.js"></script>
<script src="~/Scripts/site.js"></script>
//This is the template for our collection item, it is a Shared/EditorTemplate file.
@model Domain.Models.Themes.Asset
@{
var assetTypes = ViewData["AssetTypes"] as SelectList;
}
<div class="row">
<div class="col-md-2">
@Html.DropDownListFor(m => m.AssetType, assetTypes, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.AssetType, "", new { @class = "text-danger" })
</div>
<div class="col-md-2">
@Html.CheckBoxFor(m => m.IsAsync, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(m => m.IsAsync, "", new { @class = "text-danger" })
</div>
<div class="col-md-2">
@Html.CheckBoxFor(m => m.IsDefer, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(m => m.IsDefer, "", new { @class = "text-danger" })
</div>
<div class="col-md-2">
@Html.CheckBoxFor(m => m.IsRequiredInHeader, new { htmlAttributes = new { @class = "form - control" } })
@Html.ValidationMessageFor(m => m.IsRequiredInHeader, "", new { @class = "text-danger" })
</div>
<div class="col-md-2">
@Html.EditorFor(m => m.Url, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(m => m.Url, "", new { @class = "text-danger" })
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-danger removeAssetBtn" type="button">Remove</button>
</div>
</div>
//This is the edit form for our collection, it is a Shared/EditorTemplate file.
@model List<Domain.Models.Themes.Asset>
<div class="row">
<div class="col-md-2">
<strong>Type</strong>
</div>//This is our EditorTemplate
@model Domain.Models.Themes.Asset
@Html.EditorFor(m=>m, this.ViewData)
<div class="col-md-2">
<strong>Async</strong>
</div>
<div class="col-md-2">
<strong>Defer</strong>
</div>
<div class="col-md-2">
<strong>Required in header</strong>
</div>
<div class="col-md-2">
<strong>URL</strong>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-default" type="button" id="AddAsset">Add Asset</button>
</div>
</div>
//Here all the current collection items are rendered out, if we don't have any collection items no worries.
<div id="assets">
@for(var i=0; i< Model.Count; i++) {
@Html.EditorFor(m=>Model[i], "Asset", ViewData);
}
</div>
//This is the magical template that is hidden in the DOM ready at a moments notice to do what we tell it.
<div class="hidden">
<script type="text/html" id="assetTemplateItem">
@{
Html.RenderPartial("_AssetEditorPartial", new Domain.Models.Themes.Asset(), this.ViewData);
}
</script>
</div>
//Here we are hooking up some elements using jQuery selectors.
<script type="text/javascript">
DOMINOPROV.AddToCollectionOnClick('#AddAsset', '#assets', '#assetTemplateItem');
DOMINOPROV.RemoveCollectionItemOnClick('div', '.removeAssetBtn', '#assets');
</script>
//This is our top level view that pulls in and utilizes the Assets EditorTemplate
@model Provisioner.ViewModels.AddAssetsViewModel
@{
ViewBag.Title = "Create Assets";
}
<h2>Create</h2>
@{
Html.EnableClientValidation();
}
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
<div id="assetsForm" class="form-horizontal">
<h4>Assets</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.EditorFor(model => model.Assets, "Assets", new { AssetTypes = Model.AssetTypes })
@Html.ValidationMessageFor(model => model.Assets)
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
var DOMINOPROV = {};
//btnElem: Jquery element selector that will be used to hook into the click event.
//domCollectionWrapperElem: JQuery element selector for element wrapping the element that holds the collection.
//domTemplateItemElem: The item to be copied and used when adding to the collection.
//simpleCollection: Bool, pass true if your using a simple type like int or string in a collection.
/*Relies on jQuery-Template */
DOMINOPROV.AddToCollectionOnClick = function (btnElem, domCollectionWrapperElem, domTemplateItemElem, simpleCollection) {
$(btnElem).click(function () {
var collectionWrapper = $(domCollectionWrapperElem);
var count = collectionWrapper.children().length;
$(collectionWrapper).loadTemplate($(domTemplateItemElem), null,
{ append: true, afterInsert: function (el) { replaceMvcTagNamesForTemplate(count, el, simpleCollection); } });
resetValidatorDynamicContent();
});
}
//staticWrapperElem: Jquery element static element that contains the item with a button.
//btnElem: Jquery element selector that will be used to hook into the click event.
//domCollectionWrapperElem: JQuery element selector for element wrapping the element that holds the collection.
//domTemplateItemElem: The item to be copied and used when adding to the collection.
DOMINOPROV.RemoveCollectionItemOnClick = function (staticWrapperElement, btnClass, parentCollectionDivId) {
$(staticWrapperElement).on('click', btnClass, function () {
$(this).parents('div')[1].remove();
var rows = $(parentCollectionDivId).children();
//ReIndex to not break MVC collection databinding:)
$(rows).each(function (index, pr) {
replaceMvcTagNamesOnDelete(index, pr);
});
});
}
//index: Current collection item index
//el: Element in which to look for and replace tag names
function replaceMvcTagNamesOnDelete(index, el) {
$(el).children().find("*").attr('id', function (i, id) {
if (id !== undefined) {
return id.replace(/_.?._/g, '_' + index + '__');
}
}).attr('name', function (i, name) {
if (name !== undefined) {
return name.replace(/\[.?\]/g, '[' + index + ']');
}
}).attr('data-valmsg-for', function (i, name) {
if (name !== undefined) {
return name.replace(/\[.?\]/g, '[' + index + ']');
}
});
}
//index: Current collection item index
//el: Element in which to look for and replace tag names
function replaceMvcTagNamesForTemplate(index, el, simpleCollection) {
var dotReg = /(\.)/gi;
$(el).children().find("*").attr('id', function (i, id) {
if (id !== undefined) {
var reg = /(_)/gi;
var groups = id.match(reg);
var nth = 0;
if (simpleCollection) {
return name + '_' + index + '_';
}
return id.replace(reg, function (match) {
nth++;
return (nth === groups.length) ? '_' + index + '__' : match;
});
}
}).attr('name', function (i, name) {
if (name !== undefined) {
return replaceIndexedStringPortion(name);
}
}).attr('data-valmsg-for', function (i, name) {
if (name !== undefined) {
return replaceIndexedStringPortion(name);
}
});
function replaceIndexedStringPortion(name) {
var groups = name.match(dotReg);
if (simpleCollection) {
return name + '[' + index + ']';
}
var nth = 0;
return name.replace(dotReg, function (match) {
nth++;
return (nth === groups.length) ? '[' + index + '].' : match;
});
}
}
//Resets the unobtrusive validation
function resetValidatorDynamicContent() {
$('form').removeData("validator").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse($('form'));
}
window.DOMINOPROV = DOMINOPROV;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment