Handling ng-click and ng-dblclick on the same element with AngularJS
Asked Answered
J

6

38

I was looking for both single and double-click event handling with AngularJS, since AngularJS always fires only the ng-click event even if there is ng-dblclick directive set for our element.

Here is some working code for those who seek solution:

JS:

function MyController($scope) {

    var waitingForSecondClick = false;
    $scope.singleClickAction = function() {

        executingDoubleClick = false;
        if (waitingForSecondClick) {
            waitingForSecondClick = false;
            executingDoubleClick = true;
            return $scope.doubleClickAction();
        }
        waitingForSecondClick = true;

        setTimeout(function() {
            waitingForSecondClick = false;
            return singleClickOnlyAction();
        }, 250); // delay

        /*
         * Code executed with single AND double-click goes here.
         * ...
         */

        var singleClickOnlyAction = function() {
            if (executingDoubleClick) return;

            /*
             * Code executed ONLY with single-click goes here.
             * ...
             */

        }

    }

    $scope.doubleClickAction = function() {

        /*
         * Code executed ONLY with double-click goes here.
         * ...
         */

    };

}

HTML:

<div ng-controller="MyController">
    <a href="#" ng-click="singleClickAction()">CLICK</a>
</div>

So my question is (since I'm an AngularJS newbie): could somebody more experianced write some nice directive for handling those both events?

In my opinion the perfect way would be to change the behaviour of ng-click, ng-dblclick and add some "ng-sglclick" directive for handling single-click-only code. Don't know if it is even possible, but I'd find it very useful for everyone.

Feel free to share your opinions!

Jamieson answered 7/12, 2013 at 17:38 Comment(1)
not really much else you can do than use approach you already are using. Is not angular related, is same issue trying to combine these events regardless of using framework or notKenney
G
26

Greg's answer is definitely closest to the cleanest answer. I'm going to build on his answer to come up with a version where no new code needs to be written, and no new injections need to be used in your controller.

The first thing to question is why timeouts are used to hack around these kinds of problems. Essentially, they're used to make a function skip the rest of the current event loop so that the execution is clean. In angular, however, you are actually interested in how the digest loop works. It's almost the same as your classic event handler, except for some minor differences which make it great for UX. Some tools that you have on hand to mess around with the order of function execution include scope.$eval, scope.$evalAsync,scope.$apply, and scope.$applyAsync.

I believe the $applys will kick off another digest loop, which leaves the $evals. $eval will run whatever code you include immediately with the context of the current $scope and $evalAsync will queue your function to be run at the end of the digest loop. Essentially, $evalAsync is a cleaner version of $timeout with one big difference — the first has context and exists on the scope!

This means that you can, actually, handle ng-click and ng-dblclick on the same element. Note, however, that this will still trigger the single-click function before the double-click function. This should be sufficient:

<div ng-controller="MyController">
    <a href="#"
       ng-click="$evalAsync(singleClickAction())"
       ng-dblclick="doubleClickAction()">
       CLICK
    </a>
</div>

Here's a jsfiddle with the intended functionality using Angular 1.6.4.

Guidry answered 11/7, 2015 at 2:6 Comment(5)
This is one of the best answer and i would recommend this should be the answer for this question in optimized way too. @JamiesonHoey
I'm using Angular v1.6.4 and this doesn't work for me.Truffle
@AugustoBarreto - I don't do angular anymore, so I can't really promise too much maintenance on this question, but here's a jsfiddle of the intended effect: jsfiddle.net/stites/yxws0tst If something is broken for you, can you be more specific and/or create a demo of what you aren't seeing? That way we can keep this answer up-to-date.Guidry
@Guidry What happens to me also happens on your sample. When I double-click the text, the singleClickAction is executed followed by the doubleClickAction. I think it should only trigger the doubleClickAction. At least, it's what I need. I ended up following another path after reading https://mcmap.net/q/410779/-how-can-i-add-a-click-and-double-click-control-to-a-row-with-angularjsTruffle
@AugustoBarreto yeah, I think it depends on your use-case. I don't think I would ever want to overload a single element unless it made some sort of sense, but for some reason I guess I had to in order to brew up this answer. I'm also not sure how valid that JQuery sentence is since angular has its own way of processing events that is cross-platform. Anyhow! I'm glad you figured out a better way to go about this. I'll update the answer to address your concerns.Guidry
O
29

You could just write your own. I took a look at how angular handled click and modified it with code I found here: Jquery bind double click and single click separately

<div sglclick="singleClick()" ng-dblClick="doubleClick()" style="height:200px;width:200px;background-color:black">


mainMod.controller('AppCntrl', ['$scope', function ($scope) {
    $scope.singleClick = function() {
      alert('Single Click');
    }
    $scope.doubleClick = function() {
      alert('Double Click');
    }
}])


mainMod.directive('sglclick', ['$parse', function($parse) {
    return {
        restrict: 'A',
        link: function(scope, element, attr) {
          var fn = $parse(attr['sglclick']);
          var delay = 300, clicks = 0, timer = null;
          element.on('click', function (event) {
            clicks++;  //count clicks
            if(clicks === 1) {
              timer = setTimeout(function() {
                scope.$apply(function () {
                    fn(scope, { $event: event });
                }); 
                clicks = 0;             //after action performed, reset counter
              }, delay);
              } else {
                clearTimeout(timer);    //prevent single-click action
                clicks = 0;             //after action performed, reset counter
              }
          });
        }
    };
}])

Here's an example

Plunker

Ormiston answered 7/12, 2013 at 19:5 Comment(3)
This is a very good solution, but it does force a delay before treating a click as a single click. If you watch the UI for e.g. Windows explorer, you'll see that the single-click action happens regardless of whether it's a single- or double-click. Their trick is to make sure that nothing that happens as a result of a single-click could interrupt the intention of the double-click. In other words, if you just remove the timeout and handle the events appropriately you may get a better user experience.Tsan
You should be using $timeout provided by angular and $timeout.clear(timer) so that you don't need to do $apply and you can flush it in unit testing.Waylan
One problem with this approach is that the original event is lost when the callback gets triggered, so you cannot do things like event.preventDefault() or event.stopPropagation()Rate
G
26

Greg's answer is definitely closest to the cleanest answer. I'm going to build on his answer to come up with a version where no new code needs to be written, and no new injections need to be used in your controller.

The first thing to question is why timeouts are used to hack around these kinds of problems. Essentially, they're used to make a function skip the rest of the current event loop so that the execution is clean. In angular, however, you are actually interested in how the digest loop works. It's almost the same as your classic event handler, except for some minor differences which make it great for UX. Some tools that you have on hand to mess around with the order of function execution include scope.$eval, scope.$evalAsync,scope.$apply, and scope.$applyAsync.

I believe the $applys will kick off another digest loop, which leaves the $evals. $eval will run whatever code you include immediately with the context of the current $scope and $evalAsync will queue your function to be run at the end of the digest loop. Essentially, $evalAsync is a cleaner version of $timeout with one big difference — the first has context and exists on the scope!

This means that you can, actually, handle ng-click and ng-dblclick on the same element. Note, however, that this will still trigger the single-click function before the double-click function. This should be sufficient:

<div ng-controller="MyController">
    <a href="#"
       ng-click="$evalAsync(singleClickAction())"
       ng-dblclick="doubleClickAction()">
       CLICK
    </a>
</div>

Here's a jsfiddle with the intended functionality using Angular 1.6.4.

Guidry answered 11/7, 2015 at 2:6 Comment(5)
This is one of the best answer and i would recommend this should be the answer for this question in optimized way too. @JamiesonHoey
I'm using Angular v1.6.4 and this doesn't work for me.Truffle
@AugustoBarreto - I don't do angular anymore, so I can't really promise too much maintenance on this question, but here's a jsfiddle of the intended effect: jsfiddle.net/stites/yxws0tst If something is broken for you, can you be more specific and/or create a demo of what you aren't seeing? That way we can keep this answer up-to-date.Guidry
@Guidry What happens to me also happens on your sample. When I double-click the text, the singleClickAction is executed followed by the doubleClickAction. I think it should only trigger the doubleClickAction. At least, it's what I need. I ended up following another path after reading https://mcmap.net/q/410779/-how-can-i-add-a-click-and-double-click-control-to-a-row-with-angularjsTruffle
@AugustoBarreto yeah, I think it depends on your use-case. I don't think I would ever want to overload a single element unless it made some sort of sense, but for some reason I guess I had to in order to brew up this answer. I'm also not sure how valid that JQuery sentence is since angular has its own way of processing events that is cross-platform. Anyhow! I'm glad you figured out a better way to go about this. I'll update the answer to address your concerns.Guidry
A
14

Came across this and thought I'd throw out an alternative. It's not too different from the original poster aside from two key points.

1) There's no nested function declarations.

