Angularjs: how to position a popup menu (directive - service communication) - angularjs

I want to connect a popup menu with multiple input elements and show the menu when a new input element is focused. The menu closes on an "outside-of-menu click".
simplified example plknr link / code below.
I'm wondering about what is the most direct way to update the position of the popup menu for this situation. In other words: How to get the info about the newly focused input element back into the directive to make the changes there (position and value of the input element).
In my code I'm storing info about the position on a service (and also the reference to the currently focused input element), but this is not working (the directive does not update without scope.$apply) .
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $document, eventService) {
$("input").on("focus", function(event) {
$scope.$apply(function() {
eventService.register(event.target, $scope);
eventService.positon = $(event.target).position();
$scope.position = eventService.position;
console.log("in", $scope.position);
eventService.addMenu();
});
console.log('from event', $(event.target).position());
});
$("input").on("blur", function() {
console.log('blured');
// eventService.closeList()
});
});
app.directive("myMenu", function($document, eventService, $compile) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var menu = angular.element('<div id="menu" class="menu">menu {{menuText}}<div>');
$compile(menu)(scope);
eventService.input = $("input").first(); //set the first input
scope.menuText = eventService.input.val();
scope.$watch(function() {
return eventService.input.val();
}, function(newValue, ov) {
scope.menuText = newValue;
});
// $document.off("dialogmutex", closeMenu);
$document.on("dialogmutex", closeMenu);
// close menu on outside click:
$document.on("click", function(event) {
// if the menu or input is clicked dont close it.
if (!((event.target === elem[0]) || event.target === eventService.input[0] || (elem.find(event.target).length > 0))) {
$document.trigger("dialogmutex");
}
});
function addMenu() {
// positioning the menu does not work
var pos = eventService.position;
if (pos) {
elem.css({
top: pos.yPos,
left: pos.xPos,
position: 'absolute'
});
}
console.log("directive position:", pos);
elem.append(menu);
scope.menuText += " x "
}
function closeMenu() {
elem.find("#menu").remove();
}
addMenu(); // open menu on app start
eventService.addMenu = addMenu; // open the menu later from the controller via service
}
};
});
// service used to register a new input element with the directive.
app.service('eventService', function() {
service = {
register: function(el) {
service.input = $(el);
console.log('reg');
service.position = service.input.position();
console.log("on service", service.position)
}
};
return service;
});
Update:
I got it working using ngStyle directive on the container element and a positionCSS Object on the directive scope ,this way I only need to call $scope.$apply once (inside the event handler)

Using id for each input and the using .closest in jQuery should do the trick. You can refer to this link for detailed version.

Related

Test case for focus-next custom directive in Angular JS

I created a custom directive to resolve some focus related issues in my application.
Directive Code:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var code = e.which || e.keyCode;
var nextElem = document.getElementById(attrs.focusNext);
if (nextElem === null || nextElem === undefined) {
var altElem = document.getElementById(attrs.focusNextAlt);
if (angular.element(altElem).hasClass('ng-hide') === false) {
altElem.focus();
} else {
var selfElem = document.getElementById(attrs.focusSelf);
selfElem.focus();
}
e.preventDefault();
} else {
nextElem.focus();
e.preventDefault();
}
});
}
};
});
})();
How to use in template Use InT emplate
<md-button id="idOfElementC">MyButton</md-button>
<div tabindex="0" focus-next="idOfElementA" focus-next-alt="idOfElementB" focus-self="idOfElementC"></div>
Note:
Element with "idOfElementC" id will be just above the div using focus-next directive.
How does the directive work?
When we press tab on element with "idOfElementC" id (here button), focus will go to div using focus-next directive. The div will redirect the focus to other elements using following cases:
a) First it will check if there is any element with id "idOfElementA". If element exists, then that element will receive focus.
b) If element with id "idOfElementA" do not exist, then "idOfElementB" will receive focus.
c) If element with id "idOfElementB" do not exist as well, then finally "idOfElementA" (on which tab was pressed) will receive focus.
The directive is working fine and fixing all my issues. But, I need to write jasmine test cases for this directive.
Can anyone guide me how to write Jasmine test cases for focus?
UPDATE:
As per comment of #PetrAveryanov the directive was looking horrible and I completely agree.
Updated Directive:
(function() {
angular.module("FocusNextModule", []).directive("focusNext", function() {
return {
restrict: "A",
link: function($scope, elem, attrs) {
elem.bind("focus", function(e) {
var elemToFocus = document.getElementById(attrs.focusNext) || document.getElementById(attrs.focusNextAlt);
/*jshint -W030 */
angular.element(elemToFocus).hasClass('ng-hide') === false ? elemToFocus.focus() : document.getElementById(attrs.focusSelf).focus();
e.preventDefault();
});
}
};
});
})();
Finally, got it how to write test cases for the directive.
describe('focus-next-directive test', function() {
var compile, scope;
beforeEach(module(FocusNextModule));
beforeEach(inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
}));
it('should focus the next element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext"/>')(scope);
var nextElem = compile('<input id="idTestNext" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextElem);
div.focus();
expect(nextElem).toEqual(angular.element(document.activeElement));
div.remove();
nextElem.remove();
});
it('should focus the next alternative element', function() {
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" />')(scope);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(nextAltElem).toEqual(angular.element(document.activeElement));
div.remove();
nextAltElem.remove();
});
it('should focus the Self element', function() {
var selfElem = compile('<input id="idTestSelf" type="text" ng-class="ng-hide"/>')(scope);
var div = compile('<div tabindex="0" focus-next="idTestNext" focus-next-alt="idTestNextAlt" focus-self="idTestSelf"/>')(scope);
var nextAltElem = compile('<input id="idTestNextAlt" type="text" class="ng-hide"/>')(scope);
angular.element(document.body).append(selfElem);
angular.element(document.body).append(div);
angular.element(document.body).append(nextAltElem);
div.focus();
expect(selfElem).toEqual(angular.element(document.activeElement));
div.remove();
selfElem.remove();
nextAltElem.remove();
});
});

