Tutorial : How to create editable grids using Knockout JS

by kougiland

HTML

<script src="http://cloud.github.com/downloads/knockout/knockout/knockout-2.1.0.debug.js"></script>
<button class="button addNewButton" data-bind="click: $root.addFruit">Add a new fruit</button>
    <hr />
<table class="fruitGrid">
        <thead>
            <tr>
                <th class="headRow headColumn">
                    Fruit Name
                </th>
                <th class="headRow headColumn">
                    Fruit Colour
                </th>
                <th class="headRow tools">
                </th>
            </tr>
        </thead>
        <tbody data-bind="foreach: fruits">
            <tr class="row">
                <td class="rowItem">
                    <input type="text" class="edit" data-bind="value: name.editValue, visible: $root.isItemEditing($data)"  />
                    <label class="read" data-bind="text: name, visible: !$root.isItemEditing($data)" />
                </td>
                <td class="rowItem">
                    <select class="edit" data-bind="options: $root.availableColours, value: colour.editValue, visible: $root.isItemEditing($data)"></select>
                    <label class="read" data-bind="text: colour, visible: !$root.isItemEditing($data)" />
                </td>
                <td class="tools">
                    <a class="button toolButton" href="#" data-bind="click: $root.editFruit.bind($root), visible: !$root.isItemEditing($data)">Edit</a> 
                    <a class="button toolButton" href="#" data-bind="click: $root.removeFruit.bind($root), visible: !$root.isItemEditing($data)">Remove</a>
                    <a class="button toolButton" href="#" data-bind="click: $root.applyFruit.bind($root), visible: $root.isItemEditing($data)">Apply</a>
                    <a class="button toolButton" href="#" data-bind="click: $root.cancelEdit.bind($root), visible: $root.isItemEditing($data)">Cancel</a>
                </td>
            </tr>
        </tbody>
    </table>

CSS

/* Grid */

.fruitGrid { 
    /* structure */
    width: 550px; 
    padding: 5px; 
}

.fruitGrid .headColumn { 
    /* structure */
    padding: 10px; 
    
    /* skin */
    color: White;
    background-color: #888;

}

.fruitGrid .row:nth-child(even) { 
    /* skin */
    background-color:#eee; 
}

.fruitGrid .rowItem { 
    /* structure */
    padding: 10px; 

}

.fruitGrid .rowItem .read   { 
    /* structure */
    display: block;
    width: 150px;
}

.fruitGrid .tools { 
    /* skin */
    background-color:#ffffff;
    padding: 5px;
}

/* Buttons */

.button {
    /* structure */
    padding: 8px 12px;
    
    /* skin */
    border: 1px solid gainsboro;
    cursor: pointer;  
    text-decoration: none; 
}

.addNewButton {
    /* skin */
    background-color: #FB8F3D;   
    background-image: -webkit-linear-gradient(top, #FDA352, #FB8F3D);
    background-image: -moz-linear-gradient(top, #FDA352, #FB8F3D);
    background-image: -ms-linear-gradient(top, #FDA352, #FB8F3D);
    background-image: -o-linear-gradient(top, #FDA352, #FB8F3D);
    background-image: linear-gradient(top, #FDA352, #FB8F3D);
    color: White;
}

.toolButton {
    /* skin */
    background-color: #F9F9F9;
    background-image: -webkit-linear-gradient(top, white, #F9F9F9);
    background-image: -moz-linear-gradient(top, white, #F9F9F9);
    background-image: -ms-linear-gradient(top, white, #F9F9F9);
    background-image: -o-linear-gradient(top, white, #F9F9F9);
    background-image: linear-gradient(top, white, #F9F9F9);
    color: #444;
}

JavaScript

/*----------------------------------------------------------------------*/
/* Observable Extention for Editing
/*----------------------------------------------------------------------*/
ko.observable.fn.beginEdit = function (transaction) {
    
    var self = this;
    var commitSubscription, 
        rollbackSubscription;
    
    // get the current value and store it for editing
    if (self.slice)
        self.editValue = ko.observableArray(self.slice());
    else
        self.editValue = ko.observable(self());
        
    self.dispose = function () {        
        // kill this subscriptions
        commitSubscription.dispose();
        rollbackSubscription.dispose(); 
    };
    
    self.commit = function () {
        // update the actual value with the edit value
        self(self.editValue());
        
        // dispose the subscriptions
        self.dispose();
    };
        
    self.rollback = function () {
        // rollback the edit value
        self.editValue(self());
        
        // dispose the subscriptions
        self.dispose();
    };
    
    //  subscribe to the transation commit and reject calls
    commitSubscription = transaction.subscribe(self.commit,
                                                self,
                                                "commit");
    
    rollbackSubscription = transaction.subscribe(self.rollback,
                                                    self,
                                                    "rollback");
    
    return self;
}

/*----------------------------------------------------------------------*/
/* Item Model
/*----------------------------------------------------------------------*/

function Fruit( name, colour) {
    var self = this;
    self.name = ko.observable(name);
    self.colour = ko.observable(colour);
};

Fruit.prototype.beginEdit = function(transaction) {
    this.name.beginEdit(transaction);
   ...