Accordion menu animation sequencing - angularjs

I'm trying to create a accordion menu in angular using css3 transitions.
I'm facing issues in sequencing the animations. In the current page when I click on "Arrest" in the menu it first expands and then any other currently open element( in this case "general") collapses. How can both the animations work at the same time ?
app.directive('menuaccordion', function() {
return {
restrict : 'A',
link : function(scope, elem, attrs) {
var $el = angular.element(elem);
if($el.hasClass('app-nav-menu-lv1')) {
var name = elem.attr('data-name')
scope.$on('hidelevel1', function(e, data){
if(data != name) {
var container = $el.next()
if(container.hasClass('in')) {
container.removeClass('in');
}
}
});
}
scope.$on('hidelevel2', function(e, data){
console.log($el)
});
elem.on('click', function(e) {
e.preventDefault();
var container = angular.element(elem.next());
if(container.hasClass('in')) { //is expanded
container.removeClass('in');
}else{ //is collapsed
container.addClass('in');
var name = elem.attr('data-name')
if($el.hasClass('app-nav-menu-lv1')) {
scope.$broadcast('hidelevel1', name);
}else if($el.hasClass('app-nav-menu-lv2')) {
scope.$broadcast('hidelevel2', name);
}
}
});
}
}
});
Plnkr : http://plnkr.co/edit/Sreh0yIDvq4oy4Nhvlea?p=preview

In reality both animations are happening at the same time. The problem is that you have set a max-height of 1000px, so what it's happening is that while the ul that it's being opened starts displaying, the height of the one that it's supposed to hide is being reduced, but you can't notice it because it's too high and it takes a long time to start hiding the content. So, maybe you could change the max-height to 220px, like this:
.collapse.in {
max-height:220px;
}
Also, I've had a look at your directive, and I think that it could be improved quite a bit, like this (it's just a suggestion, nothing to do with your problem):
app.directive('menuaccordion', function($compile) {
return {
restrict : 'A',
compile:function (elem, attrs){
var aTag = elem.find('a');
var itemName = elem.attr('data-name');
aTag.attr('ng-click', "select('" + itemName + "')");
elem.next().attr('ng-class', "{in: selectedItem=='" + itemName + "'}");
return function(scope, elem, attrs) {
scope.select=function(name){
scope.$parent.selectedItem=scope.$parent.selectedItem==name?'':name;
};
};
}
};
});
Example

Related

Run angular function when scroll to up in chat app (pagination)

I have a chat app and I want design a pagination on scroll up event (NOT DOWN). I need a directive for this job.Also I want to show preloader in new page data load. How to implement it?
Hi its bit unclear what you really require.
I have just captured up scroll event in a directive hope this helps you.
updated
myApp.directive('scrolly', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var lastScrollTop = 0;
var raw = element[0];
console.log('loading directive');
raw.scrollTop=300;
element.bind('scroll', function () {
console.log(raw.scrollTop + raw.offsetHeight);
if(raw.scrollTop < lastScrollTop)
{
// alert("scroll up");
lastScrollTop = raw.scrollTop;
}
else{
lastScrollTop = raw.scrollTop;
}
if (raw.scrollTop ==0) {
scope.$apply(attrs.scrolly);
}
});
}
};
});

Multiple directives on same page execute all at once

