最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to handle document click and notify other controllers using AngularJS? - Stack Overflow

programmeradmin5浏览0评论

I have created a horizontal drop down menu using AngularJS.

The menu section is managed by an angular controller called menuController. Standard menu behavior is implemented, so that on hover main menu item gets highlighted unless it is disabled. On clicking the main menu item, the sub menu toggles. If Sub menu is in a open state, I want it to go away when user clicks anywhere else on the document. I tried to create a directive to listen for document click event but not sure on how to notify menu-controller about it. How should I implement this scenario in a AngularJS way?

Partially working Original Plunk without document click handling mechanism.

UPDATE:

Based on answered suggestion, I went with Brodcast approach and updated the script to reflect my latest changes. It is working as per my expectation. I made the globalController $broadcast a message and menuController subscribe to that message.

UPDATE 2: Modified code to inject global events definition data.

var eventDefs = (function() {
  return {
    common_changenotification_on_document_click: 'common.changenotification.on.document.click'
  };
}());

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

changeNotificationApp.value('appEvents', eventDefs);

changeNotificationApp.directive("onGlobalClick", ['$document', '$parse',
  function($document, $parse) {
    return {
      restrict: 'A',
      link: function($scope, $element, $attributes) {
        var scopeExpression = $attributes.onGlobalClick;

        var invoker = $parse(scopeExpression);

        $document.on("click",
          function(event) {
            $scope.$apply(function() {
              invoker($scope, {
                $event: event
              });
            });
          }
        );
      }
    };
  }
]);

changeNotificationApp.controller("globalController", ['$scope', 'appEvents',
  function($scope, appEvents) {
    $scope.handleClick = function(event) {
      $scope.$broadcast(appEventsmon_changenotification_on_document_click, {
        target: event.target
      });
    };
  }
]);

//menu-controller.js
changeNotificationApp.controller('menuController', ['$scope', '$window', 'appEvents',
  function($scope, $window, appEvents) {

    $scope.IsLocalMenuClicked = false;

    $scope.menu = [{
      Name: "INTEGRATION",
      Tag: "integration",
      IsDisabled: false,
      IsSelected: false,
      SubMenu: [{
        Name: "SRC Messages",
        Tag: "ncs-notifications",
        IsDisabled: false,
        AspNetMvcController: "SearchSRCMessages"
      }, {
        Name: "Target Messages",
        Tag: "advisor-notifications",
        IsDisabled: false,
        AspNetMvcController: "SearchTaregtMessages"
      }]
    }, {
      Name: "AUDITING",
      Tag: "auditing",
      IsDisabled: true,
      IsSelected: false,
      SubMenu: []
    }];

    $scope.appInfo = {
      Version: "1.0.0.0",
      User: "VB",
      Server: "azzcvy0623401v",
      IsSelected: false
    };

    var resetMenu = function() {
      angular.forEach($scope.menu, function(item) {
        item.IsSelected = false;
      });
      $scope.appInfo.IsSelected = false;
    };

    $scope.toggleDropDownMenu = function(menuItem) {
      var currentDropDownState = menuItem.IsSelected;
      resetMenu($scope.menu, $scope.appInfo);
      menuItem.IsSelected = !currentDropDownState;
      $scope.IsLocalMenuClicked = true;
    };

    $scope.loadPage = function(menuItem) {
      if (menuItem.AspNetMvcController)
        $window.location.href = menuItem.AspNetMvcController;
    };

    $scope.$on(appEventsmon_changenotification_on_document_click,
      function(event, data) {
        if (!$scope.IsLocalMenuClicked)
          resetMenu($scope.menu, $scope.appInfo);
        $scope.IsLocalMenuClicked = false;
      });
  }
]);

