Knockout Demo

Have you ever wanted to dynamically create form elements in your application? Given the trend for SPA's this is an increasingly common use case. As you may have learned from painful experience jquery.validate.unobtrusive.js does not support this by default. jQuery Validation does. Just add a new element to a form and it will be automatically parsed and subsequently validated.

In this example we use Knockout to dynamically add elements to a form. It should be noted this approach is not Knockout specific. Hopefully when I get more time I'll provide some other examples to illustrate.

This demo is "inspired" by a demo on the Knockout site. If you look closely at the View you'll see that the HTML elements in the header and in the foreach binding are driven from the View's Model, in this case the PersonModel class. We just make use of the htmlAttributes parameter to set up the KO databinding.

First name Last name

Here's the model, note that the Required attribute decorates each property of the model:

    using System.ComponentModel.DataAnnotations;

    namespace jQuery.Validation.Unobtrusive.Native.Demos.Models
    {
        public class PersonModel
        {
            [Display(Name = "First name"), 
             Required]
            public string FirstName { get; set; }

            [Display(Name = "Last name"), 
             Required]
            public string LastName { get; set; }
        }
    }
        

Here's the view (which uses the model):

    @model jQuery.Validation.Unobtrusive.Native.Demos.Models.RequiredModel
    @using (Html.BeginForm())
    {
        <div class="row">
            <table>
                <thead>
                    <tr>
                        <th>@Html.DisplayNameFor(x => x.FirstName)</th>
                        <th>@Html.DisplayNameFor(x => x.LastName)</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-bind="foreach: people">
                    <tr>
                        <td>@Html.TextBoxFor(x => x.FirstName, true, htmlAttributes: new { 
                                             data_bind = "value: FirstName, " +
                                                         "attr: { name: 'FirstName' + $index(), id: 'FirstName' + $index() }" })</td>
                        <td>@Html.TextBoxFor(x => x.LastName, true, htmlAttributes: new { 
                                             data_bind = "value: LastName, " +
                                                         "attr: { name: 'LastName' + $index(), id: 'LastName' + $index() }" })</td>
                        <td><button type="button" class="btn btn-link" data-bind="click: $root.removePerson">
                                Remove person</button></td>
                    </tr>
                </tbody>
            </table>
        </div>

        <div class="row">
            <button type="button" class="btn btn-primary" data-bind="click: addPerson">Add person</button>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>
    }
        

Here's the HTML that the view generates:

    <form action="/Demo/Dynamic" method="post">
        <div class="row">
            <table>
                <thead>
                    <tr>
                        <th>First name</th>
                        <th>Last name</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-bind="foreach: people">
                    <tr>
                        <td><input data-bind="value: FirstName, attr: { name: 'FirstName' + $index(), id: 'FirstName' + $index() }"                          
                                   data-msg-required="The First name field is required." 
                                   data-rule-required="true" 
                                   id="FirstName" name="FirstName" type="text" value="" /></td>
                        <td><input data-bind="value: LastName, attr: { name: 'LastName' + $index(), id: 'LastName' + $index() }" 
                                   data-msg-required="The Last name field is required." 
                                   data-rule-required="true" 
                                   id="LastName" name="LastName" type="text" value="" /></td>
                        <td><button type="button" class="btn btn-link" data-bind="click: $root.removePerson">Remove person</button></td>
                    </tr>
                </tbody>
            </table>
        </div>
        <div class="row">
            <button type="button" class="btn btn-primary" data-bind="click: addPerson">Add person</button>
            <button type="submit" class="btn btn-default">Submit</button>
        </div>
    </form>
        

    // The equivalent of the PersonModel class
    function PersonModel(firstName, lastName) {
        this.FirstName = ko.observable(firstName);
        this.LastName = ko.observable(lastName);
    }

    // ViewModel for screen
    function ViewModel($) {
        var self = this;

        self.people = ko.observableArray([
            new PersonModel("Bert", "Bertington"),
            new PersonModel("Charles", "Charlesforth"),
            new PersonModel("Denise", "Dentiste")
        ]);

        self.addPerson = function(viewModel, event) {
            self.people.push(new PersonModel("", "")); // Add a new person with no name
        };

        self.removePerson = function(person, event) {
            self.people.remove(person);
        };

        self.save = function(form) {
            alert("This is a valid form! These are the people: " + ko.toJSON(self.people));
        };

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

    ko.applyBindings(new ViewModel(jQuery));