Site Meter
 
 

Editing a variable-length list, Knockout-style

Following my previous blog post about Knockout, a JavaScript UI library that provides a nice structure for building responsive web UIs with a similar programming model to Silverlight, a few people have asked to see an “editable grid”-type example. There’s a read-only paging grid example here, but what about editing and validating?

Previously I wrote about editing and validating variable-length lists with ASP.NET MVC 2, and before that with ASP.NET MVC 1. Those demos were OK, but it’s way easier with Knockout – it eliminates the most of the complex moving parts and the fiddly client-server interaction, the precise element naming requirements, and the hackery needed to make validation work.

Note: Knockout works with any server-side web development framework,
but this blog post focuses on using it with ASP.NET MVC

Live demo

Before we get into the code, see what we’re about to build. It’s simple, but nice to use. Try it out in this live IFRAME:


Note: Some RSS readers will strip out the preceding IFRAME. If you don’t see the live demo, see this post on the web instead.

Right, how’s it done?

Sending some initial state down to the client

First, define a C# model for the data items we’re editing:

public class GiftModel
{
    public string Title { get; set; }
    public double Price { get; set; }
}

Next, create an action method that renders a view, passing some GiftModel instances to it:

public ActionResult Index()
{
    var initialState = new[] {
        new GiftModel { Title = "Tall Hat", Price = 49.95 },
        new GiftModel { Title = "Long Cloak", Price = 78.25 }
    };
    return View(initialState);
}

… and then, in the view, convert that C# model data into a JavaScript object:

<script type="text/javascript">
    var initialData = <%= new JavaScriptSerializer().Serialize(Model) %>;
</script>

Creating a Client-side View Model

Right now our view model only needs one property – gifts – which will be an observable array of gift items. Once we’ve got that, we can tell Knockout to bind this to any HTML nodes that request binding:

var viewModel = {
    gifts : ko.observableArray(initialData)
};
 
ko.applyBindings(document.body, viewModel);

To check this is all working, let’s just bind a SPAN’s text content to the length of the observable array:

<p>You have asked for <span data-bind="text: gifts().length">&nbsp;</span> gift(s)</p>

Because gifts is an observable array, the text in this SPAN will be updated automatically whenever the length of the array changes. With all this in place, you should now see the following:

image 

Making an editable grid

Making data editable is a matter of creating HTML input controls and binding them to that data, and the easiest way to create repeated blocks of markup is to use a template. So, here’s how to make a table containing an editor row for each gift item:

<table>
    <tbody data-bind="template: { name: 'giftRowTemplate', foreach: gifts }"></tbody>
</table>
 
<script type="text/html" id="giftRowTemplate">
    <tr>
        <td>Gift name: <input data-bind="value: Title"/></td>
        <td>Price: \$ <input data-bind="value: Price"/></td>
    </tr>
</script>

This will produce the following output:

image

Now, if you edit the contents of the text boxes, it will actually edit the underlying data model. You can’t really see any effect of that just yet, but you will be able to when we save the data.

Adding gifts

Adding items just means adding them to the gifts observable array in the underlying view model; the UI will take care of updating itself. So, create a button that triggers an ‘add’ method on the view model:

<button data-bind="click: addGift">Add Gift</button>

… and then implement such a method, pushing a new blank item into the array:

var viewModel = {
 
    // ... leave the rest as before ...
    addGift: function () {
        this.gifts.push({ Title: "", Price: "" });
    }
 
};

That does it.

Removing gifts

Again, you only need to edit the underlying array. So, edit the giftRowTemplate and put a ‘Delete’ link on each row:

<script type="text/html" id="giftRowTemplate">
    <tr>
        <!-- Leave the rest as before -->
        <td><a href="#" data-bind="click: function() { viewModel.removeGift($data) }">Delete</a></td>
    </tr>
</script>

… and implement a removeGift method on the view model:

var viewModel = {
    // ... leave the rest as before ...
 
    removeGift: function (gift) {
        this.gifts.remove(gift);
    }
 
};

Now, when you click ‘delete’ on any item, it will vanish from both the underlying data model and the HTML UI.

Saving data (i.e., submitting it back to the server)