UPDATE 3: Modified code in previous implementation to fix a bug where document click fires multiple times. Almost similar approach, but this time, if any one clicks again anywhere on the menu, the click is ignored. Please refer to the New Working Plunk for full code example

    changeNotificationApp.directive("onGlobalClick", ['$document', '$parse',
    function ($document, $parse) {
        return {
            restrict: 'A',
            link: function ($scope, $element, $attributes) {
                var scopeExpression = $attributes.onGlobalClick;

                var invoker = $parse(scopeExpression);

                $document.on("click",
                    function (event) {
                       var isClickedElementIsChildOfThisElement = $element.find(event.target).length > 0;
                            if (isClickedElementIsChildOfThisElement) return;
                        $scope.$apply(function () {
                            invoker($scope, {
                                $event: event
                            });
                        });
                    }
                );
            }
        };
    }
]);

UPDATE 4: Implemented another alternate option. Please refer to the Option 2 Plunk for full code example

    var eventDefs = (function () {
    return {
        on_click_anywhere: 'common.changenotification.on.document.click'
    };
}());

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

changeNotificationApp.value('appEvents', eventDefs);

changeNotificationApp.directive("onClickAnywhere", ['$window', 'appEvents',
  function($window, appEvents) {
    return {
      link: function($scope, $element) {
        angular.element($window).on('click', function(e) {
          // Namespacing events with name of directive + event to avoid collisions
          $scope.$broadcast(appEvents.on_click_anywhere, e.target);
        });
      }
    };
  }
]);

//menu-controller.js
changeNotificationApp.controller('menuController', ['$scope', '$window', 'appEvents', '$element',
    function ($scope, $window, appEvents, $element) {

        $scope.menu = [
            {
                Name: "INTEGRATION",
                Tag: "integration",
                IsDisabled: false,
                IsSelected: false,
                SubMenu: [
                    {
                        Name: "SRC Messages",
                        Tag: "ncs-notifications",
                        IsDisabled: false,
                        AspNetMvcController: "SearchSRCMessages"
                    },
                    {
                        Name: "Target Messages",
                        Tag: "advisor-notifications",
                        IsDisabled: false,
                        AspNetMvcController: "SearchTaregtMessages"
                    }
                ]
            },
            {
                Name: "AUDITING",
                Tag: "auditing",
                IsDisabled: true,
                IsSelected: false,
                SubMenu: []
            }
        ];

        $scope.appInfo = {
            Version: "1.0.0.0",
            User: "VB",
            Server: "azzcvy0623401v",
            IsSelected: false
        };

        var resetMenu = function () {
            angular.forEach($scope.menu, function (item) {
                item.IsSelected = false;
            });
            $scope.appInfo.IsSelected = false;
        };

        $scope.toggleDropDownMenu = function (menuItem) {
            var currentDropDownState = menuItem.IsSelected;
            resetMenu($scope.menu, $scope.appInfo);
            menuItem.IsSelected = !currentDropDownState;
        };

        $scope.loadPage = function (menuItem) {
            if (menuItem.AspNetMvcController)
                $window.location.href = menuItem.AspNetMvcController;
        };

        $scope.$on(appEvents.on_click_anywhere, function(event, targetElement) {
          var isClickedElementIsChildOfThisElement = $element.find(targetElement).length > 0;
          if (isClickedElementIsChildOfThisElement) return;

          $scope.$apply(function(){
            resetMenu($scope.menu, $scope.appInfo);
          });
        });
    }
]);

I have created a horizontal drop down menu using AngularJS.

The menu section is managed by an angular controller called menuController. Standard menu behavior is implemented, so that on hover main menu item gets highlighted unless it is disabled. On clicking the main menu item, the sub menu toggles. If Sub menu is in a open state, I want it to go away when user clicks anywhere else on the document. I tried to create a directive to listen for document click event but not sure on how to notify menu-controller about it. How should I implement this scenario in a AngularJS way?

Partially working Original Plunk without document click handling mechanism.

UPDATE:

Based on answered suggestion, I went with Brodcast approach and updated the script to reflect my latest changes. It is working as per my expectation. I made the globalController $broadcast a message and menuController subscribe to that message.

UPDATE 2: Modified code to inject global events definition data.

var eventDefs = (function() {
  return {
    common_changenotification_on_document_click: 'common.changenotification.on.document.click'
  };
}());

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

changeNotificationApp.value('appEvents', eventDefs);

