Angular Javascript animation works with ng-if but not ng-show - angularjs

I've been experimenting with angular animations and have come to the conclusion that Angular JavaScript based animations are not triggered using ng-if. I developed a simple Plunkr demonstrating the inconsistency. Essentially, the problem with this is I don't want to be appending and removing elements from the DOM (ng-if) and would rather use ng-show because one of the items being animated is an HTML5 video that I would like to begin pre-loading upon page load. The code from the Plunkr is as follows:
HTML
<button ng-click="on4=!on4">JS If Transition</button>
<div class="js-if-element" ng-if="on4">I'm animated by JS and ng-if</div>
<button ng-click="on5=!on5">JS Show Transition</button>
<div class="js-show-element" ng-show="on5">I'm animated by JS and ng-show</div>
JS
app.animation('.js-if-element', function() {
return {
enter : function(element, done) {
element.css('opacity',0);
jQuery(element).animate({
opacity: 1
}, done);
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
},
leave : function(element, done) {
element.css('opacity', 1);
jQuery(element).animate({
opacity: 0
}, done);
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
}
}
});
app.animation('.js-show-element', function() {
return {
enter : function(element, done) {
element.css('opacity',0);
jQuery(element).animate({
opacity: 1
}, done);
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
},
leave : function(element, done) {
element.css('opacity', 1);
jQuery(element).animate({
opacity: 0
}, done);
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
}
}
});
Now if you execute the code in this Plunkr the element with the ng-if directive will animate it's opacity while the element with the ng-show will not trigger the animation. Also, in the Plunkr I've tested both scenarios using keyframes/CSS transitions and both ng-show and ng-if work http://plnkr.co/edit/yzXYJnMrvYkWBnMtMLAm?p=preview

I know this is old, but incase others find it show and hide don't use enter leave. You'll have to use beforeAddClass and beforeRemoveClass.

Related

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.

Angular how to correctly destroy directive

I have a 'regionMap' directive that includes methods for rendering and destroying the map. The map is rendered inside of a modal and upon clicking the modal close button the 'regionMap' destroy method is called, which should remove the element and scope from the page. However, when returning to the modal page, that includes the 'region-map' element, the previous 'region-map' element is not removed, resulting in multiple maps being displayed. What is the correct way to remove the regionMap directive from the page when the modal is closed?
// directive
(function(){
'use strict';
angular.module('homeModule')
.directive('regionMap', regionMap);
function regionMap() {
var directive = {
restrict: 'E',
template: '',
replace: true,
link: link,
scope: {
regionItem: '=',
accessor: '='
}
}
return directive;
function link(scope, el, attrs, controller) {
if (scope.accessor) {
scope.accessor.renderMap = function(selectedRegion) {
var paper = Raphael(el[0], 665, 245);
paper.setViewBox(0, 0, 1100, 350, false);
paper.setStart();
for (var country in worldmap.shapes) {
paper.path(worldmap.shapes[country]).attr({
"font-size": 12,
"font-weight": "bold",
title: worldmap.names[country],
stroke: "none",
fill: '#EBE9E9',
"stroke-opacity": 1
}).data({'regionId': country});
}
paper.forEach(function(el) {
if (el.data('regionId') != selectedRegion.name) {
el.stop().attr({fill: '#ebe9e9'});
} else {
el.stop().attr({fill: '#06767e'});
}
});
}
scope.accessor.destroyMap = function() {
scope.$destroy();
el.remove();
}
}
}
}
})();
// controller template:
<region-map accessor="modalvm.accessor" region-item="modalvm.sregion"></region-map>
// controller:
vm.accessor = {};
...
function showMap() {
$rootScope.$on('$includeContentLoaded', function(event) {
if (vm.accessor.renderMap) {
vm.accessor.renderMap(vm.sregion);
}
});
function closeMap() {
if (vm.accessor.destroyMap) {
vm.accessor.destroyMap();
}
$modalInstance.dismiss('cancel');
}
The issue is related to loading a template with a directive inside of it. Fixed it by adding a var to check if the map has previously been rendered:
vm.accessor.mapRendered = false;
$rootScope.$on('$includeContentLoaded', function(event) {
if (vm.accessor.renderMap && !vm.accessor.mapRendered) {
vm.accessor.renderMap(vm.selectedRegions);
vm.accessor.mapRendered = true;
}
});

How to create a <<hold to confirm >> button

How would someone go about making a hold to confirm button similar to what designmodo uses?
I have a working version using jQuery but am at a loss how to incorporate this into Angular. Is this something possible with ngAnimate?
jsfiddle css:
path {
stroke-dasharray: 119;
stroke-dashoffset: 119;
}
.draw {
-webkit-animation: dash 3s ease forwards;
}
#-webkit-keyframes dash {
to {
stroke-dashoffset: 0;
}
}
jsfiddle js:
$('.delete-icon').mousedown(function() {
$('path').attr('class', 'draw');
});
$('.delete-icon').mouseup(function() {
$('path').attr('class', 'progress');
});
$("path").bind("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function(){
console.log('callback');
$('.delete-icon').hide();
});
So figured out how to do this thought I'd leave an answer in case anyone else comes across this.
Big thing was figuring out how to use jQuery to make the callback when the animation completes. There might be a way to do this with Angular but the only callbacks I could find are when the class was added/removed which is not what I needed.
http://plnkr.co/edit/Lafds0KA04mcrolR9mHg?p=preview
var app = angular.module("app", ["ngAnimate"]);
app.controller("AppCtrl", function() {
this.isHidden = false;
this.deleteIt = function() {
this.isHidden = !this.isHidden;
}
app.hidden = false;
});
app.directive("hideMe", function($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.hideMe, function(newVal) {
if(newVal) {
$animate.addClass(element, "draw");
} else {
$animate.removeClass(element, "draw");
}
});
}
});
app.animation(".draw", function() {
return {
addClass: function(element, className, done) {
//
jQuery(element).animate({
"stroke-dashoffset": 0
}, 3000, "easeOutCubic", function() {
console.log(app.hidden);
});
return function(cancel) {
if(cancel) {
jQuery(element).stop();
}
}
},
removeClass: function(element, className, done) {
//
jQuery(element).animate({
"stroke-dashoffset": 119
}, 350, "linear", function() {
console.log('canceled');
});
return function(cancel) {
jQuery(element).stop();
}
}
}
});
Ok I have a refined answer for this. View here
I dropped using animation and jQuery for two reasons:
I could not figure out how to get jQuery done to callback to
scope.
The jQuery done callback only executed on mouseup after
the animation had completed.
There are probably ways to bypass these but I couldn't figure it out.
Instead I used the angular specific animation classes that will trigger a promise on animation completion. Specifically:
.line.draw {
-webkit-animation: dash 3s ease forwards;
}
.line.draw-add {
}
.line.draw-add-active {
}
#-webkit-keyframes dash {
to {
stroke-dashoffset: 0;
}
}
I didn't need to use .line but kept it in there because lazy.
I also used isolating scope to reference the scope in the controller:
scope: {
'myAnimate': '=',
'deleteTodo': '&'
},
I think that is all the tricky parts to this solution. If anyone has any questions feel free to ask.

