Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Guard Your Model: Accept or Cancel Edits to Observables in KnockoutJS

| Comments

Two-way data-binding is one of the main benefits of using KnockoutJS. However, I have found some situations where it is inconvenient that the underlying model is immediately updated based on user input. In these scenarios, I would prefer to allow a user to accept or cancel their edits first before applying them to my view model.

I experimented with a few options for cleanly managing this type of interaction:

  • Temporary objects - One option that I tried was using a temporary copy of the real object for editing. If the user cancels, then I could simply throw away the object, while on an accept I would need to replace the original version with the edited copy. This works, but brokering the objects back and forth between the temporary and original objects can get messy, especially if your structures are complex.
  • Temporary properties – Another option that I explored was adding temporary properties on each of my objects and copying the temp values into the originals on an accept. This is pretty simple and works well, but now all of the objects require extra properties, which bloat the model and possibly need to be stripped out before being sent back to the server. Additionally, we must be careful to always bind against the temporary properties in edit scenarios.
  • Special Observable - The final option that I investigated was creating some type of augmented observable that manages this functionality internally. I settled on creating a simple protectedObservable that could be used like any other observable, but had the extra features to support the accept/cancel requirement.

Here is how I define the protectedObservable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//wrapper to an observable that requires accept/cancel
ko.protectedObservable = function(initialValue) {
    //private variables
    var _actualValue = ko.observable(initialValue),
        _tempValue = initialValue;

    //computed observable that we will return
    var result = ko.computed({
        //always return the actual value
        read: function() {
           return _actualValue();
        },
        //stored in a temporary spot until commit
        write: function(newValue) {
             _tempValue = newValue;
        }
    }).extend({ notify: "always" });

    //if different, commit temp value
    result.commit = function() {
        if (_tempValue !== _actualValue()) {
             _actualValue(_tempValue);
        }
    };

    //force subscribers to take original
    result.reset = function() {
        _actualValue.valueHasMutated();
        _tempValue = _actualValue();   //reset temp value
    };

    return result;
};

A few things to note:

  • A protectedObservable is an augmented computed observable
  • The computed observable is dependent on an observable (_actual) that is private (only accessible internally by the function).
  • It is defined as a writable computed observable, so we have control over the read and write methods. The read method always returns the value of our underlying observable The write method stores the edited value in a private variable called _temp.
  • The commit method is used to actually update the observable from the temp value, if it has changed.
  • The reset method simply tells subscribers to update their value again from the original and resets the temporary value (so it would not cause unexpected behavior if commit was called without any further edits).

We can define these in our view model similar to any other observable:

1
2
3
4
5
6
7
8
9
10
11
12
var viewModel = {
      name: ko.protectedObservable(Item A),
      quantity: ko.protectedObservable(10),
      commitAll: function() {
          this.name.commit();
          this.quantity.commit();
      },
      resetAll: function() {
          this.name.reset();
          this.quantity.reset();
      }
  };

We can data-bind to these with the normal syntax:

1
2
3
4
<input data-bind=“value: name />
<input data-bind=“value: quantity />
<button data-bind=“click: commitAll>Commit</button>
<button data-bind=“click: resetAll>Reset</button>

So, now we have a self-contained observable where values are only persisted to the model when explicitly committed.

Here is an expanded example with a list editor that uses protectedObservables:

Link to jsFiddle

I think that the idea of a computed observable that depends on its own private observable could have some possibilities for solving other issues in a clean way. For example, recently a user had issues with numeric information in input fields being saved as strings back to the model. In absence of an official option on the value binding or on observable creation to support this need, one solution could be to create a numericObservable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ko.numericObservable = function(initialValue) {
    var _actual = ko.observable(initialValue);

    var result = ko.dependentObservable({
        read: function() {
          return _actual();
        },
        write: function(newValue) {
           var parsed = parseFloat(newValue);
            _actual(isNaN(parsed) ? newValue: parsed);
        }
    });

    return result;
};

Now, if the value is numeric it would be stored in the model as a number (sample here). Basically, this technique allows us to create observables with getter/setter functions to have better control over how the data is set and retrieved.

As for something like a protectedObservable, anyone have other ideas for clean ways to deal with this scenario? I was also thinking about how validation or change tracking could be added to an observable in the same fashion. I did come across a post by Steve that could achieve similar results using the interceptor pattern that would be interesting to explore as well. Any other ideas for clever ways to use this technique or features that it could support?

Comments