AngularJS Animations with JavaScript - angularjs

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.

Related

Nested list is cut off by parent list element after slide animation

I'm building an Ionic app with nested lists of comments. I need to animate replies as child elements and retain state. Currently I'm using a simple directive with jQuery slideToggle, but this doesn't retain state (and isn't "the Angular way").
This example of slide animations by Shlomi Assaf is a great start to what I need, but it doesn't handle nested elements. I've created a nested version of his CodePen project to demonstrate the problem.
I'm not sure whether the animation function should be modified to handle nested elements, or whether my controller should call the animation on ancestor elements when the child element is animated (or after it has completed).
Assistance is appreciated. Here's the basis of the HTML using native AngularJS directives:
<button ng-click="slideToggle1=!slideToggle1">Click Me</button>
<div class="slide-toggle" ng-show="slideToggle1"> ... </div>
Here's the original animation function:
app.animation('.slide-toggle', ['$animateCss', function($animateCss) {
var lastId = 0;
var _cache = {};
function getId(el) {
var id = el[0].getAttribute("data-slide-toggle");
if (!id) {
id = ++lastId;
el[0].setAttribute("data-slide-toggle", id);
}
return id;
}
function getState(id) {
var state = _cache[id];
if (!state) {
state = {};
_cache[id] = state;
}
return state;
}
function generateRunner(closing, state, animator, element, doneFn) {
return function() {
state.animating = true;
state.animator = animator;
state.doneFn = doneFn;
animator.start().finally(function() {
if (closing && state.doneFn === doneFn) {
element[0].style.height = '';
}
state.animating = false;
state.animator = undefined;
state.doneFn();
});
}
}
return {
addClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {
height: height + 'px',
opacity: 1
},
to: {
height: '0px',
opacity: 0
}
});
if (animator) {
if (state.animating) {
state.doneFn =
generateRunner(true,
state,
animator,
element,
doneFn);
return state.animator.end();
} else {
state.height = height;
return generateRunner(true,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
},
removeClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {
height: '0px',
opacity: 0
},
to: {
height: height + 'px',
opacity: 1
}
});
if (animator) {
if (state.animating) {
state.doneFn = generateRunner(false,
state,
animator,
element,
doneFn);
return state.animator.end();
} else {
state.height = height;
return generateRunner(false,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
}
};
}]);
Here's what I'm using for now. It's a directive to set the parent element's height to auto so it expands or contracts with the child list height as it's toggled. If the parent list is toggled the height gets recalculated as normal for its animation.
app.directive('toggleParent', function () {
return {
restrict: 'C',
compile: function (element, attr) {
return function (scope, element) {
element.on('click', function (event) {
$(this).closest('.slide-toggle').css('height', 'auto');
});
}
}
}
});
CodePen demo
I'm sure that this same functionality can be implemented with the animation instead. That's what I'd really like help with.
if you want to do more than one level deep, you can use this:
jQuery
app.directive('toggleParents', function () {
return {
compile: function (element, attr) {
return function (scope, element) {
element.on('click', function (event) {
$(this).parents('.slide-toggle').css('height', 'auto');
});
}
}
}
});
Native JS
app.directive('toggleParents', function() {
return {
compile: function(element, attr) {
var parents = function(el, cls) {
var els = [];
while (el = el.parentElement) {
var hasClass = el.classList.contains(cls);
if (hasClass) {
els.push(el);
}
}
return els;
};
return function(scope, element) {
element.on('click', function(event) {
angular.element(parents(element[0], 'slide-toggle')).css('height', 'auto');
});
};
}
};
});
You only need to add else statement here:
if (closing && state.doneFn === doneFn) {
element[0].style.height = '';
} else {
element[0].style.height = 'auto';
}
This way after the animation is done, height is set to auto and that solves everything on as many levels as you want.

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.

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

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.

Can angular's ngTouch library be used to detect a long click (touch/hold/release in place) event?

