16

I want to be able to bind a single and double click event to a span of text. I know I can use

data-bind ="event: { dblclick: doSomething }

for a double click, but I also need the ability to perform a different function on single click. Any suggestions?

Andrew Whitaker
  • 124,656
  • 32
  • 289
  • 307
bdev
  • 2,060
  • 5
  • 24
  • 32
  • 1
    You can simply attach an event handler for the click event, but that will result into most browsers only triggering the click event (twice, if you double click). This is a general limitation in JavaScript. You're gonna do some tricks like this in the single-click event handler, to "fake" a double click event: http://stackoverflow.com/questions/6330431/jquery-bind-double-click-and-single-click-separately – Niko Jun 13 '12 at 15:58

5 Answers5

27
<div data-bind="singleClick: clicked, event : { dblclick: double }">
    Click Me
</div>

This will filter out the single clicks that are also double clicks.

ko.bindingHandlers.singleClick= {
    init: function(element, valueAccessor) {
        var handler = valueAccessor(),
            delay = 200,
            clickTimeout = false;

        $(element).click(function() {
            if(clickTimeout !== false) {
                clearTimeout(clickTimeout);
                clickTimeout = false;
            } else {        
                clickTimeout = setTimeout(function() {
                    clickTimeout = false;
                    handler();
                }, delay);
            }
        });
    }
};

Here is a demo.

rudolph9
  • 8,021
  • 9
  • 50
  • 80
madcapnmckay
  • 15,782
  • 6
  • 61
  • 78
12

The above answers were very helpful, but didn't give the exact solution I believe the OP was after: a simple Knockout binding that allows for exclusive single and double click events. I know the post was a year ago, but I found this article when looking to do the same thing today, so I'm posting in case this answer is useful for someone else.

The below example seems to fit the OPs requirement and may save someone some time (disclaimer: limited cross browser testing). JSFiddle: http://jsfiddle.net/UxRNy/

Also, there are the questions around whether you should use this in the first place (mobile browsers, slowing down the page, accessibility etc.) - but that is another post (e.g. https://ux.stackexchange.com/questions/7400/should-double-click-be-avoided-in-web-applications)

Example view usage:

<div data-bind="singleOrDoubleClick: { click: singleClick, dblclick: doubleClick }">
    Click or double click me...
</div>

Binding:

ko.bindingHandlers.singleOrDoubleClick= {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var singleHandler   = valueAccessor().click,
            doubleHandler   = valueAccessor().dblclick,
            delay           = valueAccessor().delay || 200,
            clicks          = 0;

        $(element).click(function(event) {
            clicks++;
            if (clicks === 1) {
                setTimeout(function() {
                    if( clicks === 1 ) {
                        // Call the single click handler - passing viewModel as this 'this' object
                        // you may want to pass 'this' explicitly
                        if (singleHandler !== undefined) { 
                            singleHandler.call(viewModel, bindingContext.$data, event); 
                        }
                    } else {
                        // Call the double click handler - passing viewModel as this 'this' object
                        // you may want to pass 'this' explicitly
                        if (doubleHandler !== undefined) { 
                            doubleHandler.call(viewModel, bindingContext.$data, event); 
                        }
                    }
                    clicks = 0;
                }, delay);
            }
        });
    }
};

The above was put together from a combination of the examples above and examples from here: https://gist.github.com/ncr/399624 - I just merged the two solutions.

Community
  • 1
  • 1
chrisjones
  • 341
  • 3
  • 4
  • The problem with this solution for me was that the single-click isn't processed until after the delay used to detect double-click. Since I wanted single-click to select a row and double-click to open details, this caused a .2 second delay in row selection which was undesirable. It may be ok for many scenarios. – dhochee Jan 06 '16 at 19:37
  • @dhochee - sounds like you just want a click binding with a flag inside it then - you're not really after double click as a distinct event. This worked great for me, thanks Chris – Hayden Crocker Aug 02 '16 at 19:17
5

@madcapnmckay provided a great answer, below is a modified version using the same idea to provide double click. And by using latest version of knockout, the vm get passed to handler as context. This can works at the same time with single click.

<div data-bind="doubleClick: clicked">
    Double click Me
</div>

--

