Implementing an editor that allows users to accept or cancel their changes is a common task in Knockout.js. Previously, I suggested the idea of a
protectedObservable that is an extended observable with the ability to commit and reset the value being edited. Since Knockout 2.0, I would probably now implement that functionality using either extenders or by augmenting the
.fn object of the core types. However, I now use a different pattern.
I described this pattern in the Twin Cities Code Camp presentation here. This technique allows you to easily copy, commit, and revert changes to an entire model. There are two rules that make this pattern work:
The creation of observables and computeds needs to be separate from actually populating them with values.
The format of the data that you put in should be the same as the format that you are able to get out. Typically this means that calling
ko.toJSon your model should result in an object that you could send back through the function described in step 1 to re-populate the values.
Separate creation and initialization
The idea is that we can apply fresh data to our object at anytime, so our constructor function should just create our structure, while a separate method handles setting the values.
1 2 3 4 5 6 7 8 9 10 11
update function (call it whatever you like
Data in matches data out
ko.toJS on our model, will give us the plain object that we need. It is okay, if there are some additional computeds/properties on the object, but it needs to be appropriate for sending through the
update function again.
1 2 3 4 5 6 7
Since observables are functions and functions are objects that can have their own properties, it is perfectly valid to create a
formatted computed off of the
price observable. In the markup, we can bind against
price.formatted. However, when calling
formatted sub-observable will disappear, as we will simply be left with a
price property and value.
Now that we have these pieces in place, it is easy to create an editor that allows reverting changes. In the event that a user chooses to cancel their editing, all we need to do is send the original data back through our
update function and we will be back to where we started.
We will need to track the original data, so that it is available to repopulate the model. A good place to cache this data is in the
update function itself. It is useful to make sure that the data is “hidden” as well, so multiple edits don’t result in recursive versions of the old data being kept around. If we were not using the prototype, then we could just use a local variable to store the data, but since we are placing our shared functions on the prototype in these samples, we need to find another way to hide this data. A simple solution is to create an empty
cache function and store our data as property off of it. This will prevent calls to
ko.toJSON from capturing this data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
In this scenario, we will let edits persist to our observables and in the event that a user chooses to cancel, we will refresh our model with the original data.
When a user accepts the data, we need to make sure that we update the cached data with the current state of the model. For example, we could simply add a
accept function to our prototype that does:
1 2 3
We now have the latest data cached, so the next time that a user cancels it will be reverted back to this updated state.
Sometimes in an editor, we would not want the currently edited observables to affect the rest of the UI until the user chooses to accept the changes. In this case, we can use these same methods to create a copy of the item for editing and then apply it back to the original. Here is how our overall view model might look:
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 34 35 36 37 38
So, we make edits to a copy of the original and on an accept we apply those edits back to the original. We can use
update function to apply the changes. In this scenario, the
revertItem function can simply throw away the copied item and clear our the selected value.
If you were going to reuse this technique often, then you could even create an extension to an observableArray that adds the appropriate observables and functions off of the observableArray itself like in this sample.
Updating from server
update function is also a handy way to apply updates from the server to our existing view model. In our case, we would likely need to add an
id to the
Item objects, so that we can identify which object needs updating. This works very well with something like SignalR or socket.io to keep various clients in sync with each other. This is an easier pattern than trying to swap items in an array or update specific properties one by one.
Editors with accept/cancel options are a common scenario in a Knockout.js application. By separating the creation of observables/computeds from poplating their values, we can commit, reset, and update models by feeding a version of the data through our
update function. When you think in terms of the plain JS object that you can get out of or put into a model, it makes many of these scenarios easy to handle.