javascriptjqueryangularjsjquery-eventsmousedown

Bind document mouseup in ng-mousedown


I want something to begin on ng-mousedown and end on mouseup. I also want that something to continue happening while the mouse button remains down if the mouse leaves the element. A pretty common way to implement this in vanilla Javascript is to bind a document mouseup event in the element mousedown event, like so:

HTML:

<button onMouseDown='onMouseDown()'>Click Me</button>
<div id="val"></div>

JS:

function onMouseDown() {
  var el = document.querySelector('#val'),
      changeValue = function() {
        el.innerHTML = 'done!';
        document.removeEventListener('mouseup', changeValue);
      };
  
  el.innerHTML = 'waiting for mouseup...';

  document.addEventListener('mouseup', changeValue);
}

I am having trouble implementing this behavior in Angular with jQuery's one(), angular.element.one, and addEventListener / removeEventListener:

HTML:

<div ng-app="MyApp" ng-controller="AppCtrl">
  <div>Note that 'done!' is not rendered, despite 'mouseup' being logged to the console</div>
  <button ng-mousedown='changeValue()'>Click Me</button>
  <div>{{value}}</div>   
</div>

JS (jQuery's one()):

angular
  .module('MyApp', [])
  .controller('AppCtrl', function($scope) {
    $scope.changeValue = function() {
      console.log('mousedown');
      $scope.value = 'waiting for mouseup...';  
      
      $(document).one('mouseup', function() {
        console.log('mouseup');
        $scope.value = 'done!';
      });
    }
  });

JS (angular.element.one):

angular
  .module('MyApp', [])
  .controller('AppCtrl', function($scope) {
    $scope.changeValue = function() {
      console.log('mousedown');
      $scope.value = 'waiting for mouseup...';  
      
      angular.element(document).one('mouseup', function() {
        console.log('mouseup');
        $scope.value = 'done!';
      });
    }
  });

JS (addEventListener / removeEventListener):

angular
  .module('MyApp', [])
  .controller('AppCtrl', function($scope) {
    $scope.changeValue = function() {
      var changeValue = function() {
        console.log('mouseup');
        $scope.value = 'done!';
        document.removeEventListener('mouseup', changeValue);
      };
      
      console.log('mousedown');
      $scope.value = 'waiting for mouseup...';  
      
      document.addEventListener('mouseup', changeValue);
    }
  });

I also tried leveraging ng-mouseup and ng-mouseleave like this:

HTML:

<div ng-app="MyApp" ng-controller="AppCtrl">
  <div>While clicking the button, move the mouse off of the button and release the mouse button. Note that 'done!' is not rendered, despite 'mouseup' being logged to the console</div>
  <button ng-mousedown='mouseDown()' ng-mouseup="mouseUp()" ng-mouseleave="mouseLeave()">Click Me</button>
  <div>{{value}}</div>  
</div>

JS:

angular
  .module('MyApp', [])
  .controller('AppCtrl', function($scope) {
    $scope.mouseDown = function() {
      console.log('mousedown');
      $scope.value = 'waiting for mouseup...';  
    }
    $scope.mouseUp = function() {
      console.log('mouseup');
      $scope.value = 'done!';
    }
    $scope.mouseLeave = function() {
      if ($scope.value && $scope.value.indexOf('waiting') > -1) {
        console.log('mouseleave, binding mouseup'); 
        $(document).one('mouseup', function() {
           $scope.mouseUp();
        });
      }
    }
  });

The result is the same. $scope.mouseUp is executed, but 'done!' is never rendered. This creates additional problems with $scope.$watch:

HTML:

<div ng-app="MyApp" ng-controller="AppCtrl">
  <div>value never === 'done!' in $scope.$watch</div>
  <button ng-mousedown='changeValue()'>Click Me</button>
  <div>{{value}}</div>   
</div>

JS:

angular
  .module('MyApp', [])
  .controller('AppCtrl', function($scope) {
    $scope.changeValue = function() {
      console.log('mousedown');
      $scope.value = 'waiting for mouseup...';  
      
      $(document).one('mouseup', function() {
        console.log('mouseup');
        $scope.value = 'done!';
      });
    }
    $scope.$watch('value', function(value) {
      if (value === 'done!') {
        console.log('value set to "done!"'); 
      }
    });
  });

Solution

  • Have no idea why this question has been left without an answer for more than a month.

    The main problem here is simple - the code that updates $scope.value is executed outside of AngularJS. In order to make your data binding work you should wrap it with $scope.$apply like this:

    $scope.$apply(function () {
      $scope.value = 'done!';                
    });
    

    Note that ng-mouseup works fine, you can see it when mouseup is triggered above the button. But if you move the mouse outside the button $(document).one('mouseup'..) event will occur.

    Just keep in mind that you must wrap all the external callbacks with $scope.$apply (JQuery HTTP callbacks, DOM events binded not with ng-* directives, setTimeout, etc.) if you need it to work with AngularJS binding or watchers. But think twice - calling $scope.$apply inside another $scope.$apply will cause an error.

    You can look through the documentation or read more here.