I've built a directive to create a toggle menu and I have problem with it when using the same diretive multiple times on the same page.
This is the directive:
function menuTrigger($document) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
var
menuOpen = false,
elButton = angular.element(document.querySelectorAll(".menu-button")),
elContent = angular.element(document.querySelectorAll(".menu-content")),
elClose = angular.element(document.querySelectorAll("[menu-close]"));
var
pos = attrs.pos,
style;
if (pos == 'tl') {
style = {top: '0', left: '0', 'transform-origin': 'top left'}
} else if (pos == 'tr') {
style = {top: '0', right: '0', 'transform-origin': 'top right'}
} else if (pos == 'bl') {
style = {bottom: '0', left: '0', 'transform-origin': 'bottom left'}
} else if (pos == 'br') {
style = {bottom: '0', right: '0', 'transform-origin': 'bottom right'}
};
element.bind('click', function(e) {
e.stopPropagation();
openMenu();
});
elClose.bind('click', function(e) {
e.stopPropagation();
closeMenu();
});
$document.on('click', function () {
if (menuOpen == true) {
closeMenu();
};
});
function openMenu() {
menuOpen = true;
elContent.removeClass('menu-hide');
elContent.css(style);
setTimeout(function(){
elContent.addClass('menu-open');
}, 100);
};
function closeMenu() {
menuOpen = false;
elContent.removeClass('menu-open');
setTimeout(function(){
elContent.addClass('menu-hide');
elContent.removeAttr('style');
}, 400);
};
}
};
}
So, for example, if I'm using 1 menu on a main view, let's say the top navbar and then in a sub view I have other menu to control a selection, when I click on one menu, both of them will open.
How can i solve this issue?
As requested an example on how to require parent directive controllers. That should enable you to use less jQuery style code.
myModule.directive('myParentDirective', function(){
return {
controller: function(){
var vm = this;
vm.foo = 'bar';
}
};
});
myModule.directive('myChildDirective', function(){
return {
require: 'myParentDirective',
link: function(scope, elem, attrs, parentController){
console.log(parentController.foo); // equals 'bar'
}
};
});
<my-parent-directive>
<my-child-directive></my-child-directive
</my-parent-directive>
You're binding multiple times to multiple elements on your page:
// This will be an array of elements that will match the class .menu-button.
// Not just the .menu-button element within your directive.
// Try typing it in your browser developer tools console to see what I mean.
angular.element(document.querySelectorAll(".menu-button"))
If you really want to grab the individual elements from within the directive, you'll need to locate them like this:
// Use the element argument from the link function
angular.element(element[0].querySelectorAll(".menu-button"));
But -- in most cases it's easier (and more elegant) to use ng-click, ng-class directives and such. Just create your click handlers on the scope object in the directive link function and wire them in the html markup.
scope.myClickHandler = function() {
// Magic goes here
};
<div my-directive ng-click="myClickHandler"></div>
Hope this helps.

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

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.

Set focus to first invalid form element in AngularJS

Basically, what I'm trying to accomplish, is to set focus to the first invalid element after a form submit has been attempted. At this point, I have the element being flagged as invalid, and I can get the $name of the element so I know which one it is.
It's "working" but a "$apply already in progress" error is being thrown...
So I must be doing something wrong here :)
Here's my code so far:
$scope.submit = function () {
if ($scope.formName.$valid) {
// Good job.
}
else
{
var field = null,
firstError = null;
for (field in $scope.formName) {
if (field[0] != '$')
{
if (firstError === null && !$scope.formName[field].$valid) {
firstError = $scope.formName[field].$name;
}
if ($scope.formName[field].$pristine) {
$scope.formName[field].$dirty = true;
}
}
}
formName[firstError].focus();
}
}
My field looping is based on this solution, and I've read over this question a few times. It seems like the preferred solution is to create a directive, but adding a directive to every single form element just seems like overkill.
Is there a better way to approach this with a directive?
Directive code:
app.directive('ngFocus', function ($timeout, $log) {
return {
restrict: 'A',
link: function (scope, elem, attr) {
scope.$on('focusOn', function (e, name) {
// The timeout lets the digest / DOM cycle run before attempting to set focus
$timeout(function () {
if (name === attr.ngFocusId) {
if (attr.ngFocusMethod === "click")
angular.element(elem[0]).click();
else
angular.element(elem[0]).focus();
}
});
})
}
}
});
Factory to use in the controller:
app.factory('focus', function ($rootScope, $timeout) {
return function (name) {
$timeout(function () {
$rootScope.$broadcast('focusOn', name);
}, 0, false);
};
});
Sample controller:
angular.module('test', []).controller('myCtrl', ['focus', function(focus) {
focus('myElement');
}
Building a directive is definitely the way to go. There is otherwise no clean way to select in element in angularjs. It's just not designed like this. I would recommend you to check out this question on this matter.
You wouldn't have to create a single directive for every form-element. On for each form should suffice. Inside the directive you can use element.find('input');. For the focus itself I suppose that you need to include jQuery and use its focus-function.
You can howerever - and I would not recommend this - use jQuery directly inside your controller. Usually angular form-validation adds classes like ng-invalid-required and the like, which you can use as selector. e.g:
$('input.ng-valid').focus();
Based on the feedback from hugo I managed to pull together a directive:
.directive( 'mySubmitDirty', function () {
return {
scope: true,
link: function (scope, element, attrs) {
var form = scope[attrs.name];
element.bind('submit', function(event) {
var field = null;
for (field in form) {
if (form[field].hasOwnProperty('$pristine') && form[field].$pristine) {
form[field].$dirty = true;
}
}
var invalid_elements = element.find('.ng-invalid');
if (invalid_elements.length > 0)
{
invalid_elements[0].focus();
}
event.stopPropagation();
event.preventDefault();
});
}
};
})
This approach requires jquery as the element.find() uses a class to find the first invalid element in the dom.

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