2) I use $timeout. I often use $timeout even without a delay...especially if I'm kicking off promises to do other work. The $timeout will fire when the digest cycle comes through which makes sure that any data changes to scope get applied.

Given

<img src="myImage.jpg" ng-click="singleClick()" ng-dblclick="doubleClick()">

In your controller the singleClick function will look like:

$scope.singleClick = function () {
    if ($scope.clicked) {
        $scope.cancelClick = true;
        return;
    }

    $scope.clicked = true;

    $timeout(function () {
        if ($scope.cancelClick) {
            $scope.cancelClick = false;
            $scope.clicked = false;
            return;
        }

        //do something with your single click here

        //clean up
        $scope.cancelClick = false;
        $scope.clicked = false;
    }, 500);
};

And the doubleClick function will look normal:

$scope.doubleClick = function () {

    $timeout(function () {

        //do something with your double click here

    });
};

Hope this helps someone...

Achitophel answered 16/3, 2015 at 9:35 Comment(4)
This is, by far, the most correct answer. You can take this a step further and make this run inline on the markup, too. I'm about to post a followup about the digest loop and how you can use $evalAsync or $applyAsync instead of a $timeout to make this look a tiny bit cleaner.Guidry
This works great! I had multiple elements so instead of $scope.click and $scope.cancelClick, I attached click and cancelClick directly to the data model for each object, passed as an argument. Now I can quickly single-click multiple elements. Even cleaner might be to use ng-init for each element, since view stuff doesn't belong in the model.Tarryn
Great answer! However, 500 ms delay for single click seems a little too long to me. Setting too short would make it not working. Is 300 ms okay? What is the best delay time for single click?Learnt
@miliu, I haven't looked at this in a while, but I seem to remember some concern about the digest cycle, this delay and the actual browser cycle that gets kicked off to pickup to work. Net/Net, you'd need to test this. Additionally, this is inherently fixed in Angular 2 and no special code is needed. It may be fixed in 1.5/1.4 as well, but I'm not up on those versions. Based on the date of this post, this was targeted against 1.2.Achitophel
R
2