update the list using onscroll event

<ul>
<li></li>
<li></li>
</ul>
when I scroll to the bottom of the page, I want the page to load <li>info</li>.
$window.onscroll = function($event){
if($window.pageYOffset > [the parent element height]){
//TODO
}
};
So how can I get the parent element height using angular? I try to visit https://docs.angularjs.org/api/ng/service/$window to find the related api but this page didn't list the attr of the $window, by the way, how can I get all attrs of $window in angular.
Try this
$document.on('scroll', function() {
// do your things
console.log($window.scrollY);
// or pass this to the scope
$scope.$apply(function() {
$scope.pixelsScrolled = $window.scrollY;
})
});
I think best way to manage directives
app = angular.module('myApp', []);
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.boolChangeClass = true;
console.log('Scrolled below header.');
} else {
scope.boolChangeClass = false;
console.log('Header is in view.');
}
scope.$apply();
});
};
});

How to dynamically load controller to a directive

So I have a directive that Will be acting as a side panel in my app. When a user clicks a button the side panel will open. In said side panel I need the controller and view for this area to be dynamic based on which button the users clicks. I have found a way to load up the template dynamically but I am running into issues with loading the controller dynamically.
Enough talking here is the code.
Directive Code
app.directive('itemForm', function($compile) {
var item1Template = '<div ng-include="view"></div>';
var item2Template = '<h1> Hello item2 Form </h1>';
var getTemplate = function(contentType) {
if(contentType === 'item1') {
return item1Template;
} else if(contentType === 'item2') {
return item2Template;
}
};
return {
restrict: 'E',
replace: 'true',
scope: {
formType: '#formType'
},
//templateUrl: scope.template,
link: function(scope, element) {
if(scope.formType === 'item1') {
scope.view = '/views/item1.html';
}
element.html(getTemplate(scope.formType)).show();
$compile(element.contents())(scope);
}
};
});
Html
<item-form form-type='{{form.type}}'> </item-form>
Controller for view that directive lives in
$scope.form = {};
$scope.openItemOneDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item1';
};
$scope.openItemTwoDlg = function() {
$scope.isFormOpen = !$scope.isFormOpen; // this opens the side panel
$scope.form.type = 'item2';
};
You can broadcast (using $broadcast) an event on click of the button. And have a listener (using $on) in the directive. This way, whenever the event is fired, directive logic will get executed.
You can refer the answer on this link for the usage of $broadcast and $on:
On-and-broadcast-in-AngularJS

AngularJS: Preventing 'mouseenter' event triggering on child elements

