Feb. 2, 2016 in KnockoutJS
While working on a project the other day I was presented with an interesting issue. In our project we are using Knockout JS for 2-way data binding between a set of data and a Leaflet JS map. With all of this data, we have many different click events needed so the user can interact with and manipulate the data in different ways.
I needed a way to have 2 different bindings on one item for both a single and a double click. The single click would open a popup, and the double click would open a sidebar with more data about that item. By default, knockout only supports one type of click binding per item. Basically I wanted something like this.
<a href="#" data-bind="click: {single: onClick, double: onDoubleClick}"> <p data-bind="text: someAtrribute"></p> </a>
How can this be done? The answer is a custom binding! So I went to researching. I came across a stack overflow post that had an example of a single or double click binding. I decided to take it a step further and override the default click binding to suit my needs. Here's what I came up with.
ko.bindingHandlers.click = { init: function(element, valueAccessor, allBindingsAccessor, viewModel, context) { var accessor = valueAccessor(); var clicks = 0; var timeout = 200; $(element).click(function(event) { if(typeof(accessor) === 'object') { var single = accessor.single; var double = accessor.double; clicks++; if (clicks === 1) { setTimeout(function() { if(clicks === 1) { single.call(viewModel, context.$data, event); } else { double.call(viewModel, context.$data, event); } clicks = 0; }, timeout); } } else { accessor.call(viewModel, context.$data, event); } }); } };
This binding overrides the default click binding. It will still work as the default does, but will also take a Javascript object with single and double click attributes. It does this by checking the type of the valueAccessor
. If it is an object, it will look for the single
and double
attributes. If it is a function, it will act like the default click binding.
Here's a quick example of how it can be used.
First we create a basic view model that will hold our points and has function for both single and double clicks.
var MainViewModel = function(points) { var self = this; self.points = ko.observable(points); } var Point = function(lat, lng) { var self = this; self.lat = lat; self.lng = lng self.onClick = function(){ console.log("single click"); } self.onDoubleClick = function(){ console.log("double click"); } } var viewModel = new ViewModel([ new Point(80, -20), new Point(60, -40), ]); ko.applyBindings(viewModel);
Here is what our html would look like for a single click binding. It will still work exactly as the default click binding does.
<div class="list-group" data-bind="foreach: points"> <a href="#" data-bind="click: onClick"> <p data-bind="text: lat"></p> <p data-bind="text: lng"></p> </a> </div>
And here is where the magic happens. Note: it is the same binding, but instead of a single function, you can pass it an object with functions to call for single and double clicks!
<div class="list-group" data-bind="foreach: points"> <a href="#" data-bind="click: {single: onClick, double: onDoubleClick}"> <p data-bind="text: lat"></p> <p data-bind="text: lng"></p> </a> </div>
The only drawback to using this binding is that there is a 200ms delay on single clicks due to it waiting for another click, but that time is fairly negligible in my situation.