I ran across while trying to find a way to handle double click and click at the same time. I used the concepts here to cancel the original click. If a second click occurs before the delay, the double click action performed. If there is not a second click, once the delay is over, the default ngClick action runs and the original event is triggered on the element (and allowed to bubble like it would have initially).

Example

<div ng-click="singleClick()"><span double-click="doubleClick()">double click me</span></div>

Code

.directive('doubleClick', function($timeout, _) {

  var CLICK_DELAY = 300
  var $ = angular.element

  return {
    priority: 1, // run before event directives
    restrict: 'A',
    link: function(scope, element, attrs) {
      var clickCount = 0
      var clickTimeout

      function doubleClick(e) {
        e.preventDefault()
        e.stopImmediatePropagation()
        $timeout.cancel(clickTimeout)
        clickCount = 0
        scope.$apply(function() { scope.$eval(attrs.doubleClick) })
      }

      function singleClick(clonedEvent) {
        clickCount = 0
        if (attrs.ngClick) scope.$apply(function() { scope.$eval(attrs.ngClick) })
        if (clonedEvent) element.trigger(clonedEvent)
      }

      function delaySingleClick(e) {
        var clonedEvent = $.Event('click', e)
        clonedEvent._delayedSingleClick = true
        e.preventDefault()
        e.stopImmediatePropagation()
        clickTimeout = $timeout(singleClick.bind(null, clonedEvent), CLICK_DELAY)
      }

      element.bind('click', function(e) {
        if (e._delayedSingleClick) return
        if (clickCount++) doubleClick(e)
        else delaySingleClick(e)
      })

    }
  }

})
Ringside answered 5/8, 2014 at 3:0 Comment(0)
F
1