I'm playing right now with the AngularJS framework and I stumbled upon a problem. I made a directive which is called 'enter'. It triggers functions on mouseenter and mouseleave. I applied it as an attribute to the table row elements. It is now triggered for every child element (all the columns and etc), but it should be only triggered, when you go with your mouse over the table row.
This is how my directive looks like:
myapp.directive('enter', function(){
return {
restrict: 'A', // link to attribute... default is A
link: function (scope, element){
element.bind('mouseenter',function() {
console.log('MOUSE ENTER: ' + scope.movie.title);
});
element.bind('mouseleave',function() {
console.log('LEAVE');
});
}
}
});
Here is an example: http://jsfiddle.net/dJGfd/1/
You have to open the Javascript console to see the log messages.
What is the best way to achieve the functionality that I want in AngularJS? I prefer to not use jQuery if there is a reasonable AngularJS solution.
You can try this:
myapp.directive('enter', function () {
return {
restrict: 'A',
controller: function ($scope, $timeout) {
// do we have started timeout
var timeoutStarted = false;
// pending value of mouse state
var pendingMouseState = false;
$scope.changeMouseState = function (newMouseState) {
// if pending value equals to new value then do nothing
if (pendingMouseState == newMouseState) {
return;
}
// otherwise store new value
pendingMouseState = newMouseState;
// and start timeout
startTimer();
};
function startTimer() {
// if timeout started then do nothing
if (timeoutStarted) {
return;
}
// start timeout 10 ms
$timeout(function () {
// reset value of timeoutStarted flag
timeoutStarted = false;
// apply new value
$scope.mouseOver = pendingMouseState;
}, 10, true);
}
},
link: function (scope, element) {
//**********************************************
// bind to "mouseenter" and "mouseleave" events
//**********************************************
element.bind('mouseover', function (event) {
scope.changeMouseState(true);
});
element.bind('mouseleave', function (event) {
scope.changeMouseState(false);
});
//**********************************************
// watch value of "mouseOver" variable
// or you create bindings in markup
//**********************************************
scope.$watch("mouseOver", function (value) {
console.log(value);
});
}
}
});
Same thing at http://jsfiddle.net/22WgG/
Also instead
element.bind("mouseenter", ...);
and
element.bind("mouseleave", ...);
you can specify
<tr enter ng-mouseenter="changeMouseState(true)" ng-mouseleave="changeMouseState(false)">...</tr>
See http://jsfiddle.net/hwnW3/

AngularJS dropdown directive hide when clicking outside

I'm trying to create a multiselect dropdown list with checkbox and filter option. I'm trying to get the list hidden with I click outside but could not figure it out how. Appreciate your help.
http://plnkr.co/edit/tw0hLz68O8ueWj7uZ78c
Watch out, your solution (the Plunker provided in the question) doesn't close the popups of other boxes when opening a second popup (on a page with multiple selects).
By clicking on a box to open a new popup the click event will always be stopped. The event will never reach any other opened popup (to close them).
I solved this by removing the event.stopPropagation(); line and matching all child elements of the popup.
The popup will only be closed, if the events element doesn't match any child elements of the popup.
I changed the directive code to the following:
select.html (directive code)
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;
});
});
}
I forked your plunker and applied the changes:
Plunker: Hide popup div on click outside
Screenshot:
This is an old post but in case this helps anyone here is a working example of click outside that doesn't rely on anything but 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);
});
}
});
}
}
});
OK I had to call $apply() as the event is happening outside angular world (as per doc).
element.bind('click', function(event) {
event.stopPropagation();
});
$document.bind('click', function(){
scope.isVisible = false;
scope.$apply();
});
I realized it by listening for a global click event like so:
.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);
});
}
}])
The event itself is then broadcasted via a News service
angular.factory('News', ['$rootScope', function($rootScope) {
var news = {};
news.setClick = function( target ) {
this.clickTarget = target;
$rootScope.$broadcast('click');
};
}]);
You can then listen for the broadcast anywhere you need to. Here is an example directive:
.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();
}
}
}])
I hope it helps!
I was not totally satisfied with the answers provided so I made my own. Improvements:
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();
});
}
I get $window directly into the link function. However, you do not need to do this exactly to get $window.
function directive($window) {
return {
restrict: 'AE',
link: function(scope, $element, attributes) {
link.call(null, scope, $element, attributes, $window);
}
};
}
There is a cool directive called angular-click-outside. You can use it in your project. It is super simple to use:
https://github.com/IamAdamJowett/angular-click-outside
The answer Danny F posted is awesome and nearly complete, but Thịnh's comment is correct, so here is my modified directive to remove the listeners on the $destroy event of the directive:
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);
});
}
}
}]);
If you put a log in the handler method, you will still see it fire when an element has been removed from the DOM. Adding my small change is enough to remove it. Not trying to steal anyone's thunder, but this is a fix to an elegant solution.
Use angular-click-outside
Installation:
bower install angular-click-outside --save
npm install #iamadamjowett/angular-click-outside
yarn add #iamadamjowett/angular-click-outside
Usage:
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');
}
I found some issues with the implementation in https://github.com/IamAdamJowett/angular-click-outside
If for example the element clicked on is removed from the DOM, the directive above will trigger the logic.
That didn't work for me, since I had some logic in a modal that, after click, removed the element with a ng-if.
I rewrote his implementation. Not battle tested, but seems to be working better (at least in my scenario)
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;
}

Resources