可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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!
回答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).
回答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.
回答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.
回答4:
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.
回答5:
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