2

Hard to come up with a descriptive title, but what I want to achieve is the following:

  • I have a Knockout Observable bound to a table row.
  • I have an Edit-button which shows a number of input fields to the user, these fields are also bound to the same observable allowing the user to edit values inline in the table row.
  • I want to save my observable in a temporary variable when the user clicks the Edit-button, so that I can undo the changes if the user wants to discard their edits.

How can I achieve this in a clean way with javascript? My object has methods in addition to plain properties.


These are the hacks that I'm aware of already:

  • Convert my object to JSON and back. (Doesn't work with methods)
  • Different kind of "clone" methods. (Haven't found any that works for me)
  • Reload the data from the server and remap the observable.
Hein Andre Grønnestad
  • 6,885
  • 2
  • 31
  • 43
  • You can store it as JSON in local storage. Check [here](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). You can convert to JSON like this 'var json = ko.toJSON(observable);' – Rex Feb 24 '17 at 14:50
  • @Rex yes, but this would not preserve the methods on my object and I would have to remap the observable. Ideally I'm looking for a javascript-only solution. – Hein Andre Grønnestad Feb 24 '17 at 14:53
  • 1
    Your C# code would not work either if `data` is a reference type... Weird that you somehow figured out the trick doesn't work for Javascript, then think in C# it works differently? – MarioDS Feb 24 '17 at 15:23
  • @MarioDS You are right of course, I'm not sure what made me think that to be honest. Thanks. – Hein Andre Grønnestad Feb 24 '17 at 15:55

2 Answers2

1

You can use an extender to store the previous values of your observable:

ko.extenders.keepHistory = function(target, option) {
    target.subscribe(function(newValue) {
       if(!target.history) target.history = ko.observableArray();
       target.history.push(newValue);
    });
    return target;
};

Usage:

var vm = function () {
    var self = this;
    this.someValue = ko.observable().extend({keepHistory: true});
}

See this (updated) demo.

Update

OK I have made changes to implement revert in the extender:

ko.extenders.keepHistory = function(target, option) {
    target.subscribe(function(newValue) {
       if (target.reverting) { target.reverting = false; return; }
       if(!target.history) target.history = ko.observableArray();
       if(!target.revert) {
           target.revert = function () {
           console.log(target.history()[target.history().length -3]);
                var previousVersion = target.history().length < 2 ? null : target.history()[target.history().length -2];
              if (!!previousVersion) {
                  target.reverting = true;
                  console.log(previousVersion);
                  target(previousVersion);
              }
           }
       }
       target.history.push(newValue);
       console.log(target.history());
    });
    return target;
};

See latest demo. Note that I have used ES6 clone method (see this post).

Community
  • 1
  • 1
GôTô
  • 7,974
  • 3
  • 32
  • 43
1

My preferred approach for these kinds of features is usually something along the lines of:

  • Have a view model for the object you'd like to be able to edit
  • Define an own export function that outputs the data that is unique to an instance of the viewmodel
  • Define an import function that can map these data back to their properties

Example

For example, say you have a Person viewmodel. While the viewmodel can contain methods and computed properties, there are two fields the user can edit: firstName and lastName: we'll export those to a plain object when we start our edit.

On import, we use this object to set our observable values, automatically recomputing our displayName property and state-computeds.

If you want it to be more general, you could contain these methods in their own class and use composition to add the feature to other view models.

var Person = function(firstName, lastName) {
  this.firstName = ko.observable(firstName);
  this.lastName = ko.observable(lastName);
  
  this.displayName = ko.pureComputed(function() {
    return [this.firstName(), this.lastName()].join(" ");
  }, this);
  
  this.saved = ko.observable(null);
  
  this.isEditing = ko.pureComputed(function() {
    return this.saved() !== null;
  }, this);
};

Person.prototype.export = function() {
  return {
    firstName: this.firstName(),
    lastName: this.lastName()
  }
};

Person.prototype.import = function(obj) {
  this.firstName(obj.firstName);
  this.lastName(obj.lastName);
}

Person.prototype.startEdit = function() {
  this.saved(this.export());
};

Person.prototype.cancelEdit = function() {
  this.import(this.saved());
  this.saved(null);
};

Person.prototype.saveEdit = function() {
  this.saved(null);
};

var App = function() {
  this.people = ko.observableArray([
    new Person("Jane", "Doe"),
    new Person("John", "Doe")
  ]);
};

ko.applyBindings(new App());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul data-bind="foreach: people">
  <li>
    
    <!-- ko ifnot: isEditing -->
    <p data-bind="text: displayName"></p>
    <button data-bind="click: startEdit">edit</button>
    <!-- /ko -->
    
    <!-- ko if: isEditing -->
    <p>
      <input data-bind="textInput: firstName" type="text">
      <input data-bind="textInput: lastName" type="text">
    </p>
    <button data-bind="click: cancelEdit">cancel</button>
    <button data-bind="click: saveEdit">save</button>
    <!-- /ko -->
    
  </li>
</ul>
user3297291
  • 22,592
  • 4
  • 29
  • 45