Undo/Redo framework

https://groups.google.com/d/topic/knockoutjs/Xsj2xJrmnyM/discussion

by rniemeyer

HTML

<script src="https://github.com/jquery/jquery-tmpl/raw/master/jquery.tmpl.js"></script>
<script src="http://knockoutjs.com/downloads/knockout-2.2.1.js"></script>
<table>
    <tr>
        <th>Name</th>
        <th></th>
        <th></th>
    </tr>            
    <tbody data-bind="template: { name: templateToUse, foreach: items}"></tbody>
</table>

<script id="itemTmpl" type="text/html">
    <tr>
        <td data-bind="text: name"></td>
        <td class="buttons">
            <button data-bind="click: function() { viewModel.editItem($data); }">Edit</button>
            <button data-bind="click: function() { viewModel.deleteItem($data); }">Delete</button>
        </td>
    </tr>
</script>

<button data-bind="click: addItem">New Item</button>

<script id="editTmpl" type="text/html">
    <tr>
        <td>
            <input data-bind="value: name" />
        </td>
        <td class="buttons">
            <button data-bind="enable: name.undoable, click: name.undo">Undo</button>
            <button data-bind="enable: name.redoable, click: name.redo">Redo</button>
        </td>
    </tr>
</script>

CSS

input { width: 75px; }
td { width: 75px; }
th { color: #666; font-size: .8em; }
.buttons { width: 150px; }

JavaScript

$(function() {
    ko.applyBindings(viewModel);
});

//wrapper for an observable that supports undo/redo
ko.undoableObservable = function(initialValue) {
    //private variables
    var _actual = ko.observable(initialValue);
    var _undos = ko.observableArray([]);
    var _redos = ko.observableArray([]);

    var result = ko.dependentObservable({
        read: function() {
            return _actual();
        },
        write: function(newValue) {
            _redos([]);
            _undos.push(_actual());
            _actual(newValue);
        }
    });

    //expose these flags to allow enabling/disabling buttons
    result.undoable = ko.dependentObservable(function() {
        return _undos().length > 0;
    });

    result.redoable = ko.dependentObservable(function() {
        return _redos().length > 0;
    });

    //go back to the latest undo value
    result.undo = function() {
        if (_undos().length > 0) {
            var last = _undos.pop();
            _redos.push(_actual());
            _actual(last);
        }
    };

    //redo the latest change
    result.redo = function() {
        if (_redos().length > 0) {
            var next = _redos.pop();
            _undos.push(_actual());
            _actual(next);
        }
    };

    return result;
};

var viewModel = {
    items: ko.observableArray([
        {
        name: ko.undoableObservable("one")},
    {
        name: ko.undoableObservable("two")},
    {
        name: ko.undoableObservable("three")}
    ]),
    selectedItem: ko.observable(),
    addItem: function() {
        var newItem = { name:  ko.undoableObservable("new") };
        this.items.push(newItem);
        this.selectedItem(newItem);
    },
    deleteItem: function(itemToDelete) {
        this.items.remove(itemToDelete);
        this.selectedItem(null);
    },
    editItem: function(item) {
        this.selectedItem(item);
    },
    templateToUse: function(item) {
        return viewModel.selectedItem() === item ? "editTmpl"...