Wednesday, April 18, 2012

Getting knocked around while dealing with a check box

I've been using Knockoutjs heavily these couple of weeks and I think it has place in my programming toolkit. With Knockout, my markup is significantly cleaner and my pages with form elements are not a pain.

Knockout isn't that hard to use. I bet you can get a good handle of it once you finish some of the tutorials. Knockout, also has a couple of cool extensions like combining it with Kendo. I got good with Knockout but I had stuff to learn.

I had to implement a feature where an "admin" can select a "role" for new accounts. To select a role, admin had to select one of three check boxes in the page. The data for the check boxes is from JSON. Not a big problem since Knockout plays nice with jQuery.

So what exactly is the problem then?

The problem was that I needed to capture the "checked" value and push it into a new array. You would think that this a straight-forward preposition. So my first attempt went something like:

My Knockout script
<script type="text/javascript"> 
...{some code omitted for clarity}
        function roleModel(id, name, description) {
            var self = this;
            self.RoleId = id;
            self.RoleName = name;
            self.Description = description;
        }

        function viewModel(){
            var self = this;
            self.AvailableRoles = ko.observableArray();
            $.getJSON("/Role/GetRoles", function (data) {
                for (var i = 0; i < data.length; i++) {
                    self.AvailableRoles.push(new roleModel(data[i].RoleId, data[i].RoleName, data[i].Description));
                }
            });
            self.Roles = ko.observableArray();
        }
...{some code omitted for clarity}

        ko.applyBindings(new viewModel());

My Markup (HTML5)
<div data-bind="foreach: AvailableRoles">  
    <input type="checkbox" data-bind="attr: {value: AvailableRoles}, checked: Roles"/>  
    <span data-bind="text: RoleName"></span>  
</div>  
Notice the Roles and AvailableRoles properties. Apparently you can't bind the AvailbleRoles "directly" into the value field of the checkbox.

After looking around the web, I hit on the that idea I needed an intermediary field to store a key (thus I can bind it into the checkbox's value field) then I can use that key to retrieve the right role object from the AvailableRole object which is an array of role objects and store it in the Role property.
            
            self.Roles = ko.computed(function () {
                return ko.utils.arrayMap(self.SelectedRolesIds(), function (roleId) {
                    return ko.utils.arrayFirst(self.AvailableRoles(), function (item) {
                        return item.RoleId == roleId;
                    });
                });
            });
            self.SelectedRolesIds = ko.observableArray();
That's the SelectedRolesIds property in my viewModel. I also changed the Roles propety to be a computed field to make it "dependent" on what Role Ids are present in the SelectedRolesIds property. You still have to make changes to the markup. This all made nice with knockout utility functions - arrayMap and arrayFirst.

<div data-bind="foreach: AvailableRoles">  
    <input type="checkbox" data-bind="attr: {value: RoleId}, checked:$root.SelectedRolesIds"/>  
    <span data-bind="text: RoleName"></span>  
 </div>
The AvailableRoles property stays the same.