AngularJS下拉指令:点击外部隐藏

44
我尝试创建一个带有复选框和筛选选项的多选下拉列表。我正在尝试在我点击外部区域时隐藏该列表,但无法理解如何实现。感谢您的帮助。 http://plnkr.co/edit/tw0hLz68O8ueWj7uZ78c
9个回答

72

注意,你提供的代码(问题中提供的Plunker)在打开第二个弹出窗口时不会关闭其他框的弹出窗口(当页面上有多个选择框时)。

通过单击框以打开新的弹出窗口,单击事件将始终被阻止。该事件永远不会到达任何其他打开的弹出窗口(以关闭它们)。

我通过删除 event.stopPropagation(); 行并匹配弹出窗口的所有子元素来解决了这个问题。

仅当事件元素与弹出窗口的任何子元素都不匹配时,弹出窗口才会关闭。

我将指令代码更改为以下内容:

select.html(指令代码)

link: function(scope, element, attr){

    scope.isPopupVisible = false;

    scope.toggleSelect = function(){
        scope.isPopupVisible = !scope.isPopupVisible;
    }

    $(document).bind('click', function(event){
        var isClickedElementChildOfPopup = element
            .find(event.target)
            .length > 0;

        if (isClickedElementChildOfPopup)
            return;

        scope.$apply(function(){
            scope.isPopupVisible = false;
        });
    });
}

我 fork 了你的 Plunker 并应用了更改:

Plunker: 在点击外部隐藏弹出 div

屏幕截图:

Plunker 屏幕截图


16
性能提示:如果你的页面上有很多这样的选择框,你应该只在弹出窗口“打开”时绑定click事件,并在弹出窗口“关闭”时立即解除click事件的绑定。 - cheneym
1
看起来 elementMatchesAnyInArray 只是想查看 event.target 是否是 element 的子元素。通过检查 element.find(event.target).length > 0 不也可以实现同样的效果吗? - dubilla
1
@dubilla 谢谢,你说得对。我已经更新了示例和 plunker。 - cheneym
2
如何在没有jQuery的情况下实现这个功能? 没有jQuery, element.find 仅限于标签名称。 - Bhoomtawath Plinsut
3
@BhoomtawathPlinsut element[0].contains(event.target)。https://developer.mozilla.org/zh-CN/docs/Web/API/Node/contains - MathieuLescure
显示剩余5条评论

53

虽然这是一篇旧文章,但如果有人需要,这是一个可行的点击外部区域的示例,而且没有依赖任何东西,只使用了angular。

module('clickOutside', []).directive('clickOutside', function ($document) {

        return {
           restrict: 'A',
           scope: {
               clickOutside: '&'
           },
           link: function (scope, el, attr) {

               $document.on('click', function (e) {
                   if (el !== e.target && !el[0].contains(e.target)) {
                        scope.$apply(function () {
                            scope.$eval(scope.clickOutside);
                        });
                    }
               });
           }
        }

    });

1
不错的解决方案,但如果您有一个调用弹出窗口但实际上没有包装它的跨度或超链接,则似乎存在缺陷。您必须将属性应用于可点击的元素,该元素随后成为目标...或者它会搜索以查看它是否包含目标,但由于超链接不包装弹出窗口,因此无法防止在其中单击时弹出窗口消失。 - KingOfHypocrites
然而,只需将我的链接和弹出窗口包装在一个div中即可解决这个常见问题...这是我能找到的最简单的纯Angular解决方案,特别棒的是我不必引入整个与Bootstrap耦合的库来完成它。 - KingOfHypocrites
8
我认为我们需要在作用域销毁时将监听器 off 掉。 - Thịnh Phạm
1
完美且最佳的解决方案。 - sanjeev shetty
4
运行完美。但我建议将scope.$applyscope.$eval更改为scope.applyAsyncscope.evalAsync,以获得更好的性能。 - wmarquardt

8

