Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an observable array with undo?

I am trying to add knockout JS to a search page on our website. Currently you open up a jQuery dialog box, which has a number of checkboxes of criteria that you can select.

There are multiple dialogs with multiple types of criteria. When you open the dialog, the checkboxes do not take effect until you hit an "Update" button, if you click cancel or just close the window, the changes you made get reverted and the dialog is set to its former state.

I read this and a few other posts. However this seems to only work with ko.observable, and I cannot seem to get it to work with ko.observableArray.

Has anyone accomplished this or have any ideas?

An example of what I want to do:

Html:

<form>
    <div>
        <div>
            <label><input type="checkbox" data-bind="checked: genders" value="1" />Male</label>
            <label><input type="checkbox" data-bind="checked: genders" value="2" />Female</label>
        </div>
    </div>
    <a id="buttonCancel">Cancel</a>
    <a id="buttonUpdate">Update</a>
</form>
<div data-bind="text: ko.toJSON(viewModel)"></div>

Javascript:

var viewModel = {
    genders: ko.observableArrayWithUndo([])
};

ko.applyBindings(viewModel);

$('#buttonCancel').click(function(){
   viewModel.genders.resetChange();
});

$('#buttonUpdate').click(function(){
    viewModel.genders.commit();
    return false;
});
like image 348
Ted Ballou Avatar asked Aug 05 '11 19:08

Ted Ballou


2 Answers

Here would be one way to approach it:

//wrapper to an observableArray of primitive types that has commit/reset
ko.observableArrayWithUndo = function(initialArray) {
    var _tempValue = ko.observableArray(initialArray.slice(0)), 
        result = ko.observableArray(initialArray);

    //expose temp value for binding
    result.temp = _tempValue;

    //commit temp value
    result.commit = function() {
        result(_tempValue.slice(0));
    };

    //reset temp value
    result.reset = function() {
        _tempValue(result.slice(0)); 
    };

    return result;
};

You would bind your checkboxes to yourName.temp and the other part of your UI to just yourName.

Here is a sample: http://jsfiddle.net/rniemeyer/YrfyW/

The slice(0) is one way to get a shallow copy of an array (or even just slice()). Otherwise, you would be performing operations on a reference to the same array.

like image 174
RP Niemeyer Avatar answered Oct 11 '22 23:10

RP Niemeyer


Given HTML similar to:

<div>
    <button data-bind="click: function() { undo(); }">Undo</button>
    <input data-bind="value: firstName" />
    <input data-bind="value: lastName" />
    <textarea data-bind="value: text"></textarea>
</div>

You could use some Knockout code similar to this, basically saving the undo stack as a JSON string representation of the state after every change. Basically you create a fake dependent observable to subscribe to all the properties in the view, alternatively you could manually iterate and subscribe to each property.

//current state would probably come from the server, hard coded here for example
var currentState = JSON.stringify({
    firstName: 'Paul',
    lastName: 'Tyng',
    text: 'Text' 
})
   , undoStack = [] //this represents all the previous states of the data in JSON format
    , performingUndo = false //flag indicating in the middle of an undo, to skip pushing to undoStack when resetting properties
    , viewModel = ko.mapping.fromJSON(currentState); //enriching of state with observables


//this creates a dependent observable subscribed to all observables 
//in the view (toJS is just a shorthand to traverse all the properties)
//the dependent observable is then subscribed to for pushing state history
ko.dependentObservable(function() {
    ko.toJS(viewModel); //subscribe to all properties    
}, viewModel).subscribe(function() {
    if(!performingUndo) {
    undoStack.push(currentState);
    currentState = ko.mapping.toJSON(viewModel);
}
});

//pops state history from undoStack, if its the first entry, just retrieve it
    window.undo = function() {
        performingUndo = true;
        if(undoStack.length > 1)
        {
            currentState = undoStack.pop();
            ko.mapping.fromJSON(currentState, {}, viewModel);
        }
        else {
            currentState = undoStack[0];
            ko.mapping.fromJSON(undoStack[0], {}, viewModel);
        }
        performingUndo = false;
};

ko.applyBindings(viewModel);

I have a sample of N-Level undo with knockout here:

http://jsfiddle.net/paultyng/TmvCs/22/

You may be able to adapt for your uses.

like image 38
Paul Tyng Avatar answered Oct 12 '22 00:10

Paul Tyng