How to cancel/revert changes to an observable model (or replace model in array with untouched copy)

11,442

Solution 1

There are a few ways to handle something like this. You can construct a new object with the same values as your current one and throw it away on a cancel. You could add additional observables to bind to the edit fields and persist them on the accept or take a look at this post for an idea on encapsulating this functionality into a reusable type (this is my preferred method).

Solution 2

I ran across this post while looking to solve a similar problem and figured I would post my approach and solution for the next guy.

I went with your line of thinking - clone the object and repopulate with old data on "undo":

1) Copy the data object into a new page variable ("_initData") 2) Create Observable from original server object 3) on "undo" reload observable with unaltered data ("_initData")

Simplified JS: var _viewModel; var _initData = {};

$(function () {
    //on initial load
    $.post("/loadMeUp", {}, function (data) {
        $.extend(_initData , data);
        _viewModel = ko.mapping.fromJS(data);
    });

    //to rollback changes
    $("#undo").live("click", function (){
        var data = {};
        $.extend(data, _initData );
        ko.mapping.fromJS(data, {}, _viewModel);
    });

    //when updating whole object from server
    $("#updateFromServer).live("click", function(){
        $.post("/loadMeUp", {}, function (data) {
            $.extend(_initData , data);
            ko.mapping.fromJS(data, {}, _viewModel);
        });
    });

    //to just load a single item within the observable (for instance, nested objects)
    $("#updateSpecificItemFromServer).live("click", function(){
        $.post("/loadMeUpSpecificItem", {}, function (data) {
            $.extend(_initData.SpecificItem, data);
            ko.mapping.fromJS(data, {}, _viewModel.SpecificItem);
        });
    });

    //updating subItems from both lists
    $(".removeSpecificItem").live("click", function(){
        //object id = "element_" + id
        var id = this.id.split("_")[1];
        $.post("/deleteSpecificItem", { itemID: id }, function(data){
            //Table of items with the row elements id = "tr_" + id
            $("#tr_" + id).remove();
            $.each(_viewModel.SpecificItem.Members, function(index, value){
                if(value.ID == id)
                    _viewModel.SpecificItem.Members.splice(index, 1);
            });
            $.each(_initData.SpecificItem.Members, function(index, value){
                if(value.ID == id)
                    _initData.SpecificItem.Members.splice(index, 1);
            });
        });
    });
});

I had an object that was complicated enough that I didn't want to add handlers for each individual property.

Some changes are made to my object in real time, those changes edit both the observable and the "_initData".

When I get data back from the server I update my "_initData" object to attempt to keep it in sync with the server.

Solution 3

Very old question, but I just did something very similar and found a very simple, quick, and effective way to do this using the mapping plugin.

Background; I am editing a list of KO objects bound using a foreach. Each object is set to be in edit mode using a simple observable, which tells the view to display labels or inputs.

The functions are designed to be used in the click binding for each foreach item.

Then, the edit / save / cancel is simply:

this.edit = function(model, e)
{
    model.__undo = ko.mapping.toJS(model);
    model._IsEditing(true);
};

this.cancel = function(model, e)
{
    // Assumes you have variable _mapping in scope that contains any 
    // advanced mapping rules (this is optional)
    ko.mapping.fromJS(model.__undo, _mapping, model);
    model._IsEditing(false);
};

this.save = function(model, e)
{
    $.ajax({
        url: YOUR_SAVE_URL,
        dataType: 'json',
        type: 'POST',
        data: ko.mapping.toJSON(model),
        success: 
            function(data, status, jqxhr)
            {
                model._IsEditing(false);
            }
    }); 
};

This is very useful when editing lists of simple objects, although in most cases I find myself having a list containing lightweight objects, then loading a full detail model for the actual editing, so this problem does not arise.

You could add saveUndo / restoreUndo methods to the model if you don't like tacking the __undo property on like that, but personally I think this way is clearer as well as being a lot less code and usable on any model, even one without an explicit declaration.

Share:
11,442

Related videos on Youtube

Damien Gabrielson
Author by

Damien Gabrielson

Updated on July 06, 2020

Comments

  • Damien Gabrielson
    Damien Gabrielson almost 4 years

    I have a viewModel with an observableArray of objects with observable variables.

    My template shows the data with an edit button that hides the display elements and shows input elements with the values bound. You can start editing the data and then you have the option to cancel. I would like this cancel to revert to the unchanged version of the object.

    I have tried clone the object by doing something like this:

    viewModel.tempContact = jQuery.extend({}, contact);
    

    or

    viewModel.tempContact = jQuery.extend(true, {}, contact);
    

    but viewModel.tempContact gets modified as soon as contact does.

    Is there anything built into KnockoutJS to handle this kind of situation or am I best off to just create a new contact with exactly the same details and replace the modified contact with the new contact on cancel?

    Any advice is greatly appreciated. Thanks!

  • Damien Gabrielson
    Damien Gabrielson about 13 years
    Thanks, Ryan! I've read a couple of posts on your site today but somehow I missed that one. Good stuff!
  • Gunslinger
    Gunslinger over 10 years
    Thanks! I'm using this code now and it works very well. I modified to make the tempvalue observable too. I also added a computed to check if the protectedObservable was dirty.