在 jQuery click 事件中停止底层 ng-click 的传播

Posted

技术标签:

【中文标题】在 jQuery click 事件中停止底层 ng-click 的传播【英文标题】:Stop propagation of underlying ng-click inside a jQuery click event 【发布时间】:2014-02-24 11:42:06 【问题描述】:

Twitter Bootstrap dropdown 嵌套在 tr 中。 tr 可通过ng-click 进行点击。单击页面上的任意位置将折叠下拉菜单。该行为是通过$document.bind('click', closeMenu) 在指令中定义的。

因此,当打开菜单并且用户单击一行时,我希望菜单关闭(就像它一样)并且我想阻止该行上的点击事件。

JSFiddle:http://jsfiddle.net/LMc2f/1/ JSFiddle +指令内联:http://jsfiddle.net/9DM8U/1/

来自 ui-bootstrap-tpls-0.10.0.js 的相关代码:

angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) 
  var openElement = null,
      closeMenu   = angular.noop;
  return 
    restrict: 'CA',
    link: function(scope, element, attrs) 
      scope.$watch('$location.path', function()  closeMenu(); );
      element.parent().bind('click', function()  closeMenu(); );
      element.bind('click', function (event) 

        var elementWasOpen = (element === openElement);

        event.preventDefault();
        event.stopPropagation();

        if (!!openElement) 
          closeMenu();
        

        if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) 
          element.parent().addClass('open');
          openElement = element;
          closeMenu = function (event) 
            if (event) 
              event.preventDefault();
              event.stopPropagation();
            
            $document.unbind('click', closeMenu);
            element.parent().removeClass('open');
            closeMenu = angular.noop;
            openElement = null;
          ;
          $document.bind('click', closeMenu);
        
      );
    
  ;
]);

我不知道如何停止 closeMenu 中的底层 ng-click 事件。

注意:我找不到访问$event 的方法,所以我无法尝试$event.stopPropagation()

【问题讨论】:

What's the best way to cancel event propagation between nested ng-click calls?的可能重复 @Stewie 我知道我可以在每个a 上使用ng-click="$event.stopPropagation()",但它不能解决点击任意位置折叠下拉菜单的情况,其中ng-click在绑定到$document 的jQuery click 事件完成后触发调用。 @Stewie 你不能使用提供的 JSFiddle 解决我的问题吗? 因此,当打开菜单并单击行时,您希望菜单关闭(因为它确实如此)并且您想阻止行上的单击事件? @Stewie 是的。确切地。但是,在函数closeMenuevent.preventDefault()event.stopPropagation() 内部不会取消底层ng-click。我认为这是由于 jQuery 和 Angular 没有共享相同的调用堆栈。 【参考方案1】:

我倾向于从模板本身调用$event.stopPropagation()。与事件相关的逻辑很可能属于那里。也应该使单元测试更容易。查看模板的人也会知道,如果不查看底层控制器,事件不会冒泡。

<div ng-click="parentHandler()">
    <div ng-click="childHandler(); $event.stopPropagation()"></div>
</div>

【讨论】:

这是一个非常干净的解决方案。 此解决方案适用于 Angular 2+(撰写本文时为 6.1 版)。在我的例子中,表格单元格上的 Angular Material 菜单触发指令正在处理点击事件,所以我只需要阻止它冒泡到行。 [matMenuTriggerFor]="moreActions" (click)="$event.stopPropagation()"【参考方案2】:

下拉指令绑定了文档上的点击事件,但是当你点击行时,事件开始从目标元素向下传播到根文档节点(td -> @ 987654324@ -> table -> document)。

这就是为什么您的行中的ng-click 处理程序总是被调用的原因,即使该指令在文档单击时“停止”气泡。

解决方案是在为文档添加点击处理程序时使用 useCapture 标志。

启动捕获后,指定类型的所有事件将被 在被分派给任何人之前分派给已注册的侦听器 DOM 树中它下方的 EventTarget。 mdn

现在,要指示下拉指令使用您自己的处理程序,您需要更改指令的来源。但这是第三方指令,出于可维护性的原因,您可能不想这样做。

这就是强大的 Angular $decorator 发挥作用的地方。您可以使用 $decorator 即时更改第三方模块的源代码,而无需实际接触实际的源文件。

因此,使用装饰器并在文档节点上使用自定义事件处理程序,您可以通过以下方式使下拉菜单运行:

FIDDLE

var myApp = angular.module('myApp', []);

/**
 * Original dropdownToggle directive from ui-bootstrap.
 * Nothing changed here.
 */
myApp.directive('dropdownToggle', ['$document', '$location', function ($document, $location) 
  var openElement = null,
      closeMenu   = angular.noop;
  return 
    restrict: 'CA',
    link: function(scope, element, attrs) 
      scope.$watch('$location.path', function()  closeMenu(); );
      element.parent().bind('click', function()  closeMenu(); );
      element.bind('click', function (event) 

        var elementWasOpen = (element === openElement);

        event.preventDefault();
        event.stopPropagation();

        if (!!openElement) 
          closeMenu();
        

        if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) 
          element.parent().addClass('open');
          openElement = element;
          closeMenu = function (event) 
            if (event) 
              event.preventDefault();
              event.stopPropagation();
            
            $document.unbind('click', closeMenu);
            element.parent().removeClass('open');
            closeMenu = angular.noop;
            openElement = null;
          ;
          $document.bind('click', closeMenu); /* <--- CAUSE OF ALL PROBLEMS ----- */
        
      );
    
  ;
]);


