Recently, I was thinking about the best way to implement a generic dirty flag in KnockoutJS and had a few thoughts. The basic idea is that when any change is made to an object or possibly the entire view model, we want a boolean to be set. With this flag, we are now able to recognize that we might need to send some or all of our data back to the server.
Implementing a basic dirty flag
In order to determine that a change has been made to our object, we really need to be notified when anything changes. We need to create a subscription to every observable on our object. Conveniently, we have a utility function,
ko.toJS, that given a root object will access all of the observable properties. So, if we create a computed observable that executes
ko.toJS, then we will automatically set up subscriptions to all of our observables. Then, we can subscribe to this single computed observable and update our flag when it changes.
One issue with this method is that every change made to any of our observables will trigger the computed observable to be re-evaluated, which will run
ko.toJS again. This could get expensive, if we are doing this at the view model level. Fortunately, it is easy enough for us to drop our subscriptions after the first change is detected. So, a basic dirty flag might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
This function will return a computed observable that depends on an internal observable flag and initially all of the observable properties on the root object passed in via
ko.toJS. There is also an internal subscription created to this computed observable. When it changes, the internal boolean is set and upon re-evaluation, the computed observable will skip running
ko.toJS and will be left only depending on this internal observable.
One thing to remember is that computed observables are evaluated for the first time when they are created. So, in order for our dirty flag to subscribe to all of our observables, we need to add it last.
It seems like this approach could be sufficient for many cases. Even putting this flag on an entire view model should be fine, as the subscription to all observables will be dropped as soon as it is dirty.
Adding some smarts to the flag
For my scenario, I wanted a little bit of additional functionality. It would be nice if the dirty flag would set itself back to false if the changes are reverted. Additionally, if I receive updates from the server, then I want to reset the dirty flag to clean after the updates have been applied. Finally, if I add a new item, we should be able to mark it as dirty right away.
There are a couple of challenges that I ran into while implementing these features. First, if the computed observable calls
ko.toJS any time after it has been added, then it recursively tries to evaluate itself. Second, if the computed observable depends on itself, then when a change is made it will get into an infinite loop.
I considered a few different ways to solve this issue.
- removing the computed observable from its parent prior to calling
ko.toJS in the read function and then adding itself back. This works, but it seemed like a little too much code to understand what my property name is on the parent, delete the property, and add it back afterwards.
- manipulating the computed observable to pretend that it is not an observable until after ko.toJS runs. I was unable to get this working properly and it required access to properties of KO that are considered private.
- writing a new version of
ko.toJSthat skips some properties.
- making the object a function and adding the computed observable to the function object. This means you have to bind to it as
ko.toJSruns, it will just see a plain function and ignore it.
I decided to go with the last option, as it seemed to be safe and straightforward. The inconvenience of having to bind to the sub-property is minor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
- This version of the flag does not give up its subscriptions after it becomes dirty. It will evaluate each change against the original version of the data to determine if it is still dirty. So, this flag is likely most appropriate for small objects rather than an entire view model.
- If a new item is added, you can mark the flag as dirty immediately.
resetmethod is provided to take a new snapshot of the current state. This can be used when applying updates from the server or after saving your data to the server, if you still allow for additional updates. Here is a sample using this flag at an item level:
Link to full sample on jsFiddle.net