Since we’re following the MVVM pattern, all our really important data is in the view model – the view (the HTML UI) is merely a representation of that. So, we don’t want to submit data from the view by doing a regular form post. Instead, we want to submit the state of the view model as this may contain more data than is visible to the user.

First, we need to know when the user wants to save their data. To do this, we can wrap up our UI inside a regular HTML <form> with a submit button, and then bind the ‘submit’ event to a method on the view model. (Doing it this way preserves familiar behaviours like pressing “Enter” to submit a form)

<form class="giftListEditor" data-bind="submit: save">
    <!-- Our other UI elements, including the table and ‘add’ button, go here -->
 
    <button data-bind="enable: gifts().length > 0" type="submit">Submit</button>
</form>

Notice that I’ve made the “submit” button enabled only when there’s at least one item. Next, add a ‘save’ method that packages up the view model state as JSON and posts it to the server:

var viewModel = {
 
    // ... leave the rest as before ...
    save: function() {
        ko.utils.postJson(location.href, { gifts: this.gifts });
    }
};

It would be perfectly easy to submit the data via Ajax using jQuery’s $.post or $.ajax methods, but I’ve chosen to use postJson (a utility function inside Knockout) to send the data as a regular HTML form submission instead. That means the server-side code doesn’t have to know anything about Ajax and can imagine you’re posting a normal HTML form. As I said, Ajax works fine too – I’m just demonstrating another option here.

The view model posts the data to the same URL you’re already on (i.e., location.href) because that’s ASP.NET MVC’s convention. To collect the data on the server, add an action method as follows:

[HttpPost]
public ActionResult Index([FromJson] IEnumerable<giftModel> gifts)
{
    // Can process the data any way we want here,
    // e.g., further server-side validation, save to database, etc
    return View("Saved", gifts);
}

That’s pretty easy! This fits into ASP.NET MVC’s usual data-entry conventions – the server-side code is totally trivial. It doesn’t use ASP.NET MVC’s usual data binding mechanism so you don’t have to worry about element naming, field prefixes, and all that; instead, the data is packaged as JSON and deserialized using [FromJson]. I’ll omit the source code to [FromJson] for now, but it’s totally general-purpose, only about 10 lines long, and is included in the downloadable example with this blog post.

The server-side code can now do whatever it wants. For this example, I’m just rendering a different view to show what data the server received.

Adding Validation

Right now, somebody could enter a text string for “Price”, and then this will cause a deserialization error on the server. This is an abomination, and must be stopped! Let’s add some client-side validation.

Knockout works nicely with any client-side validation framework that can cope with you dynamically modifying the DOM. jQuery.Validation copes perfectly with that, so let’s use it. Having referenced jquery.validate.js, define rules by putting special CSS classes on the elements in the template. (Note that jQuery.Validation lets you define custom rule logic in this way, too.)

<script type="text/html" id="giftRowTemplate">
    <tr>
        <td>Gift name: <input class="required" data-bind="value: Title, uniqueName: true"/></td>
        <td>Price: \$ <input class="required number" data-bind="value: Price, uniqueName: true"/></td>
        <td><a href="#" data-bind="click: function() { viewModel.removeGift($data) }">Delete</a></td>
    </tr>
</script>

Notice the “required” and “number” classes. One other thing to bear in mind is that, although Knockout doesn’t need your bound elements to have names or IDs, jQuery.Validation does depend on every validated element having a unique name. That’s why I’ve put uniqueName:true into the bindings. This is a trivial binding I added that just checks whether the element has a name, and if not, gives it a unique one. An easy workaround for the jQuery.Validation limitation.

Next, remove the “submit” binding from the form, and tell jQuery.Validation to catch the form’s submit event instead, and to call our view model’s save method only if the form is valid:

$("form").validate({ submitHandler: function() { viewModel.save() } });

That’s it! Now the user can’t submit blank fields, nor can they submit text for the “Price” fields. They can still add and remove items, though, and this doesn’t affect the state of the validation feedback for other rows in the table.

