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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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; }
}
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; } }
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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">
<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">
<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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Book book)
{
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Book book) {
[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 ..

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
name="PropertyName[index].ChildPropertyName"
name="PropertyName[index].ChildPropertyName"
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div class="entry">
<h1>{{title}}</h1>
</div>
<div class="entry"> <h1>{{title}}</h1> </div>
<div class="entry">
   <h1>{{title}}</h1>
 </div>

And binds it against some JSON

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{ title: "My great page" }
{ title: "My great page" }
{ title: "My great page" }

To produce HTML

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div class="entry">
<h1>My great page</h1>
</div>
<div class="entry"> <h1>My great page</h1> </div>
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Install-Package Handlebars.js
Install-Package Handlebars.js
Install-Package Handlebars.js

In it goes to the application. Now create a script bundle in BundleConfig.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
bundles.Add(new ScriptBundle("~/bundles/handlebars").Include("~/Scripts/handlebars.js"));
bundles.Add(new ScriptBundle("~/bundles/handlebars").Include("~/Scripts/handlebars.js"));
bundles.Add(new ScriptBundle("~/bundles/handlebars").Include("~/Scripts/handlebars.js"));

and reference that bundle in the view

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Scripts.Render("~/bundles/handlebars")
@Scripts.Render("~/bundles/handlebars")
@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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
text/x-handlebars-template
text/x-handlebars-template
text/x-handlebars-template

our full template is

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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 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 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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{{authorName}}
[{{index}}
{{authorName}} [{{index}}
{{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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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>
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>
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$(document).ready(function () {
$('#btnAddAuthor').on('click', function () {
//.. templating code will go here
});
});
$(document).ready(function () { $('#btnAddAuthor').on('click', function () { //.. templating code will go here }); });
$(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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var authorName = $('#txtNewAuthor').val();
var authorName = $('#txtNewAuthor').val();
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var lastAuthor = $('.shelf-author:last');
var lastAuthor = $('.shelf-author:last');
var lastAuthor = $('.shelf-author:last');

then we want the name attribute

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var lastAuthorFormName = $(lastAuthor).find(':text').attr('name');
var lastAuthorFormName = $(lastAuthor).find(':text').attr('name');
var lastAuthorFormName = $(lastAuthor).find(':text').attr('name');

Then we want the last index. If there are 3 authors then lastAuthorFormName will contain

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Authors[2].DisplayName
Authors[2].DisplayName
Authors[2].DisplayName

So some regex the current index i.e. the number in square brackets

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
lastAuthorFormName.match(/\[(\d*)\]/)
lastAuthorFormName.match(/\[(\d*)\]/)
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
lastAuthorFormName.match(/\[(\d*)\]/)[0]
lastAuthorFormName.match(/\[(\d*)\]/)[0]
lastAuthorFormName.match(/\[(\d*)\]/)[0]

would equal “[2]” and

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
lastAuthorFormName.match(/\[(\d*)\]/)[1]
lastAuthorFormName.match(/\[(\d*)\]/)[1]
lastAuthorFormName.match(/\[(\d*)\]/)[1]

would equal “2”.  So to get the next index we need

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var index = parseInt(lastAuthorFormName.match(/\[(\d*)\]/)[1]) + 1;
var index = parseInt(lastAuthorFormName.match(/\[(\d*)\]/)[1]) + 1;
var index = parseInt(lastAuthorFormName.match(/\[(\d*)\]/)[1]) + 1;

Now we have all the information we need to construct the data for the handlebar template

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var data = {
index: index,
authorName: authorName
};
var data = { index: index, authorName: authorName };
var data = {
  index: index,
  authorName: authorName
};

Gluing the template together

Gluing the data into the template is pretty much boilerplate code

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var source = $("#author-template").html();
var template = Handlebars.compile(source);
var renderedHTML = template(data)
var source = $("#author-template").html(); var template = Handlebars.compile(source); var renderedHTML = template(data)
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@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>
@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>
  @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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Book book)
{
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Book book) {
[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 *