Joining the pieces of the answers here:

  • using @GregGrater strategy for simplicity
  • creating a directive, as @Rob (the one accepted as best answer in this thread)
  • solving the issue of @Rob answer, by replacing the ngClick build-in directive by using @EricChen answer

Here the Plunker with the essence of the idea (same as snippet in this answer; see below).

Aside note: ideally, if there is no ng-dblclick defined for the element, it shouldn't prevent the single click (here a Plunker fork implementing this idea)

(function(angular) {
  'use strict';
var myApp = angular.module('myApp', []);

myApp.controller('myCtrl', ['$scope', function($scope) {
  $scope.click = false;
  $scope.singleClick = function() {
    $scope.click = 'single';
  };
  $scope.doubleClick = function() {
    $scope.click = 'double';
 };
}]);

// remove the buildin ng-Click, to solve issue with https://mcmap.net/q/405478/-handling-ng-click-and-ng-dblclick-on-the-same-element-with-angularjs
myApp.config(function($provide) { // Source: https://mcmap.net/q/410780/-angularjs-how-to-override-directive-ngclick
  $provide.decorator('ngClickDirective', ['$delegate', function ($delegate) {
   //$delegate is array of all ng-click directive, in this case 
   // first one is angular buildin ng-click, so we remove it.
   $delegate.shift();
   return $delegate;
   }]);
});

// add custom single click directive: make ngClick to only trigger if not double click
myApp.directive('ngClick', ['$parse', '$timeout', dirSingleClickExclusive]); 

function dirSingleClickExclusive($parse, $timeout) {
  return {
    restrict: 'A',
    replace : false,
    priority: 99, // after all build-in directive are compiled
    link: link
  }
	
  function link ($scope, element, attrs) {
    const delay = 400;
    var clicked = false, cancelClick = false;
    var user_function = $parse(attrs['ngClick']); //(scope);
	
    element.on('click', function (e) {
      // Adapted from: https://mcmap.net/q/405478/-handling-ng-click-and-ng-dblclick-on-the-same-element-with-angularjs
      if (clicked) cancelClick = true; // it is not a single click
      clicked = true;
      
      if (!cancelClick) { // prevent a second timeout
        $timeout(function () { // for time window between clicks (delay)
          if (cancelClick) {
            clicked = false; cancelClick = false;
            return;
          }
          $scope.$apply(function () {
            user_function($scope, {$event : e});
          });
  				
          // reset click status
          clicked = false; cancelClick = false;
        }, delay);
      }
    });
  }
}
})(window.angular);
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Example - custom single click</title>
  
  <script src="//code.angularjs.org/snapshot/angular.min.js"></script>
  <script src="app.js"></script>
  
</head>
<body ng-app="myApp">
  <div ng-controller="myCtrl">
   <button ng-click="singleClick()" ng-dblclick="doubleClick()">Click me!</button>
   <p ng-if="click">This was a {{click}} click.</p>
  </div>
</body>
</html>
Foliar answered 20/1, 2018 at 2:18 Comment(0)
M
0

it works too if calling singleClick on the doubleClick doesn't make any mistake

<div 
    onclick="var scope = angular.element(this).scope(); scope.singleClick();"
    ng-click="null" 
    ng-dblclick="doubleClick()"
></div>
Mariner answered 27/3, 2017 at 9:7 Comment(1)
Whilst this code snippet is welcome, and may provide some help, it would be greatly improved if it included an explanation of how and why this solves the problem. Remember that you are answering the question for readers in the future, not just the person asking now! Please edit your answer to add explanation, and give an indication of what limitations and assumptions apply.Stomach

© 2022 - 2024 — McMap. All rights reserved.