AngularJS Animations with JavaScript

See this JSFiddle: http://jsfiddle.net/5mUsH/3/
I'm trying to do a really simple JavaScript animation in AngularJS and jQuery. (I'm not using CSS animations because I want to support older browsers and also do more complex animations.) The code in the fiddle is from the AngularJS user guide (but slightly simplified): http://docs.angularjs.org/guide/animations
But—it doesn't work! The DOM is updated immediately (without animating). Any ideas? Thanks!
Here is the relevant markup from the JSFiddle:
<div class="sample" ng-show="checked">
Visible...
</div>
And the JavaScript:
angular.module('App', ['ngAnimate']).animation('.sample', function() {
return {
enter : function(element, done) {
element.css('opacity',0);
jQuery(element).animate({
opacity: 1
}, done);
// optional onDone or onCancel callback
// function to handle any post-animation
// cleanup operations
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
},
leave : function(element, done) {
element.css('opacity', 1);
jQuery(element).animate({
opacity: 0
}, done);
// optional onDone or onCancel callback
// function to handle any post-animation
// cleanup operations
return function(isCancelled) {
if(isCancelled) {
jQuery(element).stop();
}
}
},
}
});
I approached it in a slightly different way as the ng-show was overriding the animations. Since you wanted to use jQuery:
http://jsfiddle.net/wiredprairie/5tFCZ/1/
angular.module('App', ['ngAnimate']).animation('.sample', function () {
return {
addClass: function (element, className, done) {
if (className === 'hidden') {
jQuery(element)
.css({
opacity: 1
})
.animate({
opacity: 0
}, 500, done);
} else {
done();
}
},
removeClass: function (element, className, done) {
if (className === 'hidden') {
jQuery(element)
.css({
opacity: 0
})
.animate({
opacity: 1
}, 500, done);
} else {
done();
}
}
}
});
Basically, the hidden CSS class is toggled, then the corresponding animation code executes.

Create Hoverable popover using angular-ui-bootstrap