好的,我必须调用 $apply() 函数,因为事件发生在 Angular 之外(根据文档)。

    element.bind('click', function(event) {
    event.stopPropagation();      
    });

    $document.bind('click', function(){
    scope.isVisible = false;
    scope.$apply();
    });

非常感谢,第一个绑定解决了我的问题。使用它时,当我点击元素(下拉指令)时,它不会消失,但是当我在外面点击时,它会消失。 - Muhammad Zeshan Ghafoor

7

我通过监听全局点击事件来实现它,代码如下:

.directive('globalEvents', ['News', function(News) {
    // Used for global events
    return function(scope, element) {
        // Listens for a mouse click
        // Need to close drop down menus
        element.bind('click', function(e) {
            News.setClick(e.target);
        });
    }
}])

活动本身随后通过新闻服务进行广播。
angular.factory('News', ['$rootScope', function($rootScope) {
    var news = {};
    news.setClick = function( target ) {
        this.clickTarget = target;
        $rootScope.$broadcast('click');
    };
}]);

你可以在任意需要的地方监听广播。以下是一个指令示例:

.directive('dropdown', ['News', function(News) {
  // Drop down menu für the logo button
  return {
    restrict: 'E',
    scope: {},
    link: function(scope, element) {
      var opened = true;
      // Toggles the visibility of the drop down menu
      scope.toggle = function() {
        element.removeClass(opened ? 'closed' : 'opened');
        element.addClass(opened ? 'opened' : 'closed');
      };
      // Listens for the global click event broad-casted by the News service
      scope.$on('click', function() {
        if (element.find(News.clickTarget.tagName)[0] !== News.clickTarget) {
          scope.toggle(false);
        }
      });
      // Init
      scope.toggle();
    }
  }
}])

我希望你能帮到您!

不太清楚如何使用这种方法。在我的指令作用域中,来自上级控制器的新闻点击没有注册。您能否提供更多细节或为我们安排一个fiddle?因为这个解决方案对我来说似乎是最好的。谢谢! - Rootical V.

4

我对提供的答案并不完全满意,所以我自己做了一些改进:

  • More defensive updating of the scope. Will check to see if a apply/digest is already in progress
  • div will also close when the user presses the escape key
  • window events are unbound when the div is closed (prevents leaks)
  • window events are unbound when the scope is destroyed (prevents leaks)

    function link(scope, $element, attributes, $window) {

    var el = $element[0],
        $$window = angular.element($window);
    
    function onClick(event) {
        console.log('window clicked');
    
        // might need to polyfill node.contains
        if (el.contains(event.target)) {
            console.log('click inside element');
            return;
    
        }
    
        scope.isActive = !scope.isActive;
        if (!scope.$$phase) {
            scope.$apply();
        }
    }
    
    function onKeyUp(event) {
    
        if (event.keyCode !== 27) {
            return;
        }
    
        console.log('escape pressed');
    
        scope.isActive = false;
        if (!scope.$$phase) {
            scope.$apply();
        }
    }
    
    function bindCloseHandler() {
        console.log('binding window click event');
        $$window.on('click', onClick);
        $$window.on('keyup', onKeyUp);
    }
    
    function unbindCloseHandler() {
        console.log('unbinding window click event');
        $$window.off('click', onClick);
        $$window.off('keyup', onKeyUp);
    }
    
    scope.$watch('isActive', function(newValue, oldValue) {
        if (newValue) {
            bindCloseHandler();
        } else {
            unbindCloseHandler();
        }
    });
    
    // prevent leaks - destroy handlers when scope is destroyed
    scope.$on('$destroy', function() {
        unbindCloseHandler();
    });
    

    }

我直接在链接函数中获得了$window对象。然而,你不必像我这样做才能获得$window对象。

function directive($window) {
    return {
        restrict: 'AE',
        link: function(scope, $element, attributes) {
            link.call(null, scope, $element, attributes, $window);
        }
    };
}

4

3

Danny F的答案非常棒并且几乎完整,但是Thịnh的评论是正确的,所以这里是我修改过的指令,在指令的$destroy事件中移除监听器:

const ClickModule = angular
.module('clickOutside', [])
.directive('clickOutside', ['$document', function ($document) {
    return {
        restrict: 'A',
        scope: {
            clickOutside: '&'
        },
        link: function (scope, el, attr) {
            const handler = function (e) {
                if (el !== e.target && !el[0].contains(e.target)) {
                    scope.$apply(function () {
                        console.log("hiiii");
                        //  whatever expression you assign to the click-outside attribute gets executed here
                        //  good for closing dropdowns etc
                        scope.$eval(scope.clickOutside);
                    });
                }
            }

            $document.on('click', handler);

            scope.$on('$destroy', function() {
                $document.off('click', handler);
            });
        }
    }
}]);

如果您在处理程序中添加一个日志,当元素从DOM中删除时仍然会看到它被触发。添加我的小改动就足以将其删除。并非要抢夺任何人的光芒,但这是对优美解决方案的修复。


0

使用angular-click-outside

安装:

bower install angular-click-outside --save
npm install @iamadamjowett/angular-click-outside
yarn add @iamadamjowett/angular-click-outside

使用方法:

angular.module('myApp', ['angular-click-outside'])

//in your html
<div class="menu" click-outside="closeThis">
...
</div>

//And then in your controller
$scope.closeThis = function () {
    console.log('closing');
}

如何解决该指令的冲突? - Ivan Ferrer

0

我发现https://github.com/IamAdamJowett/angular-click-outside的实现存在一些问题。

例如,如果被点击的元素从DOM中移除,上述指令将触发逻辑。但这对我不起作用,因为我在一个模态框中有一些逻辑,在点击后使用ng-if删除了该元素。

我重写了他的实现。虽然没有经过大规模测试,但在我的场景中似乎工作得更好。

angular
  .module('sbs.directives')
  .directive('clickOutside', ['$document', '$parse', '$timeout', clickOutside]);

const MAX_RECURSIONS = 400;

function clickOutside($document, $parse, $timeout) {
  return {
    restrict: 'A',
    link: function ($scope, elem, attr) {
      // postpone linking to next digest to allow for unique id generation
      $timeout(() => {
        function runLogicIfClickedElementIsOutside(e) {
          // check if our element already hidden and abort if so
          if (angular.element(elem).hasClass('ng-hide')) {
            return;
          }

          // if there is no click target, no point going on
          if (!e || !e.target) {
            return;
          }

          let clickedElementIsOutsideDirectiveRoot = false;
          let hasParent = true;
          let recursions = 0;

          let compareNode = elem[0].parentNode;
          while (
            !clickedElementIsOutsideDirectiveRoot &&
            hasParent &&
            recursions < MAX_RECURSIONS
          ) {
            if (e.target === compareNode) {
              clickedElementIsOutsideDirectiveRoot = true;
            }

            compareNode = compareNode.parentNode;
            hasParent = Boolean(compareNode);
            recursions++; // just in case to avoid eternal loop
          }

          if (clickedElementIsOutsideDirectiveRoot) {
            $timeout(function () {
              const fn = $parse(attr['clickOutside']);
              fn($scope, { event: e });
            });
          }
        }

        // if the devices has a touchscreen, listen for this event
        if (_hasTouch()) {
          $document.on('touchstart', function () {
            setTimeout(runLogicIfClickedElementIsOutside);
          });
        }

        // still listen for the click event even if there is touch to cater for touchscreen laptops
        $document.on('click', runLogicIfClickedElementIsOutside);

        // when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around
        $scope.$on('$destroy', function () {
          if (_hasTouch()) {
            $document.off('touchstart', runLogicIfClickedElementIsOutside);
          }

          $document.off('click', runLogicIfClickedElementIsOutside);
        });
      });
    },
  };
}

function _hasTouch() {
  // works on most browsers, IE10/11 and Surface
  return 'ontouchstart' in window || navigator.maxTouchPoints;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接