ko.bindingHandlers.doubleClick= {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var handler = valueAccessor(),
            delay = 200,
            clickTimeout = false;

        $(element).click(function() {
            if(clickTimeout !== false) {
                handler.call(viewModel);
                clickTimeout = false;
            } else {        
                clickTimeout = setTimeout(function() {
                    clickTimeout = false;
                }, delay);
            }
        });
    }
};
Morio
  • 8,463
  • 5
  • 25
  • 29
  • This does not work for me. I get something like "Undefined has no method: call()" on handler.call(); –  Dec 02 '13 at 20:08
  • I like this solution, but it does not pass the normal Model and Event parameters to your function. – Grandizer Apr 23 '14 at 15:01
  • Okay, figured out what I needed to get Model and Event properties to show up like a normal KO event. Just change the like that starts with `$(element).click(function() {` to `$(element).click(function (e) {` **note the e** and then the `handler.call(viewModel);` to `handler.call(this, bindingContext.$data, e);` The `bindingContext.$data` is the preferred way KO wants it based on [this](http://knockoutjs.com/documentation/custom-bindings.html) starting with KO 3.0. – Grandizer Apr 23 '14 at 15:51
4

First, I wouldn't recommend click binding at all. Instead you should use "click" and "dblclick" handlers from jQuery:

$(someParentElement).on('click', 'your span selector', function (event) {
    var myViewModelFragment = ko.dataFor(this);
    // your code here
});

$(someParentElement).on('dblclick', 'your span selector', function (event) {
    var myViewModelFragment = ko.dataFor(this);
    // your code here
});

Edit: see also Niko's suggestion regarding supporting both single and double clicks. Basically, you should count the number of clicks manually and call different functions accordingly. I assumed jQuery handles this for you but, unfortunately, it doesn't.

Community
  • 1
  • 1
  • 3
    Why wouldn't you recommend that? – Niko Jun 13 '12 at 16:00
  • Mostly because it behaves similarly to plain-old-js `onclick` attribute. It attaches event handlers directly to the element. There are two problems with that. First, if you have many such elements you'll end up with many handlers. JS runtime has to optimize them separately and it increases memory use. Second, if you attach a handler via Knockout `click` to some element and this element has a child with _live_ `click` event attached to it then ko-click will shadow the live-click. Live click won't be called and you won't get the right behavior. – Andrei Андрей Листочкин Jun 13 '12 at 16:16
  • Steve Sanderson also recommends it: http://blog.stevensanderson.com/2011/12/21/knockout-2-0-0-released/ (see point 4. Cleaner event handling) – Andrei Андрей Листочкин Jun 13 '12 at 16:17
  • 8
    @Andrew - I wouldn't say Steve recommends this. He gives you the tools to do it both ways. He discouraged the inline function type, however. I accept what you are saying re the number of handlers. However the disadvantage of your technique is that it breaks separation of concerns between the viewmodel and the view. I would propose a third way. Use click when items are singular and write a custom **delegate** binding that could be placed on parent dom elements. That way we get the best of both worlds and keep separation. – madcapnmckay Jun 13 '12 at 17:01
  • @Andrew - Also re your second point, can you give an example of the behavior you mention. I didn't understand so tried to reproduce and everything works fine. http://jsfiddle.net/madcapnmckay/5LtEa/1/ – madcapnmckay Jun 13 '12 at 17:36
  • Oh, my bet. Here's a issue I found https://github.com/SteveSanderson/knockout/issues/28 and the corresponding bit in the docs: http://knockoutjs.com/documentation/click-binding.html (**Note 4: Preventing the event from bubbling**) It seems like I started using Knockout before this feature was introduced and assumed that `click` binding prevents bubbling. Compare your fiddle to this: http://jsfiddle.net/listochkin/5LtEa/2/ Thanks, I learned something today! – Andrei Андрей Листочкин Jun 14 '12 at 08:17
  • Regarding your first point. Generally I prefer using bindings for properties only and bind event handlers via jQuery or Sammy.js or what have you. There're two reasons for that. One is that it gives me a separation between _property bindings_ and _actions_, the former being passive agents and the later being active ones. – Andrei Андрей Листочкин Jun 14 '12 at 08:23
  • Second reason is that in general we need to react to way too many different kinds of events: click, mouse-overs, touch events, keyboard, etc. Writing custom bindings for those events is tedious and quite difficult to get right since these events can step on each other quite often. Like, for example, the problem that @bdev has run into with clicks and double-clicks. – Andrei Андрей Листочкин Jun 14 '12 at 08:32
  • This is a terrible idea, because you need to track selectors and manually add and remove event handlers. In a complex app, this will become unmaintainable. – leftclickben Oct 20 '14 at 06:01
  • 2
    The only reason I'm not downvoting is it that it's based on an older version of knockout. Newer versions actually use, by default, jquery events if jquery is available. There's also a setting that can switch the system over to native events. – bug-a-lot Mar 01 '16 at 17:15
  • @bug-a-lot oh, feel free to edit the answer. I don't work with Knockout anymore so I'm not aware of latest improvements the team made. – Andrei Андрей Листочкин Mar 11 '16 at 20:21
0

Here is my programming solution to the question:

var ViewModel = function() {
  var self = this;
  this.onSingleClick = function() {
    self.lastTimeClicked = undefined;
    console.log('Single click');
  };

  this.onDoubleClick = function() {
    console.log('Double click');
  };

  this.timeoutID = undefined;

  this.lastTimeClicked = undefined;
  this.clickDelay = 500;
  this.clickedParagraph = function(viewModel, mouseEvent) {
    var currentTime = new Date().getTime();
    var timeBetweenClicks = currentTime - (viewModel.lastTimeClicked || currentTime);
    var timeToReceiveSecondClick = viewModel.clickDelay - timeBetweenClicks;

    if (timeBetweenClicks > 0 && timeBetweenClicks < viewModel.clickDelay) {
      window.clearTimeout(viewModel.timeoutID); // Interrupt "onSingleClick"
      viewModel.lastTimeClicked = undefined;
      viewModel.onDoubleClick();
    } else {
      viewModel.lastTimeClicked = currentTime;
      viewModel.timeoutID = window.setTimeout(viewModel.onSingleClick, timeToReceiveSecondClick);
    }
  };
};

ko.applyBindings(new ViewModel(), document.getElementById("myParagraph"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-debug.js"></script>
<p id="myParagraph" data-bind="click: clickedParagraph">Click me</p>

Every "double click" is created from two single clicks. If there is a "double click", we have to make sure that the event handler for the first single click does not get executed (this is why I use window.setTimeout & window.clearTimeout). When setting the timers, we also have to consider that the first click on an element can be already a double click.

In my code I set the clickDelay to 500ms. So two clicks within 500ms are recognized as a "double click". You can also increase this value to test the behaviour of my clickedParagraph function.

Benny Code
  • 51,456
  • 28
  • 233
  • 198