On the forums for Knockout, there have been several questions related to temporarily preventing the changes to an observable from being sent out to subscribers. Typically, someone wants to apply many changes to their view model, perhaps as the result of an update call to the server, without having subscriptions triggered until the very end.
This behavior is not currently built into Knockout, but it is still possible to achieve this in a fairly straightforward way. Suppose we have a simple view model that looks like:
123456789
varviewModel={users:ko.observableArray(usersFromServer)}//just the active usersviewModel.activeUsers=ko.computed(function(){returnko.utils.arrayFilter(viewModel.users(),function(user){returnuser.isActive();});},viewModel);
If we receive updates from the server that add/remove users and/or modify the status of each user, then the activeUsers computed observable will be re-evaluated on each and every one of those changes.
To add the ability to pause activeUsers from being re-evaluated on every change, we can create an augmented computed observable with this added functionality.
12345678910111213141516171819202122232425262728
//wrapper for a computed observable that can pause its subscriptionsko.pauseableComputed=function(evaluatorFunction,evaluatorFunctionTarget){var_cachedValue="";var_isPaused=ko.observable(false);//the computed observable that we will returnvarresult=ko.computed(function(){if(!_isPaused()){//call the actual function that was passed inreturnevaluatorFunction.call(evaluatorFunctionTarget);}return_cachedValue;},evaluatorFunctionTarget);//keep track of our current value and set the pause flag to release our actual subscriptionsresult.pause=function(){_cachedValue=this();_isPaused(true);}.bind(result);//clear the cached value and allow our computed observable to be re-evaluatedresult.resume=function(){_cachedValue="";_isPaused(false);}returnresult;};
We can now replace our computed observable with a pauseableComputed:
When we want to apply many updates at once, we can wrap our actual updates with the pause() and resume() calls.
1234567
//callback from AJAX call to retrieve updatesviewModel.onUpdateUsersSuccess=function(){this.activeUsers.pause();//add or remove users//update status flags for usersthis.activeUsers.resume();}.bind(viewModel);
The key fact that makes this work is that dependency detection is performed each time that a computed observable is re-evaluated. This means that the actual dependencies for a computed observable can change over time. In our case, the dependencies will look like:
When we call pause(), the _isPaused flag is set, which forces our computed observable to be re-evaluated. Now, we release our subscriptions to the users array and to the isActive flag of each user, but we do keep a cached copy of our latest value to return while we are paused.
When we call resume(), the cached value is thrown away and our computed observable is re-evaluated, because our only current dependency, the _isPaused flag, has now changed.
From what I have seen, the most common scenario is that a computed observable depends on many observables that you want to update en masse before re-evaluating the dependentObservable. However, at the bottom of the code for this sample, there is also an implementation that supports pausing writeable computed observables and one for pausing an observableArray as well (would be the same pattern for an observable).
As a follow up to the post on Knockout utilities, I thought it might be useful to mention one less than obvious trick that you can use in conjunction with ko.toJSON.
When preparing your data to be sent back to the server, typically you will call ko.toJSON on your view model or at least part of your view model. This first calls ko.toJS to give you a clean copy of your objects with all observables unwrapped into plain properties. Then it converts this clean object into JSON using JSON.stringify, which is implemented natively by your browser (or with a library like json2.js for older browsers).
However, it is not uncommon for your objects to contain computed observables or other observables that aren’t meant to go back to the server. Maybe your server-side framework can’t handle the extra properties or you don’t want to wastefully send the extra bytes on the wire. In some cases you might even have a child with a reference to its parent, which creates a circular reference that causes problems in the conversion to JSON.
While you could walk your object tree and delete any unwanted properties, there is actually an easier way. JSON.stringify will check if each object has a toJSON method and execute it first before serializing the object to its JSON representation. The name of the function “toJSON” is a bit misleading, as the function should return an object that is ready to be serialized into JSON, not your own attempt at a JSON string representing the object.
Suppose that you are constructing a simple Person object that looks like:
Maybe your server-side code is not expecting the full property or you just want to avoid sending it to be efficient. To control which properties are actually serialized, you can implement a toJSON function for your object. It could look something like:
12345
Person.prototype.toJSON=function(){varcopy=ko.toJS(this);//easy way to get a clean copydeletecopy.full;//remove an extra propertyreturncopy;//return the copy to be serialized};
There are certainly many ways that you could implement this function, but the main objective is to return a version of the object that is ready to serialized. Now, your JSON would look like:
1
{"first":"John","last":"Smith"}
What if you would prefer to only send back the full name? Suppose that you have an array of Person objects and in JSON you want to convert it to an array of strings containing just the full names. In that case you could implement toJSON like:
I used unwrapObservable, because if this happens to get called as part of ko.toJSON, then all of the observables would already be plain properties. This way we are flexible enough to handle it either way. If we had an array of Person objects, when converted to JSON they would look like:
1
["Ted Johnson","Jon Johnson","Rachel Johnson"]
Now, we have just an array of strings (no property names) to send to the server.
Implementing the toJSON function for your objects can be a nice way to avoid manually mapping your view model. You can easily transform your data into whatever format is most suitable to be sent to your server.
Today the 1.2.0 version of Knockout was released. Check out this postfor details on the changes that are included. I am very excited to not have to keep saying this feature was not in the 1.12 release, you will need the latest code! Lots of great improvements in 1.2 including serious performance fixes, new bindings (event, attr, and html), and my personal favorite new feature, writable computed observables. I think that this is a really solid release and I look forward to the future of this great library.
The 1.2.0 script is available here. If you are working in Visual Studio, then you can also get it via NuGet (Install-Package knockoutjs).
Update #2: Check out this post for a new look at this topic and take a look at the plugin here.
Update: In Knockout 2.0, it is now possible to use ko.dataFor and ko.contextFor instead of tmplItem and use this technique with native templates. Documentation here.
Prior to working with Knockout, I had recently grown fond of event delegation in JavaScript where an event listener is attached to a parent container and events from its children bubble up to this handler. I had primarily been using jQuery’s live() or its more precise cousin delegate() to handle this type of interaction in a fairly painless way.
There are several fairly obvious advantages to using event delegation in cases where you would be adding listeners for lots of events (like for every cell in a large grid):
New elements added dynamically to the container can be handled without any additional code. Elements can be removed without having to manage unbinding handlers.
Less overhead (memory, processing) than wiring up events to every element.
Gives you the flexibility to use one handler to respond to events at multiple levels or from different kinds of elements.
At first glance, it seemed like event delegation didn’t quite fit the Knockout model. You certainly could set a handler on a parent element using the click or event binding. This would allow you to respond to actions on the children, but you would have no connection to the actual data that was used to generate the child element. You could also just use something like jQuery’s live() or delegate() functionality, but you would not get the integration with your view model data that you are used to in the bindings.
tmplItem - a hidden gem in jQuery Templates
In Knockout, the majority of the child content is typically rendered through templates, using the jQuery Templates plug-in. One lesser publicized feature of the plug-in is the tmplItem function. For any element generated by a template, this function allows you to get back to the data that was used to render the template.
For example,
123456
<divdata-bind="template: { name: 'nameTmpl', data: person }"></div><script id="nameTmpl"type="text/html"><spanid="name">${firstName}${lastName}</span></script>
So, using the tmplItem function, we are actually able to retrieve some context from an element that was rendered by a template. This is the piece that we were missing to connect our delegated event with the model data related to our child element.
One note: if you are using {{each}} syntax, tmpItem() will still return the data that was the context of the entire template. You can use the template binding or the {{tmpl}} tag to help achieve the proper context for your elements.
Are there benefits to doing event delegation in Knockout?
Wiring up events for dynamically created elements is generally not a problem in Knockout, as we are usually generating the content in templates and can easily add our event handlers declaratively. The other benefits of event delegation still stand though, especially in the case that we have a need to wire up a large number of handlers on our page. I can also see one other small advantage: the first time that you wrote a removeItem method, you might have been a little disappointed that you had to use an anonymous function like:
Update: Knockout 2.0 eliminates this pain point by automatically passing the data as the first argument.
With event delegation, we could possibly add a binding to a parent element and just call a method off of the view model passing the actual data that was returned through tmplItem as part of the binding.
Creating a custom binding for delegated clicks
Here is what I started with:
1234567891011121314151617
ko.bindingHandlers.delegatedClick={init:function(element,valueAccessor,allBindings,viewModel){varcallback=valueAccessor();//build a function that will call our method and pass the real data to itvarrealValueAccessor=function(){returnfunction(event){varelement=event.target;//get real contextvarcontext=$(event.target).tmplItem().data;returncallback.call(viewModel,context,event);}}//call real click binding's initko.bindingHandlers.click.init(element,realValueAccessor,allBindings,viewModel);}};
So, we are just trying to prepare a function that we can pass on to the real click binding. The function uses tmplItem to retrieve the actual data object related to the element that was clicked and then executes the callback that was passed in through the binding. The actual data object is passed to the function. Now, I can put a binding at the table level to remove an item when I click on a “Delete” button in a cell.
This works, but removeItem will get called for Any click inside the table. It doesn’t matter if we click the button, another cell, or a header cell, we will always attempt to run the method. This is certainly not what we want. To limit the elements that trigger our actual method, we can pass in a selector to the binding. As we already have a dependency on jQuery through the jQuery Templates plug-in, we can use jQuery’s .is functionality to make sure that we only proceed when there is a match. Now we pass an object containing our function and a selector to the binding:
In our binding handler, we can make a small modification to check if the element matches our selector.
12345
if($(element).is(options.selector)){//get real contextvarcontext=$(event.target).tmplItem().data;returnoptions.callback.call(viewModel,context,event);}
This works properly when calling parent functions that receive the children as a parameter. However, what if you want to actually call a method off of the child object in this manner? I found that based on the way that Knockout does parsing of the bindings, something like this will not work:
This will error out before it gets to my binding, unless childFunction actually exists on the data that is the context of the table’s data binding (the view model passed to applyBindings, unless you are in a template). We can still make this work though by passing the function name as a string and reconciling it in the binding.
For a minute, I was disappointed that it requires passing a string for the callback, but then I came to the realization that really the whole data-bind attribute is just a string anyways.
Now, I can check for this in the binding:
1234567
//get real contextvarcontext=$(event.target).tmplItem().data;if(typeofoptions.callback==="string"&&typeofcontext[options.callback]==="function"){returncontext[options.callback].call(context,event);}returnoptions.callback.call(viewModel,context,event);
If the callback supplied in the binding is a string, then we will call the function off of the child object passing just the event to it. If the function is not a string, then we will call it off of the parent object and pass the child object and the event to it. This allows us to decide whether we want to use a method on our view model or a method on our child object.
One limitation that I immediately ran into was that I might want to pass in multiple click handlers that use different selectors. We should be able to pass in a list of handlers to add as an array.
So, my final version looked like:
1234567891011121314151617181920212223
ko.bindingHandlers.delegatedClick={init:function(element,valueAccessor,allBindings,viewModel){varclicks=valueAccessor();ko.utils.arrayForEach(clicks,function(options){varrealValueAccessor=function(){returnfunction(event){varelement=event.target;if($(element).is(options.selector)){//get real contextvarcontext=$(event.target).tmplItem().data;if(typeofoptions.callback==="string"&&typeofcontext[options.callback]==="function"){returncontext[options.callback].call(context,event);}returnoptions.callback.call(viewModel,context,event);}}}//call real click binding's initko.bindingHandlers.click.init(element,realValueAccessor,allBindings,viewModel);});}};
Finally, it might be nice to respond to other events in this way, like mouseover and mouseout. Based on the existing event binding, here is a more generic version of the delegatedClick binding:
//binding to do event delegation for any eventko.bindingHandlers.delegatedEvent={init:function(element,valueAccessor,allBindings,viewModel){vareventsToHandle=valueAccessor()||{};//if a single event was passed, then convert it to an arrayif(!$.isArray(eventsToHandle)){eventsToHandle=[eventsToHandle];}ko.utils.arrayForEach(eventsToHandle,function(eventOptions){varrealCallback=function(event){varelement=event.target;varoptions=eventOptions;//verify that the element matches our selectorif($(element).is(options.selector)){//get real contextvarcontext=$(event.target).tmplItem().data;//if a string was passed for the function, then assume it is a function of the real contextif(typeofoptions.callback==="string"&&typeofcontext[options.callback]==="function"){returncontext[options.callback].call(context,event);}//if a function was passed, then give it the real context as a paramreturnoptions.callback.call(viewModel,context,event);}}varrealValueAccessor=function(){varresult={};result[eventOptions.event]=realCallback;returnresult;}ko.bindingHandlers.event.init(element,realValueAccessor,allBindings,viewModel);});}};
We could bind to the mouseover and mouseout events of our header cells like:
I can also now rewrite the delegatedClick binding to be a wrapper to this generic binding.
Here is a grid-like sample that shows using event delegation when binding to a function on the view model, binding to a function on a cell’s related object, and binding to the mouseover and mouseout events on the header cells:
In Knockout, event delegation is probably not necessary in most cases. However, I think that there are scenarios where this could be useful and beneficial, especially when dealing with an extremely large number of elements that need their events handled in similar ways.
Steve Sanderson, the creator of KnockoutJS, presented a great introduction to the library at MIX11 this week. The talk was just the right length and had a good pace. I think that this would be an outstanding video to point your boss, co-workers, or friends at to help explain what the heck this Knockout thing is that you are so excited about!