changeNotificationApp.directive("onGlobalClick", ['$document', '$parse',
  function($document, $parse) {
    return {
      restrict: 'A',
      link: function($scope, $element, $attributes) {
        var scopeExpression = $attributes.onGlobalClick;

        var invoker = $parse(scopeExpression);

        $document.on("click",
          function(event) {
            $scope.$apply(function() {
              invoker($scope, {
                $event: event
              });
            });
          }
        );
      }
    };
  }
]);

changeNotificationApp.controller("globalController", ['$scope', 'appEvents',
  function($scope, appEvents) {
    $scope.handleClick = function(event) {
      $scope.$broadcast(appEvents.common_changenotification_on_document_click, {
        target: event.target
      });
    };
  }
]);

//menu-controller.js
changeNotificationApp.controller('menuController', ['$scope', '$window', 'appEvents',
  function($scope, $window, appEvents) {

    $scope.IsLocalMenuClicked = false;

    $scope.menu = [{
      Name: "INTEGRATION",
      Tag: "integration",
      IsDisabled: false,
      IsSelected: false,
      SubMenu: [{
        Name: "SRC Messages",
        Tag: "ncs-notifications",
        IsDisabled: false,
        AspNetMvcController: "SearchSRCMessages"
      }, {
        Name: "Target Messages",
        Tag: "advisor-notifications",
        IsDisabled: false,
        AspNetMvcController: "SearchTaregtMessages"
      }]
    }, {
      Name: "AUDITING",
      Tag: "auditing",
      IsDisabled: true,
      IsSelected: false,
      SubMenu: []
    }];

    $scope.appInfo = {
      Version: "1.0.0.0",
      User: "VB",
      Server: "azzcvy0623401v",
      IsSelected: false
    };

    var resetMenu = function() {
      angular.forEach($scope.menu, function(item) {
        item.IsSelected = false;
      });
      $scope.appInfo.IsSelected = false;
    };

    $scope.toggleDropDownMenu = function(menuItem) {
      var currentDropDownState = menuItem.IsSelected;
      resetMenu($scope.menu, $scope.appInfo);
      menuItem.IsSelected = !currentDropDownState;
      $scope.IsLocalMenuClicked = true;
    };

    $scope.loadPage = function(menuItem) {
      if (menuItem.AspNetMvcController)
        $window.location.href = menuItem.AspNetMvcController;
    };

    $scope.$on(appEvents.common_changenotification_on_document_click,
      function(event, data) {
        if (!$scope.IsLocalMenuClicked)
          resetMenu($scope.menu, $scope.appInfo);
        $scope.IsLocalMenuClicked = false;
      });
  }
]);

UPDATE 3: Modified code in previous implementation to fix a bug where document click fires multiple times. Almost similar approach, but this time, if any one clicks again anywhere on the menu, the click is ignored. Please refer to the New Working Plunk for full code example

    changeNotificationApp.directive("onGlobalClick", ['$document', '$parse',
    function ($document, $parse) {
        return {
            restrict: 'A',
            link: function ($scope, $element, $attributes) {
                var scopeExpression = $attributes.onGlobalClick;

                var invoker = $parse(scopeExpression);

                $document.on("click",
                    function (event) {
                       var isClickedElementIsChildOfThisElement = $element.find(event.target).length > 0;
                            if (isClickedElementIsChildOfThisElement) return;
                        $scope.$apply(function () {
                            invoker($scope, {
                                $event: event
                            });
                        });
                    }
                );
            }
        };
    }
]);

UPDATE 4: Implemented another alternate option. Please refer to the Option 2 Plunk for full code example

    var eventDefs = (function () {
    return {
        on_click_anywhere: 'common.changenotification.on.document.click'
    };
}());

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

changeNotificationApp.value('appEvents', eventDefs);

changeNotificationApp.directive("onClickAnywhere", ['$window', 'appEvents',
  function($window, appEvents) {
    return {
      link: function($scope, $element) {
        angular.element($window).on('click', function(e) {
          // Namespacing events with name of directive + event to avoid collisions
          $scope.$broadcast(appEvents.on_click_anywhere, e.target);
        });
      }
    };
  }
]);

