Code Buckets

Buckets of code

.Net

Handlerbars.js and Working with Complex Objects in MVC

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.

authors-correct-display

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

authors-with-add-button

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

  1. The authors name as inputted by the user
  2. 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.

author-successfully-added

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/

 

LEAVE A RESPONSE

Your email address will not be published. Required fields are marked *