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
2 Answers
Reset to default 18You 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.