In a previous post I used an EditorFor template to display child objects correctly on a view. Now we are going to use some simple JavaScript templating to add child objects. We have a page display book details and want to add and save additional authors.
Demo app
The demo app is a simple bookshelf application. It has a view that uses EditorFor templates to display a book which we want to save. The book can have multiple authors. It is a complex object.
public class Book { public int? BookID { get; set; } //.. lots of other properties public List<Author> Authors { get; set; } } public class Author { public int AuthorID { get; set; } public string DisplayName { get; set; } }
The book is rendered onto the Book.cshtml view and the user sees a book then can be edited and saved as required.
The authors are rendered in this format
<input id="Authors_0__AuthorID" name="Authors[0].AuthorID" type="hidden" value="0"> <input id="Authors_0__DisplayName" name="Authors[0].DisplayName" type="text" value="Adam Freeman"> <input id="Authors_1__AuthorID" name="Authors[1].AuthorID" type="hidden" value="0"> <input id="Authors_1__DisplayName" name="Authors[1].DisplayName" type="text" value="Matthew MacDonald">
When this form is posted back, the default model binder in MVC uses the name attribute to correctly bind the authors as a child object of the book object. So we get a correctly populated book object passed through to the controller action.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Book book) {
The book parameter is populated will the correct authors. Nice. In general, to work with child objects the correct format for the name attribute in the HTML form element is ..
name="PropertyName[index].ChildPropertyName"
We’ll need to bear that in mind when we are creating a control to add new authors.
The Problem
The authors display correctly and work well but what if we want to add additional authors to the book before saving? Perhaps the book is missing authors. Perhaps I’m a rampant egotist and want to add myself as an author to every book in my virtual bookshelf. Let’s use Handlerbars.js and some JQuery goodness to do this.
The Solution
Handlebars.js
Handlebars.js is one of many JavaScript templating engines that bind fragments of HTML and produce data driven templates. Handlebars takes a template such as
<div class="entry"> <h1>{{title}}</h1> </div>
And binds it against some JSON
{ title: "My great page" }
To produce HTML
<div class="entry"> <h1>My great page</h1> </div>
That’s going to be good for us. We’ve got some pretty chunky fragments of HTML and we want to work with it in the cleanest way we can.
Integrating Handlerbars.js into MVC
Install the package using nuget
Install-Package Handlebars.js
In it goes to the application. Now create a script bundle in BundleConfig.cs
bundles.Add(new ScriptBundle("~/bundles/handlebars").Include("~/Scripts/handlebars.js"));
and reference that bundle in the view
@Scripts.Render("~/bundles/handlebars")
Creating the HandleBar template
Let’s dive straight in and create the template. We want a HTML fragment that will generate us a new author. HandlerBar templates need to be in script tags with the type
text/x-handlebars-template
our full template is
<script id="author-template" type="text/x-handlebars-template"> <div class="form-group shelf-author"> <div class="col-md-10"> <input id="Authors_{{index}}__AuthorID" name="Authors[{{index}}].AuthorID" type="hidden" value="0"> <input class="form-control text-box single-line" id="Authors_{{index}}__DisplayName" name="Authors[{{index}}].DisplayName" type="text" value="{{authorName}}"> </div> </div> </script>
This gives us a hidden field for the AuthorID and a text field for the author name. The curly brackets tell us where the holes in the template are so
{{authorName}} [{{index}}
Are the two pieces of data that the template needs. Handlebars will replace these with the data we supply it.
Constructing the Author add control
Now we need a HTML control to add the author. We want something like this
So a textbox and a button with a squirt of CSS
div class="col-md-4 col-md-offset-2 form-inline"> @Html.TextBox("txtNewAuthor", null, new { @class = "form-control" }) <input type="button" id="btnAddAuthor" value="Add" class="btn btn-primary" /> </div>
Let’s assume that JQuery is already reference and available. Next job is to wire up the click event to the button
$(document).ready(function () { $('#btnAddAuthor').on('click', function () { //.. templating code will go here }); });
Which is standard stuff.
Obtaining the data for the template
Next we need to construct the JSON data that Handlebars will use to generate the HTML. We need two pieces of information
- The authors name as inputted by the user
- The next index of the author elements
Grabbing the author name is straightforward
var authorName = $('#txtNewAuthor').val();
However to get the next index we need find the current highest index. First we need to navigate the DOM to find the last author. The authors are all wrapped in the ‘shelf-authors’ class to facilitate this
var lastAuthor = $('.shelf-author:last');
then we want the name attribute
var lastAuthorFormName = $(lastAuthor).find(':text').attr('name');
Then we want the last index. If there are 3 authors then lastAuthorFormName will contain
Authors[2].DisplayName
So some regex the current index i.e. the number in square brackets
lastAuthorFormName.match(/\[(\d*)\]/)
The entire regex matches “[2]” and the first and only group (\d*) matches the digit itself. The output for this is an array. The first element of the array is the full match. All subsequent elements are the matches for the containing groups (defined in regex by parenthesis). So
lastAuthorFormName.match(/\[(\d*)\]/)[0]
would equal “[2]” and
lastAuthorFormName.match(/\[(\d*)\]/)[1]
would equal “2”. So to get the next index we need
var index = parseInt(lastAuthorFormName.match(/\[(\d*)\]/)[1]) + 1;
Now we have all the information we need to construct the data for the handlebar template
var data = { index: index, authorName: authorName };
Gluing the template together
Gluing the data into the template is pretty much boilerplate code
var source = $("#author-template").html(); var template = Handlebars.compile(source); var renderedHTML = template(data)
Grab the template with a CSS selector then compile and combine. The HTML is now correct and can be inserted into the DOM
The Complete Solution
So combining all the steps together we get
@Scripts.Render("~/bundles/handlebars") <script id="author-template" type="text/x-handlebars-template"> <div class="form-group shelf-author"> <div class="col-md-10"> <input id="Authors_{{index}}__AuthorID" name="Authors[{{index}}].AuthorID" type="hidden" value="0"> <input class="form-control text-box single-line" id="Authors_{{index}}__DisplayName" name="Authors[{{index}}].DisplayName" type="text" value="{{authorName}}"> </div> </div> </script> <script type="text/javascript"> $(document).ready(function () { $('#btnAddAuthor').on('click', function () { //..don’t insert blank authors if ($('#txtNewAuthor').val() == '') return; //.. get the author name var authorName = $('#txtNewAuthor').val(); //.. get the index var lastAuthor = $('.shelf-author:last'); var lastAuthorFormName = $(lastAuthor).find(':text').attr('name'); var index = parseInt(lastAuthorFormName.match(/\[(\d*)\]/)[1]) + 1; //.. format data as JSON var data = { index: index, authorName: authorName }; //.. combine data with template var source = $("#author-template").html(); var template = Handlebars.compile(source); //.. insert new author HTML into DOM lastAuthor.after(template(data)); //.. clear the input textbox $('#txtNewAuthor').val(''); }); }); </script>
The HTML element is now correct and when this is posted back the new author will be present in the action method i.e.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Book book) {
The book parameter will have a fourth author which will be saved.
So I can now add myself as an author to every book in my library which goes some way into feeding my monstrous and uncontrolled ego – which is what it’s all about really.
Useful Links
Project page for handlebars
http://handlebarsjs.com/
Nuget page for handlebars
https://www.nuget.org/packages/Handlebars.js/
Demo application source
https://github.com/timbrownls20/BookShelf/