/**
 * This is were we decorate the dropdownToggle directive
 * in order to change the way the document click handler works
 */
myApp.config(function($provide)
  'use strict';

  $provide.decorator('dropdownToggleDirective', [
      '$delegate',
      '$document',
      function ($delegate, $document) 

        var directive = $delegate[0];
        var openElement = null;
        var closeMenu = angular.noop;

        function handler(e)
            var elm = angular.element(e.target);
          if(!elm.parents('.dropdown-menu').length)
            e.stopPropagation();
            e.preventDefault();
          
          closeMenu();
          // After closing the menu, we remove the all-seeing handler
          // to allow the application click events to work nnormally
          $document[0].removeEventListener('click', handler, true);
        

        directive.compile = function()
          return function(scope, element) 
            scope.$watch('$location.path', closeMenu);
            element.parent().bind('click', closeMenu);
            element.bind('click', function (event) 

              var elementWasOpen = (element === openElement);

              event.preventDefault();
              event.stopPropagation();

              if (!!openElement) 
                closeMenu();
              

              if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) 
                element.parent().addClass('open');
                openElement = element;
                closeMenu = function (event) 
                  if (event) 
                    event.preventDefault();
                    event.stopPropagation();
                  
                  $document.unbind('click', closeMenu);
                  element.parent().removeClass('open');
                  closeMenu = angular.noop;
                  openElement = null;
                ;


                // We attach the click handler by specifying the third "useCapture" parameter as true
                $document[0].addEventListener('click', handler, true);
              
            );
          ;
        ;

        return $delegate;
      
  ]);

);

更新:

请注意,除非目标元素是实际的下拉选项,否则更新后的自定义处理程序将防止冒泡。这将解决即使单击下拉选项也会阻止单击事件的问题。

这仍然不会阻止事件冒泡到行(从下拉选项),但这与下拉指令没有任何关系。无论如何,为了防止这种冒泡,您可以将 $event 对象传递给 ng-click 表达式函数并使用该对象来阻止偶数冒泡到表格行:

<div ng-controller="DropdownCtrl">
  <table>
    <tr ng-click="clicked('row')">
      <td>

        <div class="btn-group">
          <button type="button" class="btn btn-default dropdown-toggle">
            Action <span class="caret"></span>
          </button>
          <ul class="dropdown-menu" role="menu">
            <li ng-repeat="choice in items">
              <a ng-click="clicked('link element', $event)">choice</a>
            </li>
          </ul>
        </div>

      </td>
    </tr>
  </table>
</div>
function DropdownCtrl($scope) 
  $scope.items = [
    "Action",
    "Another action",
    "Something else here"
  ];

  $scope.clicked = function(what, event) 
    alert(what + ' clicked');
    if(event)
      event.stopPropagation();
      event.preventDefault();
    
  


【讨论】:

我的 JSFiddle 有问题。与此同时,我正在尝试您的解决方案。 好的,一个问题解决了。除了现在,我无法选择该选项。该点击也被阻止。有什么想法吗?【参考方案3】:

您需要在小提琴的第 2 行传递 ng-click 事件,然后在您的方法中对该对象执行 preventDefault 和 stopPropigation

<tr ng-click="do($event)">

【讨论】:

此事件是点击链上的最后一个事件,如果打开下拉菜单,则根本不应触发。我尝试了您的建议,但当我单击该行关闭下拉菜单时,我仍然收到一条警报消息。 closeMenu() 有一个事件作为参数。您可能需要像这样将点击事件参数传递给 closeMenu: element.parent().bind('click', function(event) closeMenu(event); ); 如果您仔细查看代码,事件参数将通过 $document.bind('click', closeMenu) 传递给 closeMenu。调用event.preventDefault()event.stopPropagation() 对底层ng-click 没有影响。我的假设是 jQuery 事件对象在与 $event 不同的堆栈上运行。 你确定父事件对象和子事件对象是一样的吗?当有父点击时,似乎 closeMenu 被调用了两次,一次带有子事件,一次来自父事件,没有事件 父级没有点击事件。父级只是一个包含 open CSS 类的 div。困扰我的事件是ng-click 在表格行本身上触发的事件。请注意,在指令中,jQuery click 事件被绑定和解除绑定到$document(整个页面),以允许用户单击页面上的任意位置并关闭下拉菜单。我什至找不到取消 AngularJS 事件以跟随 jQuery 的方法。

以上是关于在 jQuery click 事件中停止底层 ng-click 的传播的主要内容,如果未能解决你的问题,请参考以下文章

jQuery事件捕获停止传播

将 onclick/ng-click 事件附加到网格数据绑定事件中的元素

Selenium click 不适用于 angularjs ng-click 事件

没有 jquery/js 的动画 ng-click

ASP.NET MVC中jQuery与angularjs混合应用传参并绑定数据

Angular Js中的JQuery数据库Ng-Click无法正常工作?