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

前端 未结 5 1961
萌比男神i
萌比男神i 2021-01-01 12:19

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 s

相关标签:
5条回答
  • 2021-01-01 12:29

    I needed something similar, and I couldn't use the protected observables, as I needed the computed to update on the temporary values. So I wrote this knockout extension:

    This extension creates an underscore version of each observable ie self.Comments() -> self._Comments()

    ko.Underscore = function (data) {
        var obj = data;
        var result = {};
        // Underscore Property Check
        var _isOwnProperty = function (isUnderscore, prop) {
            return (isUnderscore == null || prop.startsWith('_') == isUnderscore) && typeof obj[prop] == 'function' && obj.hasOwnProperty(prop) && ko.isObservable(obj[prop]) && !ko.isComputed(obj[prop])
        }
        // Creation of Underscore Properties
        result.init = function () {
            for (var prop in obj) {
                if (_isOwnProperty(null, prop)) {
                    var val = obj[prop]();
                    var temp = '_' + prop;
                    if (obj[prop].isObservableArray)
                        obj[temp] = ko.observableArray(val);
                    else
                        obj[temp] = ko.observable(val);
                }
            }
        };
        // Cancel
        result.Cancel = function () {
            for (var prop in obj) {
                if (_isOwnProperty(false, prop)) {
                    var val = obj[prop]();
                    var p = '_' + prop;
                    obj[p](val);
                }
            }
        }
        // Confirm
        result.Confirm = function () {
            for (var prop in obj) {
                if (_isOwnProperty(true, prop)) {
                    var val = obj[prop]();
                    var p = prop.replace('_', '');
                    obj[p](val);
                }
            }
        }
        // Observables
        result.Properties = function () {
            var obs = [];
            for (var prop in obj) {
                if (typeof obj[prop] == 'function' && obj.hasOwnProperty(prop) && ko.isObservable(obj[prop]) && !ko.isComputed(obj[prop])) {
                    var val = obj[prop]();
                    obs.push({ 'Name': prop, 'Value': val });
                }
            }
            return obs;
        }
    
        if (obj != null)
            result.init();
    
        return result;
    }
    

    This extension will save you writing duplicates of each of your observables and ignores your computed. It works like this:

    var BF_BCS = function (data) {
        var self = this;
    
        self.Score = ko.observable(null);
        self.Comments = ko.observable('');
    
        self.Underscore = ko.Underscore(self);
    
        self.new = function () {
            self._Score(null);
            self._Comments('');
            self.Confirm();
        }
    
        self.Cancel = function () {
            self.Pause();
            self.Underscore.Cancel();
            self.Resume();
        }
    
        self.Confirm = function () {
            self.Pause();
            self.Underscore.Confirm();
            self.Resume();
        }
    
        self.Pause = function () {
    
        }
    
        self.Resume = function () {
    
        }
    
        self.setData = function (data) {
            self.Pause();
    
            self._Score(data.Score);
            self._Comments(data.Comments);
            self.Confirm();
            self.Resume();
        }
    
        if (data != null)
            self.setData(data);
        else
            self.new();
    };
    

    So as you can see if you have buttons on html:

    <div class="panel-footer bf-panel-footer">
        <div class="bf-panel-footer-50" data-bind="click: Cancel.bind($data)">
            Cancel
        </div>
        <div class="bf-panel-footer-50" data-bind="click: Confirm.bind($data)">
            Save
        </div>
    </div>
    

    Cancel will undo and revert your observables back to what they were, as were save will update the real values with the temp values in one line

    0 讨论(0)
  • 2021-01-01 12:39

    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.

    0 讨论(0)
  • 2021-01-01 12:44

    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.

    0 讨论(0)
  • 2021-01-01 12:47

    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).

    0 讨论(0)
  • 2021-01-01 12:47

    You might consider using KO-UndoManager for this. Here's a sample code to register your viewmodel:

    viewModel.undoMgr = ko.undoManager(viewModel, {
      levels: 12,
      undoLabel: "Undo (#COUNT#)",
      redoLabel: "Redo"
    });
    

    You can then add undo/redo buttons in your html as follows:

     <div class="row center-block">
        <button class="btn btn-primary" data-bind="
          click: undoMgr.undoCommand.execute, 
          text: undoMgr.undoCommand.name, 
          css: { disabled: !undoMgr.undoCommand.enabled() }">UNDO</button>
        <button class="btn btn-primary" data-bind="
          click: undoMgr.redoCommand.execute, 
          text: undoMgr.redoCommand.name, 
          css: { disabled: !undoMgr.redoCommand.enabled() }">REDO</button>
      </div> 
    

    And here's a Plunkr showing it in action. To undo all changes you'll need to loop call undoMgr.undoCommand.execute in javascript until all the changes are undone.

    0 讨论(0)
提交回复
热议问题