Creating HTML for a MVC Bindable List<T> Dynamically with Knockout.js

January 02, 2015

One of the great benefits of knockout.js is adding items client-side – especially for tables. Knockout and jQuery also already have great support for sending that data back to the server via a GET/POST. However, what if I want to do a form submit and send all of that data to the server? Maybe you are doing something like rendering a pdf or something that you don’t want to have to handle in javascript?

In this post, we will go over how to have Knockout generate the correct HTML so when you POST MVC’s model binding it will pick it up for you. Keeping javascript in sync is always a pain and error prone – especially between the markup and the javascript file. This post assumes you already have understanding of how to use Knockout.js and MVC. Inline editing of the table is out of scope for this post.

Our scenario is allowing the user to add Employees client side. Upon adding an Employee object, it will add a new table row with the corresponding HTML. Let’s start with our HTML.

The table:

   1: <table id="employeeList">
   2:     <thead>
   3:         <tr class="">
   4:             <td style="width:1px"></td>
   5:             <th>
   6:                 Name
   7:             </th>
   8:         </tr>
   9:     </thead>
  10:     <tbody data-bind="foreach: EmployeeList">
  11:         <tr>
  12:             <td>
  13:                 <input type="hidden" data-bind="value: Attribute.Identifier, attr: { name: Attribute.Index }" />
  14:                 <!-- ko foreach: {data: $root.EmployeeListProperties, as: '_propkey'} -->
  15:                 <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" />
  16:                 <!-- /ko -->
  17:             </td>
  18:             <td data-bind="text: Name"></td>
  19:         </tr>
  20:     </tbody>
  21: </table>

You can additional columns/bindings if you wish. For code simplicity we are only going to show the “Name” column and the rest of the fields will be hidden inputs.

You can see that we have nested for-loops. The outer will loop over each item in the observableArray EmployeeList. The inner will loop over each property we specify and create a hidden input for it. These properties are stored in an array appropriately named EmployeeListProperties. It uses the $parent keyword to get the employee from the outer loop. In javascript, you can access a property on an object using either dot notation (Employee.Name) or array indexing (Employee[“Name”]). This is how we set the value attribute. We set the name attribute a similar way except it is on an Attribute property (Employee.Attribute.Name). All of the name attributes will be on a property called Attribute that we will append to the object. Lets look at that now.

Here is our viewmodel in Knockout:

   1: var self = this;
   2:  
   3: self.ListName = "Employees";
   4: self.EmployeeListProperties =
   5:     ["Id", "EmployeeNumber", "Score", "Notes", "Fail"];
   6:  
   7: self.EmployeeList = ko.observableArray([]);
   8:  
   9: function addItem(itemToAdd) {
  10:     var item = appendMetadata(itemToAdd);
  11:  
  12:     self.EmployeeList.push(item);
  13: }
  14:  
  15: function appendMetadata(employee) {
  16:  
  17:     var identifier = "Identifier_" +
  18:         ko.unwrap(employee.EmployeeNumber).toString();
  19:  
  20:     employee.Attribute = {
  21:         Identifier: identifier,
  22:         Index: self.ListName + ".Index"
  23:     };
  24:  
  25:     var properties = self.EmployeeListProperties;
  26:  
  27:     for (var i = 0; i < properties.length; i++) {
  28:  
  29:         employee.Attribute[properties[i]] =
  30:             createModelBinderHtml(identifier, properties[i]);
  31:     }
  32:  
  33:     return employee;
  34: }
  35:  
  36: function createModelBinderHtml(identifier, field) {
  37:     return self.ListName + '[' + identifier + '].' + field;
  38: }

ListName is parameter/property that we will be binding to in MVC. EmployeeListProperties is the list of properties on the object that we want the model binding to pick up. We could have it dynamically go through every property on the object using Object.keys() and passing the object, but I don’t want to create inputs for any knockout create fields or for nested objects (those won’t bind correctly anyway).

AddItem is the method called from an event handler when we add the employee to the table (outside the scope of this post).

AppendMetadata is where the Attribute property is added to the object and all of the properties in our list is added to Attribute. (To know how MVC model binding works with a list, check out this exellent article from Phil Haack.) First, we need a unique identifier. Normally this would be some integer/ID, but for this example we are using EmployeeNumber which is a string. Even if you are using an integer, you should still prepend some text to it to make it a string to save yourself a LOT of additional work. Maintaining an unbroken sequence is a nightmare. Next, we create our initial object/property for Attribute. The Index property is required if you are not doing sequential numerical indexing so we just add it manually. Then, we loop through our list of properties and add each one. Once we have done all of them, we return the employee object.

CreateModelBinderHtml takes in the unique identifer and the field name and creates the appropriate syntax to put into the name attribute on a Html input.

In our scenario, the correct syntax for the EmployeeNumber field would be:

   1: <input type="hidden" value="11493" name="Employees[Emp_No_11493].EmployeeNumber">

Once this executes, the rendered HTML in our first column is:

   1: <input type="hidden" data-bind="value: Attribute.Identifier, attr: { name: Attribute.Index }" value="Identifier_11493" name="Employees.Index">
   2: <!-- ko foreach: {data: $root.EmployeeListProperties, as: '_propkey'} -->
   3: <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" value="0" name="Employees[Identifier_11493].Id">
   4: <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" value="11493" name="Employees[Identifier_11493].EmployeeNumber">
   5: <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" value="0" name="Employees[Identifier_11493].Score">
   6: <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" value="" name="Employees[Identifier_11493].Notes">
   7: <input type="hidden" data-bind="value: $parent[_propkey], attr: { name: $parent.Attribute[_propkey] }" value="false" name="Employees[Identifier_11493].Fail">
   8: <!-- /ko -->

Assuming that we have a controller that takes an IEnumerable of an Employee object/parameter named Employees that has these properties, then it should model bind it for you when submitting the form.

This approach isn’t for every scenario. Most of the time you can do regular server calls via javascript and that is enough. However, whenever you do need to POST a client-side added table to the server, you’ll be glad that you can copy and paste this code on your page and just change a few variables.

Information and material in our blog posts are provided "as is" with no warranties either expressed or implied. Each post is an individual expression of our Sparkies. Should you identify any such content that is harmful, malicious, sensitive or unnecessary, please contact marketing@sparkhound.com.

Meet Sparkhound

Review our capabilities and services, meet the leadership team, see our valued partnerships, and read about the hardware we've earned.

Engage with us

Get in touch with any of our offices, or check out our open career positions and consider joining Sparkhound's dynamic team.