Of course you can still validate the data on the server, too – and you need to if blank data would actually violate your business rules in some important way. Typically I’d recommend putting such logic into your domain layer, to ensure the rules are always respected no matter what UI technology (ASP.NET MVC, Knockout, Silverlight, an iPhone app) is connected to it. To display any errors, you can use something like an ASP.NET MVC “ValidationSummary” helper. (Or you can just throw an exception and give up if it looks like the user is deliberately bypassing client-side validation – it’s up to you.)

There’s plenty more I could describe here, such as how to use bindings to add animated transitions (e.g., applying jQuery’s fadeIn or slideUp when users add or remove items), but I think this is enough for now.

Questions and support

I’ve had plenty of questions about how to do things with Knockout since my blog post last week. To capture these discussions and make them public, I’ve made a Google Group for Knockout at http://groups.google.com/group/knockoutjs – please use this for general questions about the framework. However if you have comments about the particular example shown in this post, go ahead and post them here on this blog.

If you want to play with the gift list editor code a little more, download the demo project.

50 Responses to Editing a variable-length list, Knockout-style

  1. Steve Owen

    Hi Steve. Another superb post, very well and succinctly presented, and a brilliant library. Thanks so much. I fully intend to use it on our new project.

  2. bennyb

    Brilliant post.

  3. Steve Gentile

    Thanks Steve – looks fantastic – glad you setup the Google Group

  4. Igor Loginov

    It’s a really good solution. Thank you.

    Could you, please, provide an example of templated editable grid with drop-down list containing both texts and values, and with check boxes in grid cells? This would be very helpful.

  5. Yura

    Steve, how about HTML validation? The “data-bind” is not valid HTML attribute.

  6. Steve

    Jon, Igor – there are a bunch of additional example of using Knockout at http://knockoutjs.com/examples/. Regarding the specific examples you’ve asked for, I’ll see if I can find time to put something together.

    Yura – that is certainly a worthwhile question, and I did give this careful consideration when designing Knockout. My view is that since it’s valid in HTML5, and in the meantime, it’s really useful, works on all mainstream browsers and devices, and doesn’t break anything, I find the benefits to be well worth it. I have never yet worked for a client who insisted on getting no warnings from an HTML spec validator, so I don’t see any practical problem with it for the considerable majority of projects. If you are working for someone who for whatever artificial reason won’t accept this then I guess you can’t make use of the library, but I think that would be extremely rare in practice.

  7. Mickey

    Hi, Steve!
    First of all – great library!!!

    I have a question:

    I want to implement some kind of “master-details” UI. For example, I have a folder->files array that I want to display as 2 , – clicking on specific folder, fills files’ with chosen folder’s files.
    For now I have my data:
    var folderStructure = [
    { id: 1, name: "Folder1", files: [
    { name: "File1-1" }, { name: "File1-2"}, { name: "File1-3" }
    ] },
    { id: 2, name: “Folder2″, files: [
    { name: "File2-1" }, { name: "File2-2" }
    ] },
    { id: 3, name: “Folder3″, files: [
    { name: "File3-1" }, { name: "File3-2" }, { name: "File3-3" },
    { name: "File3-4" }
    ] }
    ];

    Folder template:

    (${ id }) ${ name }

    Files template:

    ${ name }

    The :
    Folders List:

    I get this to work with this code:
    var viewModel1 = {};

    $(function () {
    viewModel1 = {
    folders: ko.observableArray(folderStructure),
    selectedFolder: ko.observable(),
    showItems: function (currentFolder) {
    this.selectedFolder(currentFolder);
    this.selectedFolderFiles(this.selectedFolder().files);
    },
    selectedFolderFiles: ko.observableArray([])
    };
    ko.applyBindings($(“#forViewModel1″)[0], viewModel1);
    });

    But in my ViewModel there is “selectedFolderFiles” that takes its data from the “selectedFolder” object.

    I would like to use only “selectedFolder” and bind it to 2 ULs, so the first (Folders) UL stays as is and the second has as “foreach” something like “selectedFolder.files”.

    Is there a way to do it?

    Thank you.

  8. Mickey

    Hi, again

    I see that all my HTML is gone… how do I add it?

    folders template:
    <script type=”text/html” id=”folderTemplate”>
    <li id=”${ id }”><a href=”#” data-bind=”click: function() { viewModel1.showItems($data); } “>(${ id }) ${ name }</a></li>
    </script>

    files template:
    <script type=”text/html” id=”fileTemplate”>
    <li>${ name }</li>
    </script>

    folders UL:
    <ul id=”folderList” data-bind=”template: { name: ‘folderTemplate’, foreach: folders }”></ul>

    files UL:
    <ul id=”fileList” data-bind=”template: { name: ‘fileTemplate’, foreach: selectedFolderFiles }”></ul>

  9. Jon Hilton

    Hi Steve,

    Just a thought but might it be worth adding a link to http://groups.google.com/group/knockoutjs on http://knockoutjs.com/ for those people who don’t stumble across this post?

    Great work by the way, just built a quick prototype using knockout and so far it’s working really well.

  10. Pingback: Knockout 1.1.0 + new project site launched « Steve Sanderson’s blog

  11. Mike

    Simply brilliant Steve – I love ASP MVC and Silverlight and now that (with your great Knockout library) bring MVVM to JS and HTML, I love that too :)

  12. Daniel WIlliams

    This is a great example. On the main site knockoutjs.com, I did not see any good examples of posting back with the modified viewmodel data- but this is a critical piece.

  13. Daniel WIlliams

    In this code example there is validation on the price to make sure it is numeric. Yet I can still hit Submit with an invalid number. Is there a way to tie the submit button’s enabled state to the presence or absence of validation messages?

  14. Pete

    For those like me still stuck with .Net 3.5 the code from the [FromJson] attribute won’t work as there isn’t an overload on JavaScriptSerializer.Deserialize that takes a type parameter.

    I managed to get it to work using the following (that needs references to System.ServiceModel.Web and System.Runtime.Serialization) but is there a neater way?

    private class JsonModelBinder : IModelBinder
    {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
    var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
    if (string.IsNullOrEmpty(stringified))
    return null;
    var serializer = new DataContractJsonSerializer(bindingContext.ModelType);
    var ms = new MemoryStream(Encoding.Unicode.GetBytes(stringified));
    return serializer.ReadObject(ms);
    }
    }

  15. How would you modify this example if you needed one of the rows to be the “default” row, represented by radio buttons?

  16. Pingback: Binding DateTime to knockout view model with default JavaScriptSerializer | DEEP in PHP

  17. Steve

    1. Bug alert!:

    There is a “workflow bug” in the grid: Say the page gets loaded with one gift. If the user then deletes the gift, then gifts.Length == 0 and the Submit button is disabled, ie, the User cannot delete the gift.

    I got around this by doing:

    0) || (@Model.gifts.Count() > 0))” type=”submit”>Submit

    Ie, by using the original count of the objects in gifts.

    ———

    2. One row at a time…?:

    On another level, I found the code needed to deal with Linq to SQL on the server quite difficult to write even for a simple case.

    Do you have any plans to produce a KO grid that allows you to edit or delete one line at a time? Ie, the “usual” way of selecting a row, then editing it. Or else adding a new row.

  18. Sorry, typo with the fix:

    data-bind=”enable: ((stepLinks().length > 0) || (@Model.StepLinks.Count() > 0))”

    I also forgot to say how I like KO in general.

  19. Great article. Just one question, for accessibility I’d prefer to have the HTML in there also so I can deal with assistive tech or people who don’t have JS enabled. Do you know if KO support’s this?

  20. Muneeb

    @Glyn: KnockoutJs is all Javascript. No JS = No KnockoutJs.

  21. Steve

    While it’s certainly true that KO requires JavaScript, it is still possible to support screen readers using standards such as ARIA (http://www.w3.org/TR/wai-aria-roadmap/)

  22. Chase

    Hi, I love the article you alude to the ability to use the built in validation with mvc using the Validation summary But I cannot seem to make this work. Would you mind posting an example?

  23. Sam

    Steve,

    I am working on a datagrid with knockout.js whose rows have text boxes. Now I want to add a dropdown but data binding does not seem to work.

    On this line, I am getting a knockout error saying that ‘widgets’ are undefined.

    If I place the dropdown outside of a row template, it works – which suggests that I cannot do this unless there is a trick or a workaround for data binding inside of a row template. Thank you in advance.

    Here is my row template

    Delete

  24. Daniel

    Hi, maybe i missed it, but i tried for hours to make it work but it just didn’t do anything. After a lot try and error I found the error. (Should have debugged the js earlier…)
    Since Version 1.05 there has been a breaking change.
    ko.applyBindings now gets the ViewModel as first parameter and the DOM node as second parameter.
    Instead of:
    ko.applyBindings(document.body, viewModel);
    You need to write
    ko.applyBindings(viewModel, document.body);
    now.
    Hope this helps.

  25. adrian

    Hi, Great post,

    I want to implement a master- details view on one form. I have used a normal form to implement the master section and i have implemented a variation of the cart editor to be the ‘master view’ where i can add multiple products.

    now the problem i am having is how can i submit my normal model, containing the master section and the details model made up of json data, which i can the process on the server side? is this possible i do i need to convert the whole model to JSON?

  26. Adam Webber

    A small tip for jQuery integration:
    If you want to enclose the javascript in a jQuery ready guard clause eg. $(function () {..}); then the delete link stops working. To fix this, declare the viewModel outside the jQuery ready clause. eg. var viewModel = {};

  27. Hi Steven,

    Any hints on the changes required to use the MVC3 Razor View Engine?

    Using just the minimal code to create the first view (binding 2 gifts) worked perfectly in MVC2, with jquery-1.4.2.js and knockout-1.01.js. Changing to jquery-1.4.4.js continued to work properly. However, moving to MVC3 with the same script files allowed the view to render but did not bind the data.

    Would you advise how to pass the model to the script in razor?

    The line “var initialData = ” appears to be the issue and most likely there is a simple conversion – something we all need to learn.

    Thanks,

    Greg

  28. Dariel

    Greg, I was able to get it working using the following:

    @{
    string data = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(Model);
    }

    And then specify the initialData like this:

    var initialData = @Html.Raw(data);

    How do you get validation working using data-val? Is it because the HTML isn’t part of the DOM?

    I’d like to be able to specify min, max length and do something like:

  29. Dariel

    Looks like my example got cut off:

    input id=”Value”
    class=”text-box single-line”
    data-val=”true”
    data-val-length=”minimum length of 5 and a maximum length of 10.”
    data-val-length-max=”10″
    data-val-length-min=”5″
    data-val-required=”The Name field is required.”
    name=”Value”
    type=”text”
    value=”"

  30. James

    Hi Steve,
    Are there any alternatives to using the FromJson attribute? Perhaps a way to plug in a utility to convert the JSON to something more friendly to the MVC action method?

    I ran across this blog post showing a jQuery addIn that converts JSON into something ASP.Net MVC’s DefaultModelBinder can handle directly (http://erraticdev.blogspot.com/2010/12/sending-complex-json-objects-to-aspnet.html), which seemed interesting, but I’m not sure if there’s a way to get Knockout to use this approach.

    Also, what guidance would you offer for separating JavaScript code from my MVC Views (i.e., like the unobtrusive jQuery approach)?

    -James

  31. Adrian Hedleu

    Hi Dariel,

    I have tried your fix for MVC3 Razor but it did not work out for me.

    I am getting the data from the model fine and i van see the initialData variable populated using firebug, but the the data is not binding.

    Can you please post some code.. perhaps i am missing something here.

    thanks in advance.

  32. Hey how are you doing 2day? I really liked your blog. Hope you keep working on it. Have a awesome night :D

  33. Brian Kirkland

    Arian,
    I was able to get this working on MVC3, but not with the latest jQuery. Try using jquery.1.4.4. Also, initialData should be populated as follows:

    var initialData = @Html.Raw(Json.Encode(Model));

    Then setup your viewModel using ko.observableArray on initialData.

    Hope this helps. I posted a ? on StackOverflow for this: http://stackoverflow.com/questions/5873636/how-to-enable-data-binding-in-knockoutjs-using-the-asp-net-mvc-3-razor-view-eng

  34. I liked the knockout.js
    However I have one question. We are binding the events to the DOM elements using data-bind property. Is it not breaking the priniciple of UNOBTRUSIVE JavaScript Code?

    Like in jQuery we use to bind the events without writing anything on DOM Element. By just selecting the element and using bind method.

    I think you should encourage reader or user of knockout.js to use jQuery Event Binding rather writing it in data-bind property in order to adhere the principle of
    UNOBTRUSIVE javascript.

  35. moldyseaswimmer

    use this to avoid &quot in your JSON
    var initialData = “@Html.Raw( new JavaScriptSerializer().Serialize(Model))”;

  36. moldyseaswimmer

    for anyone else using ‘knockoutjs 1.2.1′
    actually that should be var initialData = @Html.Raw( new JavaScriptSerializer().Serialize(Model));

    i.e. no double quotes

    and you also need use ko.applyBindings(viewModel, document.body); instead of ko.applyBindings(document.body, viewModel);

  37. Renso

    When using jQuery validation runner the drop down list of complex objects in an observableArray causes the “value” element to be empty and only has a text value in the options tag of the select html tag. So when the user selects an option the Knockout value is correctly being given the complex object that I then parse to post back to the server (I parse the Id out of it), BUT, the validationrunner plugin fails validation and insists I did not select anything from that drop down box and I’m assuming it is because the value property of the options tag is empty

    Sample of the SELECT tag:

    …(deprecated) (1 – 11)StoreFront Package (1 – 2)StoreFront Package (12 – 14)StoreFront Package (12 – 36)StoreFront Package (15 – 17)

    I tested this by loading the drop down with $.ajaxOptions (just a way to load options for a drop down dynamically, not really important just so that you know how I load it), and then the validation works just fine. Has anyone experienced this?

  38. FR

    Excellent post.

    Thank you very much.

    (Knockout.js rules)

  39. Many thanks for a great article (and library too!). Just to point out a “gotcha” for anyone using this with the latest version (2) of knockout, i.e. knockout-2.0.0.js rather than knockout-1.01.js which is the version in the download code. The order of the parameters in ko.applyBindings has changed so that the model comes first:

    // in Knockout 2.0.0 the order of the parameters has changed, the model now comes first with the html element second (found via using Inspect element, Scripts in Gooole Chrome)
    //ko.applyBindings(document.body, viewModel);
    ko.applyBindings(viewModel, document.body);

    After that change, all worked fine!
    Thanks again for all your work on Knockout and this blog.

  40. Kaushik

    Hi in the code the way bindings have been applied are as

    ko.applyBindings(document.body, viewModel);

    But the documentation here (http://knockoutjs.com/documentation/observables.html#mvvm_and_view_models) is confusing me , why have you swapped the param positions?

    document says : “Optionally, you can pass a second parameter to define which part of the document you want to search for data-bind attributes.”

  41. Kaushik

    I found my answer it was in @Patrick Lee ‘s Comment. Thanks.

    Thanks Steve.

  42. Cheeta

    For those trying to port this app to mvc 3: don’t forget to add a reference to jquery.validate.js in _Layout.cshtml.

    If you don’t: the $(“form”).validate() method will be undefined, and thus the submitHandler will not be ‘bound’ to the viewModel.save() method.

    And then you will get a ‘normal’ form-post, which sends the wrong object to the server…

  43. dominic

    Thanks mate this cleared a lot of stuff up in a nice and clean way with examples that didn’t bore me

  44. Ervimar

    Hey Steve, great post and great framework, Congratz!!!
    Please could you post an example like this without using MVC but Webforms instead?
    Thanks a lot!

  45. This is really interesting, Youre a very skilled blogger. Ive joined your feed and appear forward to seeking much more of your magnificent post. Also, Concerning shared your blog around my myspace!

  46. YouTube was began in 2005 by 3 PayPal employees furthermore, as then YouTube has expanded to among the largest websites on the planet. Youtube Currently has over 1 billion page views every thirty days Well as over 1 Billion Distinctive Page Views Per month.

  47. Anything on the wise is plenty

  48. Howdy! This is certainly form of off topic however i take some advice from a recognized blog. Could it be not easy to setup your blog? Im not very techincal however can figure things out pretty quick. Im contemplating creating my own, personal but Im undecided the place to start. Have any tips or suggestions? Cheers

  49. Hi Steve

    Give me example add,edit,update and delete the data in database and grid view….