Edit in place Knockout

by HugeHugh

HTML

<table>
    <thead>
        <th>ID</th>
        <th>Name</th>
        <th>Position</th>
        <th>&nbsp;</th>
    </thead>
    <tbody data-bind="foreach:rows">
        <tr data-bind="visible:!isEditMode()">
            <td data-bind="text:id"></td>
            <td data-bind="text:name"></td>
            <td data-bind="text:position"></td>
            <td><input type="button" data-bind="click:editRow" value="Edit"/></td>
        </tr>
        <tr data-bind="visible:isEditMode">
            <td data-bind="text:id"></td>
            <td><input data-bind="value:name"/></td>
            <td><select data-bind="options: $parent.rows, optionsText: 'position', optionsValue: 'position', value: position"></select></td>
            <td><input type="button" data-bind="click:saveRow" value="Save"/><input type="button" data-bind="click:cancelEdit" value="Cancel"/></td>
        </tr>
    </tbody>
</table>

<div class="saver" data-bind="visible:isSaving">Saving to the database...</div>

<input type="button" data-bind="click:function(){isAddRowFormVisible(true)}" value="Add a row"/>

<div class="darkener" data-bind="visible: isAddRowFormVisible, click:function(){isAddRowFormVisible(false)}">
    <div class="addRowForm" data-bind="click: function(){ return true }, clickBubble: false">
    <h3>Add a row</h3>
    ID: <input data-bind="value:newID, valueUpdate: 'afterkeydown'"/><br/>
    Name: <input data-bind="value:newName, valueUpdate: 'afterkeydown'"/><br/>
    Position: <input data-bind="value:newPosition, valueUpdate: 'afterkeydown'"/><br/>
    <input type="button" value="Add row" data-bind="click:addRow, disable: !canAddRow()"/>
    <input type="button" data-bind="click:function(){isAddRowFormVisible(false)}" value="Close"/>
    </div>
</div>

CSS

.saver {position:absolute; top:0;right:0;background:green;color:white;padding:4px;display:inline-block}
.darkener {position:absolute; top:0;right:0;bottom:0;left:0; background:rgba(0,0,0,0.5)}
.addRowForm {width:300px; height:200px; background:#eee; border:1px solid #000; box-shadow:0 0 20px rgba(0,0,0,0.8); padding:8px; position:absolute;left:50%;top:50%; margin-left:-150px; margin-top:-100px; border-radius:8px}

JavaScript

function rowViewModel(initId, initName, initPosition) {
    var name = ko.observable(initName);
    var position = ko.observable(initPosition);
    var isEditMode = ko.observable(false);
    
    var preEditValues = {};
    
    function editRow() {
        preEditValues = {
            name: name(),
            position: position()
        };
        isEditMode(true);
    }
    function saveRow() {
        isEditMode(false);
        pageModel.isSaving(true);
        // commit the changes to the DB with an ajax call
        // fake it with an artificial 1 second delay
        setTimeout(function() {
            pageModel.isSaving(false);
        }, 1000);
    }
    function cancelEdit() {
        name(preEditValues.name);
        position(preEditValues.position);
        isEditMode(false);
    }
    
    return {
        // id is not observable because it is never modified
        id: initId,
        name: name,
        position: position,
        isEditMode: isEditMode,
        editRow: editRow,
        saveRow: saveRow,
        cancelEdit: cancelEdit
    };
}

var rows = [];
rows.push(rowViewModel(1, "Hugh", "Developer"));
rows.push(rowViewModel(2, "Zach", "Developer"));
rows.push(rowViewModel(3, "Josh", "Designer"));
rows.push(rowViewModel(4, "Laura", "Designer"));
rows.push(rowViewModel(5, "Jake", "Security"));
rows.push(rowViewModel(6, "Bernie", "Real Estate Mogul"));

function addRow() {
    pageModel.isSaving(true);    
    // commit the changes to the DB with an ajax call
    
    // 1 second delay simluates server lag
    setTimeout(function() {
        pageModel.isAddRowFormVisible(false);
        // where to add
        var where = 3;
        pageModel.rows.splice(where, 0, rowViewModel(pageModel.newID(), pageModel.newName(), pageModel.newPosition()));
        pageModel.newID(7);
        pageModel.newName('');
        pageModel.newPosition('');
        pageModel.isSaving(false);
    }, 1000);
}

var pageModel = {
    rows: ko.observableArray(rows),
   ...