//menu-controller.js
changeNotificationApp.controller('menuController', ['$scope', '$window', 'appEvents', '$element',
    function ($scope, $window, appEvents, $element) {

        $scope.menu = [
            {
                Name: "INTEGRATION",
                Tag: "integration",
                IsDisabled: false,
                IsSelected: false,
                SubMenu: [
                    {
                        Name: "SRC Messages",
                        Tag: "ncs-notifications",
                        IsDisabled: false,
                        AspNetMvcController: "SearchSRCMessages"
                    },
                    {
                        Name: "Target Messages",
                        Tag: "advisor-notifications",
                        IsDisabled: false,
                        AspNetMvcController: "SearchTaregtMessages"
                    }
                ]
            },
            {
                Name: "AUDITING",
                Tag: "auditing",
                IsDisabled: true,
                IsSelected: false,
                SubMenu: []
            }
        ];

        $scope.appInfo = {
            Version: "1.0.0.0",
            User: "VB",
            Server: "azzcvy0623401v",
            IsSelected: false
        };

        var resetMenu = function () {
            angular.forEach($scope.menu, function (item) {
                item.IsSelected = false;
            });
            $scope.appInfo.IsSelected = false;
        };

        $scope.toggleDropDownMenu = function (menuItem) {
            var currentDropDownState = menuItem.IsSelected;
            resetMenu($scope.menu, $scope.appInfo);
            menuItem.IsSelected = !currentDropDownState;
        };

        $scope.loadPage = function (menuItem) {
            if (menuItem.AspNetMvcController)
                $window.location.href = menuItem.AspNetMvcController;
        };

        $scope.$on(appEvents.on_click_anywhere, function(event, targetElement) {
          var isClickedElementIsChildOfThisElement = $element.find(targetElement).length > 0;
          if (isClickedElementIsChildOfThisElement) return;

          $scope.$apply(function(){
            resetMenu($scope.menu, $scope.appInfo);
          });
        });
    }
]);
Share Improve this question edited Aug 15, 2015 at 11:18 Vinod asked Oct 26, 2013 at 3:29 VinodVinod 1,9523 gold badges19 silver badges27 bronze badges 1
  • I have updated the above mentioned plunk with fully working sample and also updated the java script source code listed in the above post. – Vinod Commented Oct 28, 2013 at 19:23
Add a comment  | 

2 Answers 2

Reset to default 18

You can simplify the directive into something like this:

changeNotificationApp.directive('onDocumentClick', ['$document',
  function($document) {
    return {
      restrict: 'A',
      link: function(scope, element, attrs) {

        var onClick = function() {
          scope.$apply(function() {
            scope.$eval(attrs.onDocumentClick);
          });
        };

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

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

And then pass a function from the menuController to it:

<section class="local-nav" ng-controller="menuController" on-document-click="someFunction()">

No need for the globalController this way.

If you want to keep the globalController and handle it from there, you can:

1.) Make the menu into a service and then inject it into all controllers that need to be able to control it.

2.) Broadcast an event from globalController and listen for it in menuController.

Specific alternative solution: You can turn the directive into a 'on-outside-element-click' and use it like this:

<ul on-outside-element-click="closeMenus()">

The directive looks like this and will only call closeMenus() if you click outside the ul:

changeNotificationApp.directive('onOutsideElementClick', ['$document',
  function($document) {
    return {
      restrict: 'A',
      link: function(scope, element, attrs) {

        element.on('click', function(e) {
          e.stopPropagation();
        });

        var onClick = function() {
          scope.$apply(function() {
            scope.$eval(attrs.onOutsideElementClick);
          });
        };

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

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

Working Plunker: http://plnkr.co/edit/zVo0fL2wOCQb3eAUx44U?p=preview

Well you have done things well. If you apply the same directive over the menuController

<section class="local-nav" ng-controller="menuController"  on-global-click="handleClick($event)>

and have the click handler defined in your menuController you are all set to go.

I don't think there is any harm in having multiple handlers for the event on document. So where ever you define this directive that element can respond to the global document click event.

Update: As i tested this, it leads to another problem where this method get called, where ever you click on the page. You need a mechanism to differentiate now.

发布评论

评论列表(0)

  1. 暂无评论