I have the following code for creating a popover in my template file:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-placement="right"
popover-trigger="mouseenter"
popover-popup-delay="50"
visibility>
</span>
I have a few clickable links on the popover. But the problem is I'm not able to hover on the popover created. I referred to the link http://jsfiddle.net/xZxkq/
and tried to create a directive viz. 'visibility' for this purpose.
Here is the code:
myAppModule.directive("visibility", function ($timeout,$rootScope) {
return {
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
},
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
}
}
});
But I get an exception for 'element.popover' since it is undefined. Please point as to what I'm doing wrong and how can I show/hide the angular ui popover from the directive. I am using angular ui bootstrap JS file.
I have solved it in a very cleaned way and thought to share it:
.popover is being created not as a child of the uib-popover
so the idea is to wrap uib-popover with a parent and to control show&hide on hovering the parent.
.popover and uib-popover are children of this parent
so just left to set popover-trigger=none and you have what you are wishing for.
I created a plunk example:
<span ng-init="popoverOpened=false" ng-mouseover="popoverOpened=true" ng-mouseleave="popoverOpened=false">
<button class="btn btn-default" uib-popover-html="htmlPopover"
popover-trigger="none" popover-placement="bottom-left" popover-is-open="popoverOpened" >
<span>hover me</span>
</button>
</span>
enjoy.
I don't know if this is relevant to the OP anymore, but I've had the same problem and fortunately I managed to solve it.
Undefined error
First thing first, the undefined error you are getting might be (at least in my case) because you are using the development version of ui-bootstrap. In my case I got this error when trying to bind element.popover. After adding the minified version of the library the error went away.
Keep the popover open when hovering over it
To do this I have created a custom directive that makes use of the popover from the ui-bootstrap library.
Directive
app.directive('hoverPopover', function ($compile, $templateCache, $timeout, $rootScope) {
var getTemplate = function (contentType) {
return $templateCache.get('popoverTemplate.html');
};
return {
restrict: 'A',
link: function (scope, element, attrs) {
var content = getTemplate();
$rootScope.insidePopover = false;
$(element).popover({
content: content,
placement: 'top',
html: true
});
$(element).bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
$(element).popover('show');
scope.attachEvents(element);
}
}, 200);
});
$(element).bind('mouseleave', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover)
$(element).popover('hide');
}, 400);
});
},
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
}
};
});
This directive also accepts a custom template for the popover, so you are not limited to just title and some text in it. You can create your own html template and feed it to the control.
Usage
<a href="#" hover-popover>Click here</a>
Hopes this helps someone else in the future :)
Edit
As requested, here is a Fiddle link. It lacks the styling, but it should demonstrate the way it works.
There I spend 1 day and finally get solution.
<button uib-popover="{{dynamicPopover.content}}"
popover-trigger="outsideClick" popover-is-open="popoverIsOpen"
ng-mouseenter="popoverIsOpen = !popoverIsOpen"
popover-title="{{dynamicPopover.title}}" type="button" class="btn btn-default">Dynamic Popover</button>
Please check
Plunkeer Link
Check only Dynamic Popover button code
Thanks,
I think Cosmin has the hoverable popover right, but it does seem to be using the Twitter Bootstrap popover method. The idea is to have this hoverable popover implemented only with AngularJS and one of the Bootstrap wrappers for AngularJS, which are UI Bootstrap or AngularStrap.
So I have put together an implementation which uses only AngularStrap:
myApp.directive('hoverablePopover', function ($rootScope, $timeout, $popover) {
return {
restrict: "A",
link: function (scope, element, attrs) {
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.show();
scope.attachEventsToPopoverContent();
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.hide();
}
}, 400);
});
},
controller: function ($scope, $element, $attrs) {
//The $attrs will server as the options to the $popover.
//We also need to pass the scope so that scope expressions are supported in the popover attributes
//like title and content.
$attrs.scope = $scope;
var popover = $popover($element, $attrs);
$scope.popover = popover;
$scope.insidePopover = false;
$scope.attachEventsToPopoverContent = function () {
$($scope.popover.$element).on('mouseenter', function () {
$scope.insidePopover = true;
});
$($scope.popover.$element).on('mouseleave', function () {
$scope.insidePopover = false;
$scope.popover.hide();
});
};
}
};
});
When you have a popover element, you need to take into account that you have the element that triggers the popover and you also have the element with the actual popover content.
The idea is to keep the popover open when you mouse over the element with the actual popover content. In the case of my directive, the link function takes care of the element that triggers the popover and attaches the mouseenter/mouseout event handlers.
The controller takes care of setting the scope and the popover itself via the AngularStrap $popover service. The controller adds the popover object returned by the AngularStrap service on the scope so that it is available in the link function. It also adds a method attachEventsToPopoverContent, which attaches the mouseenter/mouseout events to the element with the popover content.
The usage of this directive is like this:
<a title="Popover Title" data-placement="left" data-trigger="manual" data-content="{{someScopeObject}}" content-template="idOfTemplateInTemplateCache" hoverablePopover="">
You have to put the trigger in single quotes, because, reasons:
<button uib-popover="I appeared on mouse enter!" popover-trigger="'mouseenter'" type="button" class="btn btn-default">Mouseenter</button>
demo:
https://jsbin.com/fuwarekeza/1/edit?html,output
directive:
myAppModule.directive('popoverHoverable', ['$timeout', '$document', function ($timeout, $document) {
return {
restrict: 'A',
scope: {
popoverHoverable: '=',
popoverIsOpen: '='
},
link: function(scope, element, attrs) {
scope.insidePopover = false;
scope.$watch('insidePopover', function (insidePopover) {
togglePopover(insidePopover);
})
scope.$watch('popoverIsOpen', function (popoverIsOpen) {
scope.insidePopover = popoverIsOpen;
})
function togglePopover (isInsidePopover) {
$timeout.cancel(togglePopover.$timer);
togglePopover.$timer = $timeout(function () {
if (isInsidePopover) {
showPopover();
} else {
hidePopover();
}
}, 100)
}
function showPopover () {
if (scope.popoverIsOpen) {
return;
}
$(element[0]).click();
}
function hidePopover () {
scope.popoverIsOpen = false;
}
$(document).bind('mouseover', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = true;
scope.$digest();
}
})
$(document).bind('mouseout', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = false;
scope.$digest();
}
})
scope.$on('$destroy', function () {
$(document).unbind('mouseenter');
$(document).unbind('mouseout');
})
function inside (target) {
return insideTrigger(target) || insidePopover(target);
}
function insideTrigger (target) {
return element[0].contains(target);
}
function insidePopover (target) {
var isIn = false;
var popovers = $('.popover-inner');
for (var i = 0, len = popovers.length; i < len; i++) {
if (popovers[i].contains(target)) {
isIn = true;
break;
}
}
return isIn;
}
}
}
}]);
html:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-is-open="{{post.$open}}"
popover-trigger="click"
popover-hoverable="true"
visibility>
</span>
html
<span class="icon-globe" id="visibilityFor" popover="hello how are you"
popover-placement="right" popover-trigger="mouseenter"
popover-popup-delay="50" viz>
</span>
directive
myAppModule.directive('viz', function ($rootScope,$timeout){
return{
restrict:"A",
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
}
}
});
Note : - Don't forget to include angular-strap after jQuery.js & angular.js
This feature was added in Angular UI Bootstrap 0.14.0 and is documented here. Disable the triggers and use the popover-is-open property to manually dictate the opened/closed state.
What I did that gets my by in 0.13.X is to set the element to be hoverable to a <button> and then set the popover-trigger="focus". Then style the button how you wish, and focus the button by clicking it. You can hover in the popover and click a link, all I need to do.
Easiest way to have a mouse-event using uib-popover
Look at the below working example !
You need not have a uib-tabset, I faced an issue with uib-tabset and so added that example.
<uib-tabset>
<uib-tab>
<uib-tab-heading>
Tab 1
</uib-tab-heading>
<div>
<span ng-mouseover="popoverIsOpen = true"
ng-mouseleave="popoverIsOpen = false">
<button uib-popover-template="'includeFile.html'"
popover-trigger="outsideClick"
popover-is-open="popoverIsOpen"
popover-placement="right"
type="button" class="btn btn-default">
Dynamic Popover
</button>
</span>
</div>
<p> tab 1</p>
</uib-tab>
<uib-tab>
<uib-tab-heading>
Tab 2
</uib-tab-heading>
<p> tab 2</p>
</uib-tab>
</uib-tabset>
Template: includeFile.html
<div>
<span>This is for tesitng</span>
<strong> www.google.com</strong>
</div>
I needed to do this as well. I have a checkbox in a table cell that can have 3 possible states: Enabled, Disabled, or Special case. The UI spec I have asked for a popover over the box that shows either of those statuses, or for the special case a sentence with a link.
I tried several of these solutions and one of them worked for me, and they all added extra code. After some playing around, I determined I could just add the "popover-popup-close-delay" attribute with a dynamic value. So this works for me:
<td uib-popover-html="getPopoverTxt()" popover-popup-close-delay="{{ele.isspecial ? 2000 : 300}}" popover-popup-delay="300" popover-append-to-body="true" popover-placement="top" popover-trigger="mouseenter">
<input id="issynced{{ele.id}}" name="isChecked" type="checkbox" data-ng-checked="ele.ischecked" data-ng-model="ele.ischecked">
<label for="issynced{{ele.id}}"></label>
</td>
Some context: My table is looping over an array of data objects, so ele is a single object. The getPopoverTxt() is just a simple method in my controller that returns one of the 3 labels I want to show ("Enabled", "Disabled", or "Special Text with HTML"). Its not necessary here, but the takeaway is to get the HTML to work, you have to wrap the string value in $sce.trustAsHtml(), like:
var specialText = $sce.trustAsHtml('Text with a link to contact support');
The rest is all the usual popover and form input settings we normally use. The "popover-popup-close-delay" is the key.

Resources