My AngularJS app needs to be able to detect both the start and stop of a touch event (without swiping). For example, I need to execute some logic when the touch begins (user presses down their finger and holds), and then execute different logic when the same touch ends (user removes their finger). I am looking at implementing ngTouch for this task, but the documentation for the ngTouch.ngClick directive only mentions firing the event on tap. The ngTouch.$swipe service can detect start and stop of the touch event, but only if the user actually swiped (moved their finger horizontally or vertically) while touching. Anyone have any ideas? Will I need to just write my own directive?
Update 11/25/14:
The monospaced angular-hammer library is outdated right now, so the Hammer.js team recommend to use the ryan mullins version, which is built over hammer v2.0+.
I dug into ngTouch and from what I can tell it has no support for anything other than tap and swipe (as of the time of this writing, version 1.2.0). I opted to go with a more mature multi-touch library (hammer.js) and a well tested and maintained angular module (angular-hammer) which exposes all of hammer.js's multi-touch features as attribute directives.
https://github.com/monospaced/angular-hammer
It is a good implementation:
// pressableElement: pressable-element
.directive('pressableElement', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, $elm, $attrs) {
$elm.bind('mousedown', function (evt) {
$scope.longPress = true;
$scope.click = true;
// onLongPress: on-long-press
$timeout(function () {
$scope.click = false;
if ($scope.longPress && $attrs.onLongPress) {
$scope.$apply(function () {
$scope.$eval($attrs.onLongPress, { $event: evt });
});
}
}, $attrs.timeOut || 600); // timeOut: time-out
// onTouch: on-touch
if ($attrs.onTouch) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouch, { $event: evt });
});
}
});
$elm.bind('mouseup', function (evt) {
$scope.longPress = false;
// onTouchEnd: on-touch-end
if ($attrs.onTouchEnd) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouchEnd, { $event: evt });
});
}
// onClick: on-click
if ($scope.click && $attrs.onClick) {
$scope.$apply(function () {
$scope.$eval($attrs.onClick, { $event: evt });
});
}
});
}
};
})
Usage example:
<div pressable-element
ng-repeat="item in list"
on-long-press="itemOnLongPress(item.id)"
on-touch="itemOnTouch(item.id)"
on-touch-end="itemOnTouchEnd(item.id)"
on-click="itemOnClick(item.id)"
time-out="600"
>{{item}}</div>
var app = angular.module('pressableTest', [])
.controller('MyCtrl', function($scope) {
$scope.result = '-';
$scope.list = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 },
{ id: 7 }
];
$scope.itemOnLongPress = function (id) { $scope.result = 'itemOnLongPress: ' + id; };
$scope.itemOnTouch = function (id) { $scope.result = 'itemOnTouch: ' + id; };
$scope.itemOnTouchEnd = function (id) { $scope.result = 'itemOnTouchEnd: ' + id; };
$scope.itemOnClick = function (id) { $scope.result = 'itemOnClick: ' + id; };
})
.directive('pressableElement', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, $elm, $attrs) {
$elm.bind('mousedown', function (evt) {
$scope.longPress = true;
$scope.click = true;
$scope._pressed = null;
// onLongPress: on-long-press
$scope._pressed = $timeout(function () {
$scope.click = false;
if ($scope.longPress && $attrs.onLongPress) {
$scope.$apply(function () {
$scope.$eval($attrs.onLongPress, { $event: evt });
});
}
}, $attrs.timeOut || 600); // timeOut: time-out
// onTouch: on-touch
if ($attrs.onTouch) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouch, { $event: evt });
});
}
});
$elm.bind('mouseup', function (evt) {
$scope.longPress = false;
$timeout.cancel($scope._pressed);
// onTouchEnd: on-touch-end
if ($attrs.onTouchEnd) {
$scope.$apply(function () {
$scope.$eval($attrs.onTouchEnd, { $event: evt });
});
}
// onClick: on-click
if ($scope.click && $attrs.onClick) {
$scope.$apply(function () {
$scope.$eval($attrs.onClick, { $event: evt });
});
}
});
}
};
})
li {
cursor: pointer;
margin: 0 0 5px 0;
background: #FFAAAA;
}
<div ng-app="pressableTest">
<div ng-controller="MyCtrl">
<ul>
<li ng-repeat="item in list"
pressable-element
on-long-press="itemOnLongPress(item.id)"
on-touch="itemOnTouch(item.id)"
on-touch-end="itemOnTouchEnd(item.id)"
on-click="itemOnClick(item.id)"
time-out="600"
>{{item.id}}</li>
</ul>
<h3>{{result}}</h3>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Based on: https://gist.github.com/BobNisco/9885852
The monospaced angular-hammer library is outdated right now, so the Hammer.js team recommend to use the ryan mullins version, which is built over hammer v2.0+.

Resources