AngularJS $animateCss on both page load and view change - javascript

I use this controller to grab blog posts:
appCtrl.controller('NewsCtrl', ['$scope', '$http',
function ($scope, $http) {
var posts = [];
$http.get("https://www.googleapis.com/blogger/v3/blogs/BLOG_ID/posts?&key=MY-API-KEY")
.then(function (response) {
posts = response.data;
$scope.items = posts.items;
});
console.log($scope);
}
]);
Then display them on the page like this:
<div ng-repeat="item in items" class="repeat_animation" animate-on-load>
<h2>{{item.title}}</h2>
<span ng-bind-html="item.content"></span>
</div>
I have the following css to animate each returned post:
.repeat_animation {
&.ng-enter-stagger,
&.ng-leave-stagger,
&.ng-move-stagger {
-webkit-transition-delay: .2s;
transition-delay: .2s;
transition-duration: 0s;
-webkit-transition-duration: 0s;
}
&.ng-enter,
&.ng-leave,
&.ng-move {
-webkit-transition: .5s ease-in-out all;
transition: .5s ease-in-out all;
}
&.ng-leave.ng-leave-active,
&.ng-enter,
&.ng-move {
opacity: 0;
}
&.ng-leave,
&.ng-move.ng-move-active,
&.ng-enter.ng-enter-active {
opacity: 1;
}
}
The items animate in sequence if I load/reload the News view directly but they all animate at the same time if I change to a different page view and then go back again.
However by invoking the following directive they do animate each time I navigate from one page view to the News view but they don't animate if I reload the page without navigating to it from another view.
app.directive('animateOnLoad', ['$animateCss', function ($animateCss) {
return {
'link': function (scope, element) {
$animateCss(element, {
'event': 'enter',
structural: true
}).start();
}
};
}]);
How can I force posts to always animate sequentially each time the page view is accessed, either directly or on view change?

In the end, the only way I could get staggering to work on page load and view change was by setting the from/to opacity style in the directive like this:
app.directive('animateOnLoad', ['$animateCss', function ($animateCss) {
return {
'link': function (scope, element) {
$animateCss(element, {
event: 'enter',
structural: true,
from: { 'opacity': 0 },
to: { 'opacity': 1 }
}).start();
}
};
}]);
I could not get it to work using css classes from the stylesheet (except intermittently using angular-animate.js version 1.4.3)

Related

Don't ng-show element until ng-hide CSS transition is complete?

Simple question, but I'm having implementation troubles. If I have the following DOM setup:
<h1 class="fade" ng-repeat="child in parent.children" ng-show="parent.activeChild== child ">#{{ child.title }}</h1>
When the activeChild property of the parent model changes, how can I fade out the currently active child, before the model changes, and then fade in the newly active child post-change.
I have it working roughly, with just CSS transitions using this:
.fade.ng-hide-add {
transition:opacity 1s ease;
}
.fade.ng-hide-remove {
transition:opacity 1s ease 1s;
}
.fade.ng-hide-add {
opacity:1;
&.ng-hide-add-active {
opacity:0;
}
}
.fade.ng-hide-remove {
opacity:0;
&.ng-hide-remove-active {
opacity:1;
}
}
But, this ends up producing this problem (Plunkr):
Essentially, I want to chain my animation. I've tried reading the ng-animate docs, but I'm having trouble the syntax necessary to deliver the effect I want.
I've seen the Angular docs have something like this:
app.animation('.fade', [function() {
return {
addClass: function(element, className, doneFn) {
},
removeClass: function(element, className, doneFn) {
}
};
}]);
What is className? Is it the class I want to apply while fading in/out? The class I'm expecting?
What is doneFn meant to be? I assume it's a function that's run once the animation is complete? What goes in there?
What do I do in the addClass and removeClass function then, if I already have a doneFn?
The Goal
I'd like to generate a working animation directly using Angular's ngAnimate module, with either CSS or JS. How can I achieve this?
Why do you use a separate <h1> for each heading. You can use a single <h1> tag to show your heading.
I have created a demo for your problem and I have successfully done your requirement.
Updated
Note, codes are edited to use ngAnimate module. When you use ngAnimate module, it will create a class .ng-hide when you hide an element,
Here is the controller for your app,
app2.controller("testController", ["$scope", "$timeout", function ($scope, $timeout) {
$scope.heading = {};
$scope.heading.show = true;
$scope.parent = {};
$scope.parent.children = ["A", "B", "C", "D"];
$scope.parent.activeChild = "A";
$scope.changeHeading = function (child) {
$timeout(function () {
$scope.parent.activeChild = child;
$scope.heading.show = true;
}, 1000);
}
}]);
And your html page should be look like this,
<div ng-controller="testController">
<h1 class="myAnimateClass" ng-show="heading.show" ng-class="{fadeIn : heading.fadeInModel==true, fadeOut : heading.fadeOutModel}"> {{parent.activeChild}} </h1>
<p ng-repeat="child in parent.children" ng-click="heading.show = false;changeHeading(child)">{{child}}</p>
</div>
And I have used CSS3 to implement the fade in and fade out animation,
.myAnimateClass {
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
opacity:1;
}
.myAnimateClass.ng-hide {
opacity: 0;
}
Explanation
To achieve your requirement, I have used ng-class and $timeout in angularJS.
You can see that, I have only one <h1> tag to display your heading. When I change the heading I just change it's binding property $scope.parent.activeChild.
And I have used two scope variables $scope.heading.fadeOutModel and $scope.heading.fadeInModel to add and remove classes fadeIn and fadeOut dynamically.
When user clicks to change the heading, I have added the class fadeOut to your heading. So, this will show an animation of fade out. And also I have fired a function in app.js, changeHeading().
You can see that, I forced the angular to wait for 1000 milliseconds to finish fade out animation. After this time, it will replace the selected heading to new one and add a class fadeIn. So, it will start animation for fade in.
Hope this will help you !!!
A more ng-animate way to show a specific element depending on a selection would be to use ngSwitch. This directive is used to conditionally swap DOM structure on your template based on a scope expression. Here is a example.
HTML
<button ng-repeat="item in items" ng-click="parent.selection = item">{{ item }}</button>
<div class="animate-switch-container" ng-switch on="parent.selection">
<div class="animate-switch" ng-switch-when="foo">foo</div>
<div class="animate-switch" ng-switch-when="bar">bar</div>
</div>
Javascript
$scope.items = ['foo', 'bar'];
$scope.parent = {
selection: $scope.items[0]
}
CSS
.animate-switch-container {
position:relative;
height:40px;
overflow:hidden;
}
.animate-switch {
padding:10px;
}
.animate-switch.ng-animate {
transition:opacity 1s ease;
}
.animate-switch.ng-leave.ng-leave-active,
.animate-switch.ng-enter {
opacity: 0;
}
.animate-switch.ng-leave,
.animate-switch.ng-enter.ng-enter-active {
opacity: 1;
}
This is not chaining, but it is a working animation directly using Angular's ngAnimate module. Also here is a example of it on angular's website.
You can use .animation to define animations that are Javascript based. For example, the functions you define as the values of addClass and removeClass
app.animation('.fade', [function() {
return {
addClass: function(element, className, doneFn) {
},
removeClass: function(element, className, doneFn) {
}
};
}]);
are called by Angular when it detects that you are adding or removing a class from an element, from one of the methods:
{{ }} interpoation in a template. E.g. <span class="{{shouldFade ? 'fade' : ''}}">....
Using ng-class in a template. E.g. <span ng-class="{fade: shouldFade}">...
Using the $animate service in a directive. E.g. $animate.addClass(element, 'fade') or $animate.removeClass(element, 'fade')
What is className? Is it the class I want to apply while fading in/out? The class I'm expecting?
In this example it will be fade. It a bit strange admittedly as in the example it is already clear this is the class name involved. However, if in the same digest cycle you're adding multiple classes to the same element, then the concatenation of them are passed as this string.
What is doneFn meant to be? I assume it's a function that's run once the animation is complete? What goes in there?
It's a function that you call once whatever Javascript animation you define is done. For example, to define an animation that does nothing as all:
addClass: function(element, className, doneFn) {
doneFn();
},
Calling it tells Angular that the animation has complete. This will, among other things, remove the ng-animate class from the element.
What do I do in the addClass and removeClass function then, if I already have a doneFn?
You put in them some code, perhaps using timeouts or a 3rd party library, to change the element somehow. When you have finished, you call doneFn. For example, a 1 step opacity "animation":
addClass: function(element, className, doneFn) {
element.css('opacity', 0.5);
setTimeout(function() {
doneFn();
}, 1000);
},
I'd like to generate a working animation directly using Angular's ngAnimate module, with either CSS or JS.
This doesn't really have much to do with the answers above! If I were doing a real-case, I strongly suspect I would position the elements absolutely, as anything else (that I can think of) at least, is a bit overly complicated.
However, if you do really want to chain the animations using ngAnimate, one possible way is to use the fact that $animate.addClass and $animate.removeClass returns a promise when it completes. In order to chain onto the end of such a promise returned when hiding an element, it must be called from some sort of central location, and keep track of which element is visible, being hidden, and being shown.
A way of doing this is to use 2 custom directives. One will be on each element to show and hide, that could be used very much like ngShow. The other will be a parent directive that will allow only one element to be visible at any time, and chain removal of the ng-hide class (and associated animations) after any addition of ng-hide. The directives will have to communicate, could be called something like ngShowUnique and ngShowUniqueController, such as in the following example.
<div ng-show-unique-controller>
<h1 class="fade" ng-repeat="child in parent.children" ng-show-unique="parent.activeChild == child">#{{child.title}}</h1>
</div>
and they could be implemented as below.
app.directive('ngShowUniqueController', function($q, $animate) {
return {
controller: function($scope, $element) {
var elements = [];
var expressions = [];
var watchers = [];
var unregisterWatchers = null;
var visibleElement = null;
function registerWatchers() {
unregisterWatchers = $scope.$watchGroup(expressions, function(vals) {
var newCurrentIndex = vals.indexOf(true);
var addPromise;
if (visibleElement) {
// Set a fixed height, as there is a brief interval between
// removal of this class and addition of another
$element.css('height', $element[0].getBoundingClientRect().height + 'px');
addPromise = $animate.addClass(visibleElement, 'ng-hide');
} else {
addPromise = $q.when();
}
visibleElement = elements[newCurrentIndex] || null;
if (!visibleElement) return;
addPromise.then(function() {
if (visibleElement) {
$animate.removeClass(visibleElement, 'ng-hide').then(function() {
$element.css('height', '');
});
}
})
});
}
this.register = function(element, expression) {
if (unregisterWatchers) unregisterWatchers();
elements.push(element[0]);
expressions.push(expression);
registerWatchers();
// Hide elements initially
$animate.addClass(element, 'ng-hide');
};
this.unregister = function(element) {
if (unregisterWatchers) unregisterWatchers();
var index = elements.indexOf(element[0]);
if (index > -1) {
elements.splice(index, 1);
expressions.splice(index, 1);
}
registerWatchers();
};
}
};
});
app.directive('ngShowUnique', function($animate) {
return {
require: '^ngShowUniqueController',
link: function(scope, element, attrs, ngShowUniqueController) {
ngShowUniqueController.register(element, function() {
return scope.$eval(attrs.ngShowUnique);
});
scope.$on('$destroy', function() {
ngShowUniqueController.unregister(element);
});
}
};
});
This can be seen at http://plnkr.co/edit/1eJUou4UaH6bnAN0nJn7?p=preview . I have to admit, it's all a bit faffy.
using ngRepeat that shows only one element at time, in my opinion, is a bad idea... because you're showing only one element!
you can use the parent.activeChild property directly...
Have a look on what follows:
Note: I did this snippet in just ten minutes, it's unoptimized and can have some bug... you can use it as starter :)
(function(window, angular, APP) {
APP
.value('menuObject', {
name: 'Main Navigation',
current: null,
children: [{
label: 'Don\'t ng-show element until ng-hide CSS transition is complete?',
url: 'http://stackoverflow.com/questions/33336249/dont-ng-show-element-until-ng-hide-css-transition-is-complete',
isCurrent: false
},
{
label: 'Hitmands - Linkedin',
url: 'http://it.linkedin.com/in/giuseppemandato',
isCurrent: false
},
{
label: 'Hitmands - Github',
url: 'https://github.com/hitmands',
isCurrent: false
},
{
label: 'Hitmands - StackOverflow',
url: 'http://stackoverflow.com/users/4099454/hitmands',
isCurrent: false
}
]})
.directive('menu', function(menuObject, $q) {
function menuCtrl($scope, $element) {
$scope.parent = menuObject;
this.getCurrentChild = function() {
return $scope.parent.current;
};
this.getDomContext = function() {
return $element;
};
this.setCurrentChild = function(child) {
return $q.when($scope.parent)
.then(function(parent) {
parent.current = child;
return parent;
})
.then(function(parent) {
return parent.children.forEach(function(item) {
item.isCurrent = child && (item.label === child.label);
});
})
};
}
return {
restrict: 'A',
templateUrl: 'embedded-menutemplate',
scope: {},
controller: menuCtrl
};
})
.directive('menuItem', function($animate, $q, $timeout) {
function menuItemPostLink(iScope, iElement, iAttributes, menuCtrl) {
iElement.bind('click', setCurrentTitle);
iScope.$on('$destroy', function() {
iElement.unbind('click', setCurrentTitle);
})
function setCurrentTitle(event) {
event.preventDefault();
var title;
return $q
.when(menuCtrl.getDomContext())
.then(function(_menuElement) {
title = angular.element(
_menuElement[0].querySelector('#menuItemCurrent')
);
})
.then(function() {
return title.addClass('fade-out');
})
.then(function() {
return $timeout(menuCtrl.setCurrentChild, 700, true, iScope.child);
})
.then(function() {
return title.removeClass('fade-out');
})
}
}
return {
require: '^menu',
link: menuItemPostLink,
restrict: 'A'
};
})
;
})(window, window.angular, window.angular.module('AngularAnimationExample', ['ngAnimate']));
nav {
text-align: center;
}
.link {
display: inline-block;
background-color: lightseagreen;
color: black;
padding: 5px 15px;
margin: 1em;
}
#menuItemCurrent {
padding: 1em;
text-transform: uppercase;
border: 1px solid black;
}
#menuItemCurrent span {
transition: 500ms opacity linear;
opacity: 1;
}
#menuItemCurrent.fade-out span {
opacity: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-animate.js"></script>
<article ng-app="AngularAnimationExample">
<nav menu></nav>
<script id="embedded-menutemplate" type="text/ng-template">
<nav >
<a menu-item class="link" ng-repeat="child in parent.children track by $index" ng-bind="child.label" ng-href="{{ child.url }}"></a>
<h1 id="menuItemCurrent"><span ng-bind="parent.current.url || 'NoMenuCurrentSelected'"></span></h1>
{{ parent.current || json }}
</nav>
</script>
</article>
The problem is that H1 is a block level element that is positioned within it's parent and no overlap is allowed. That is why you see one animation that's disappearing pushing down the animation that is appearing.
You can see that this is happening more clearly here: Demo
To fix this, you want to keep the block level element H1, and make its position relative, so that it can keep its relative position in the overall flow of the page. Then set the child SPAN elements to have absolute positioning - absolute position relative to the parent H1. This allows all span elements to overlap each other.
CSS
.fade {
opacity: 1;
position: relative;
}
.fade.ng-hide-add {
transition:opacity 1s ease;
position: absolute;
}
.fade.ng-hide-remove {
transition:opacity 1s ease 1s;
position: absolute;
}
.fade.ng-hide-add {
opacity:1;
}
.fade.ng-hide-add.ng-hide-add-active {
opacity:0;
}
.fade.ng-hide-remove {
opacity:0;
}
.fade.ng-hide-remove.ng-hide-remove-active {
opacity:1;
}
HTML
<body ng-controller="MainCtrl">
<h1><span class="fade" ng-repeat="child in parent.children" ng-show="parent.activeChild == child ">#{{child.title}}</span></h1>
<button ng-repeat="child in parent.children" ng-click="parent.activeChild = child">{{ child.title }}</button>
</body>
There is one problem though... Since the SPAN elements have absolute positioning, it is removed from flow when animating, and the parent H1 can't resize to fit the SPAN contents. This causes the SPAN to jump unexpectedly.
The way to address this (and admittedly, it's a bit of a hack) is by adding an empty space after the SPAN repeater. So when the ngRepeat SPANS get pulled out of normal flow because of absolute positioning, the empty space which is outside the ngRepeat preserves the spacing of the H1.
Here is a working Plunker.
You might want to look into transitionend event which is supported by all modern browsers.
element.addEventListener('transitionend', callback, false);
Quick answer to this - To solve this problem in the past I have always positioned the content absolute. This way when the transition takes place it stays in the same position.
There is no other way around it because the content takes up space in the dom if its inline or inline-block which is why you see the jump until the transition is finished

How do you animate with AngularJs?

I've got some frustration with ng-animate. I've spent two days reading about it, and I still don't understand how it works. Here is what I'm doing.
I have an element that I can drag around with my mouse. When the element reaches a specific threshold (130px left or right), I want to reset and animate it back to it's original position (0).
My element isn't entering, or leaving the Dom; and even though it's "moving" it's not changing it's position within the DOM, so it's not really "ng-move"ing.
UPDATE
By popular request, here's some code
app.directive('ngSwipeMessage', function ($touch, $animate) {
const leftThreshold = -130;
const rightThreshold = 130;
return {
restrict: 'A',
link: function ($scope, element, attr, controller) {
var handler = $touch.bind(element, {
start: function (touch, ev) {
console.log("start");
},
end: function (touch, ev) {
console.log("end");
// if they stop moving, then reset
//$animate.addClass(element, "ng-move ng-move-active");
},
move: function (touch, ev) {
console.log("move");
element.css({
left: touch.distanceX
});
if (touch.distanceX < leftThreshold || touch.distanceX > rightThreshold) {
// fire our handler action and reset the element position
//$animate.addClass(element, "ng-move ng-move-active");
}
}
});
$scope.$on('$destroy', function () {
console.log("destroying");
handler();
});
}
};
});
And here is the CSS
#list-messages .message.ng-move {
-webkit-transition: 0.2s linear all;
-moz-transition: 0.2s linear all;
-o-transition: 0.2s linear all;
transition: 0.2s linear all;
/* future proof */
}
#list-messages .message.ng-move.ng-move-active {
left: 0 !important;
}
So the things I've tried using is $animate.leave/enter/move but they appear to actually want to leave, enter, or move around the DOM. I've started experimenting with addClass/removeClass, which has shown some promise. Overall, creating a custom animation isn't (in my opinion) documented very well.
Any help is appreciated. Thanks.

Coordinates and rotation of elements not loaded by jmpress when triggered using angular $scope.$watch

Use Case
jmpress requires that the DOM be constructed before loading. Using AngularJS, the DOM can be constructed with templates and jmpressinitialized with a directive watching the DOM.
When the data is loaded during DOM loading, i.e. hard coded data and jmpress is triggered on the $(document).ready() event, the desired effect is observed. See jsfiddle.
However, when the data is loaded over an AJAX request and jmpress is triggered over a watch, the coordinates and rotation attributes are ignored by jmpress.
Problem
I suspect that the watch fires before the template is constructed, so jmpress sees that the coordinates and rotations in the DOM are blank.
Question
Is there a way to get jmpress to use the coordinates and rotations from the DOM created with the angular template?
Is it just better to add the coordinates and rotations as attributes in the link function rather than using the template?
Code
A jsFiddle using AJAX request is created.
Template
<div ng-app="app" ng-controller="randomData" ui-impress>
<div ng-repeat="item in data"
class="step"
data-duration="2000"
data-x="{{ item.coordinates.x }}"
data-y="{{ item.coordinates.y }}"
data-z="{{ item.coordinates.z }}"
data-rotate-x="{{ item.rotate.x }}"
data-rotate-y="{{ item.rotate.y }}"
data-rotate-z="{{ item.rotate.z }}"
data-scale="{{ item.scale }}"
>
{{ item.message }}
</div>
</div>
CSS
.step {
opacity: 0.1;
width: 20em;
height: 3em;
}
.step.active {
-webkit-transition: opacity 1s;
-moz-transition: opacity 1s;
-ms-transition: opacity 1s;
-o-transition: opacity 1s;
transition: opacity 1s;
opacity: 1;
}
Directive
var app = angular.module("app", []);
app.directive('uiImpress', function() {
return {
restrict: 'A',
scope: false,
link: function($scope, $element) {
var init = $scope.$watch('data', function(data) {
if (data) {
$($element).children('.step').each(function(index, step) {
$($element).jmpress('init', step);
});
$($element).jmpress();
init();
};
});
}
};
});
Controller
Basically, just a random data generator.
function randomData($scope, $http) {
$scope.data;
var slides = 10;
var config = {
coordinates: [ -1000, 1000 ],
rotate: [ -180, 180 ],
scale: [ 0.1, 10 ]
};
var randomDataSource = "http://api.icndb.com/jokes/random/" + slides;
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateData(messages) {
return _.range(slides).map(function(value, index) {
return {
message: messages[index],
coordinates : {
x: getRandomInt.apply(null, config.coordinates),
y: getRandomInt.apply(null, config.coordinates),
z: getRandomInt.apply(null, config.coordinates)
},
rotate: {
x: getRandomInt.apply(null, config.rotate),
y: getRandomInt.apply(null, config.rotate),
z: getRandomInt.apply(null, config.rotate)
},
scale: getRandomInt.apply(null, config.scale)
};
});
}
$http.get(randomDataSource).then(function(response) {
var messages = response.data.value.map(function(item) {
return item.joke;
});
$scope.data = generateData(messages);
})
};
In the listener function of the $watch, remove the following code:
$($element).children('.step').each(function(index, step) {
$($element).jmpress('init', step);
});
Wrap the remaining code in a $evalAsync call:
var unregister = $scope.$watch('data', function(data) {
if (data) {
$scope.$evalAsync(function() {
$($element).jmpress();
unregister();
});
};
});
This delays the code execution until after the DOM has been manipulated by Angular, but before the browser renders.
Demo - $evalAsync: http://jsfiddle.net/CV2JQ/
It's good to know that if you need the code to be executed after the rendering (depends on the plugin) you can instead inject and use the $timeout service:
$timeout(function () {
$($element).jmpress();
init();
});
Demo - $timeout: http://jsfiddle.net/c9v9G/
If both alternatives work I prefer the first one as it executes the code before the rendering, which can in some cases remove flickering. In this case it doesn't seem to matter though.

Show loading animation for slow script using AngularJS?

In angularjs 1.2 operations like filtering an ng-repeat with many rows (>2,000 rows) can become quite slow (>1 sec).
I know I can optimize execution times using limitTo, pagination, custom filters, etc. but I'm still interested to know if it's possible to show a loading animation while the browser is busy running long scripts.
In case of angular I think that could be invoked whenever $digest is running because that seems to be the main function that takes up most time and might be called several times.
In a related question there were no useful answers given. Any help greatly appreciated!
The problem is that as long as Javascript is executing, the UI gets no chance to update. Even if you present a spinner before filtering, it will appear "frozen" as long as Angular is busy.
A way to overcome this is to filter in chunks and, if more data are available, filter again after a small $timeout. The timeout gives the UI thread a chance to run and display changes and animations.
A fiddle demonstrating the principle is here.
It does not use Angular's filters (they are synchronous). Instead it filters the data array with the following function:
function filter() {
var i=0, filtered = [];
innerFilter();
function innerFilter() {
var counter;
for( counter=0; i < $scope.data.length && counter < 5; counter++, i++ ) {
/////////////////////////////////////////////////////////
// REAL FILTER LOGIC; BETTER SPLIT TO ANOTHER FUNCTION //
if( $scope.data[i].indexOf($scope.filter) >= 0 ) {
filtered.push($scope.data[i]);
}
/////////////////////////////////////////////////////////
}
if( i === $scope.data.length ) {
$scope.filteredData = filtered;
$scope.filtering = false;
}
else {
$timeout(innerFilter, 10);
}
}
}
It requires 2 support variables: $scope.filtering is true when the filter is active, giving us the chance to display the spinner and disable the input; $scope.filteredData receives the result of the filter.
There are 3 hidden parameters:
the chunk size (counter < 5) is small on purpose to demonstrate the effect
the timeout delay ($timeout(innerFilter, 10)) should be small, but enough to give the UI thread some time to be responsive
the actual filter logic, which should probably be a callback in real life scenarios.
This is only a proof of concept; I would suggest refactoring it (to a directive probably) for real use.
Here are the steps:
First, you should use CSS animations. No JS
driven animations and GIFs should be used within heavy processes bec. of the single thread limit. The animation will freeze. CSS animations are separated from the UI thread and they are supported on IE 10+ and all major browsers.
Write a directive and place it outside of your ng-view with
fixed positioning.
Bind it to your app controller with some special flag.
Toggle this directive's visibility before and after long/heavy processes.
(You can even bind a text message to the directive to display some
useful info to the user). -- Interacting with this or anything else directly within a loop of heavy process will take way longer time to finish. That's bad for the user!
Directive Template:
<div class="activity-box" ng-show="!!message">
<img src="img/load.png" width="40" height="40" />
<span>{{ message }}</span>
</div>
activity Directive:
A simple directive with a single attribute message. Note the ng-show directive in the template above. The message is used both to toggle the activity indicator and also to set the info text for the user.
app.directive('activity', [
function () {
return {
restrict: 'EA',
templateUrl: '/templates/activity.html',
replace: true,
scope: {
message: '#'
},
link: function (scope, element, attrs) {}
};
}
]);
SPA HTML:
<body ng-controller="appController">
<div ng-view id="content-view">...</div>
<div activity message="{{ activityMessage }}"></div>
</body>
Note that the activity directive placed outside of ng-view. It will be available on each section of your single-page-app.
APP Controller:
app.controller('appController',
function ($scope, $timeout) {
// You would better place these two methods below, inside a
// service or factory; so you can inject that service anywhere
// within the app and toggle the activity indicator on/off where needed
$scope.showActivity = function (msg) {
$timeout(function () {
$scope.activityMessage = msg;
});
};
$scope.hideActivity = function () {
$timeout(function () {
$scope.activityMessage = '';
}, 1000); // message will be visible at least 1 sec
};
// So here is how we do it:
// "Before" the process, we set the message and the activity indicator is shown
$scope.showActivity('Loading items...');
var i;
for (i = 0; i < 10000; i += 1) {
// here goes some heavy process
}
// "After" the process completes, we hide the activity indicator.
$scope.hideActivity();
});
Of course, you can use this in other places too. e.g. you can call $scope.hideActivity(); when a promise resolves. Or toggling the activity on request and response of the httpInterceptor is a good idea too.
Example CSS:
.activity-box {
display: block;
position: fixed; /* fixed position so it doesn't scroll */
z-index: 9999; /* on top of everything else */
width: 250px;
margin-left: -125px; /* horizontally centered */
left: 50%;
top: 10px; /* displayed on the top of the page, just like Gmail's yellow info-box */
height: 40px;
padding: 10px;
background-color: #f3e9b5;
border-radius: 4px;
}
/* styles for the activity text */
.activity-box span {
display: block;
position: relative;
margin-left: 60px;
margin-top: 10px;
font-family: arial;
font-size: 15px;
}
/* animating a static image */
.activity-box img {
display: block;
position: absolute;
width: 40px;
height: 40px;
/* Below is the key for the rotating animation */
-webkit-animation: spin 1s infinite linear;
-moz-animation: spin 1s infinite linear;
-o-animation: spin 1s infinite linear;
animation: spin 1s infinite linear;
}
/* keyframe animation defined for various browsers */
#-moz-keyframes spin {
0% { -moz-transform: rotate(0deg); }
100% { -moz-transform: rotate(359deg); }
}
#-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(359deg); }
}
#-o-keyframes spin {
0% { -o-transform: rotate(0deg); }
100% { -o-transform: rotate(359deg); }
}
#-ms-keyframes spin {
0% { -ms-transform: rotate(0deg); }
100% { -ms-transform: rotate(359deg); }
}
#keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
Hope this helps.
Use spin.js and the site http://fgnass.github.com/spin.js/ shows the step which is quite easy.
the loading animation is in CSS which separated from the UI thread and therefore loaded smoothly.
What you can do is detect the end of the ngRepeat as this post says.
I'll do something like, in the controller:
$scope.READY = false;
And in the directive, as the post above says, I'll do something like:
if (scope.$last) {
$scope.READY = true;
}
And you can have a css based loader/spinner with
<div class="loader" ng-show="!READY">
</div>
Ofcourse you can also have css based animations which are independent of js execution.
You could run the filter in another thread using WebWorkers, so your angularjs page won't block.
If you don't use webworkers the browser could get a javascript execution timeout and stop your angular app completely and even if you don't get any timeout your application freezes until the calculation is done.
UPDATE 23.04.14
I've seen a major performance improvement in a large scale project using scalyr and bindonce
Here is an working example :-
angular
.module("APP", [])
.controller("myCtrl", function ($scope, $timeout) {
var mc = this
mc.loading = true
mc.listRendered = []
mc.listByCategory = []
mc.categories = ["law", "science", "chemistry", "physics"]
mc.filterByCategory = function (category) {
mc.loading = true
// This timeout will start on the next event loop so
// filterByCategory function will exit just triggering
// the show of Loading... status
// When the function inside timeout is called, it will
// filter and set the model values and when finished call
// an inbuilt $digest at the end.
$timeout(function () {
mc.listByCategory = mc.listFetched.filter(function (ele) {
return ele.category === category
})
mc.listRendered = mc.listByCategory
$scope.$emit("loaded")
}, 50)
}
// This timeout is replicating the data fetch from a server
$timeout(function () {
mc.listFetched = makeList()
mc.listRendered = mc.listFetched
mc.loading = false
}, 50)
$scope.$on("loaded", function () { mc.loading = false })
})
function makeList() {
var list = [
{name: "book1", category: "law"},
{name: "book2", category: "science"},
{name: "book1", category: "chemistry"},
{name: "book1", category: "physics"}
]
var bigList = []
for (var i = 0; i <= 5000; i++) {
bigList = bigList.concat(list)
}
return bigList
}
button {
display: inline-block;
}
<html>
<head>
<title>This is an Angular Js Filter Workaround!!</title>
</head>
<body ng-app="APP">
<div ng-controller="myCtrl as mc">
<div class = "buttons">
<label>Select Category:- </label>
<button ng-repeat="category in mc.categories" ng-click="mc.filterByCategory(category)">{{category}}</button>
</div>
<h1 ng-if="mc.loading">Loading...</h1>
<ul ng-if="!mc.loading">
<li ng-repeat="ele in mc.listRendered track by $index">{{ele.name}} - {{ele.category}}</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
</body>
<html>
Promise/deferred can be used in this case, you can call notify to watch the progress of your code, documentation from angular.js: https://code.angularjs.org/1.2.16/docs/api/ng/service/$q
Here is a tutorial on heavy JS processing that uses ng-Promise: http://liamkaufman.com/blog/2013/09/09/using-angularjs-promises/, hope it is helpful.
//app Factory for holding the data model
app.factory('postFactory', function ($http, $q, $timeout){
var factory = {
posts : false,
getPosts : function(){
var deferred = $q.defer();
//avoiding the http.get request each time
//we call the getPosts function
if (factory.posts !== false){
deferred.resolve(factory.posts);
}else{
$http.get('posts.json')
.success(function(data, status){
factory.posts = data
//to show the loading !
$timeout(function(){
deferred.resolve(factory.posts)
}, 2000);
})
.error(function(data, status){
deferred.error('Cant get the posts !')
})
};
return deferred.promise;
},
getPost : function(id){
//promise
var deferred = $q.defer();
var post = {};
var posts = factory.getPosts().then(function(posts){
post = factory.posts[id];
//send the post if promise kept
deferred.resolve(post);
}, function(msg){
deferred.reject(msg);
})
return deferred.promise;
},
};
return factory;
});
You can use this code taken from this url:
http://www.directiv.es/angular-loading-bar
there you can find a workin demo also.
Here is thei code:
<!DOCTYPE html>
<html ng-app="APP">
<head>
<meta charset="UTF-8">
<title>angular-loading-bar example</title>
<link rel="stylesheet" type="text/css" href="/application/html/js/chieffancypants/angular-loading-bar/loading-bar.min.css"/>
<style>
body{
padding:25px;
}
</style>
</head>
<body ng-controller="ExampleController">
<button id="reloader" ng-click="getUsers()">Reload</button>
<ul ng-repeat="person in data">
<li>{{person.lname}}, {{person.fname}}</li>
</ul>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular-animate.min.js"></script>
<script src="/application/html/js/chieffancypants/angular-loading-bar/loading-bar.min.js"></script>
<script>
angular.module('APP', ['chieffancypants.loadingBar', 'ngAnimate']).
controller("ExampleController",['$scope','$http',function($scope,$http){
$scope.getUsers = function(){
$scope.data=[];
var url = "http://www.filltext.com/?rows=10&fname={firstName}&lname={lastName}&delay=3&callback=JSON_CALLBACK"
$http.jsonp(url).success(function(data){
$scope.data=data;
})
}
$scope.getUsers()
}])
</script>
</body>
</html>
How do I use it?
Install it via npm or bower
$ npm install angular-loading-bar
$ bower install angular-loading-bar
To use, simply include it as a dependency in your app and you're done!
angular.module('myApp', ['angular-loading-bar'])

How do I animate between CSS classes using ngAnimate?

In my app, I want to show a loading indication during loading. I have implemented this successfully:
lyr.ui.directive('loading', ['$timeout', function($timeout) {
return function(scope, element, attr) {
element.addClass('content-loading');
var unbind = scope.$watch(attr.loading, function(value) {
if (value) {
element.removeClass('content-loading');
element.addClass('content-loaded');
$timeout(function() {
element.removeClass('content-loaded');
}, 250);
unbind();
}
});
};
}]);
Plunker: http://plnkr.co/edit/L6opqA?p=preview
However, I think I'm duplicating a lot of ngAnimate code in adding and removing classes, and in hard-coding the removal of content-loaded using a hard-coded timeout.
Is it possible to convert my example to use ngAnimate with less duplicated code?
It's hard to figure this out myself because the documentation on ngAnimate does not focus on JS-triggered animations.
Small note that is not reflected in the plunker: I have to render the element's children during loading, because the element contains a jQuery plugin that calls $.width, which would return 0 for elements that aren't rendered.
I found another plunker that showed me how to do this.
Solution: http://plnkr.co/edit/bzIReo?p=preview
myApp.directive('loading', ['$animate', function($animate) {
return function(scope, element, attr) {
element.addClass('content-loading');
var unbind = scope.$watch(attr.loading, function(value) {
if (value) {
$animate.removeClass(element, 'content-loading');
unbind();
}
});
};
}]);
CSS:
.content-loading {
background-image: url(data:image/gif;base64,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);
background-repeat: no-repeat;
background-position: 20px 10px;
}
.content-loading > * {
opacity: 0;
}
/* Class name generated by ngAnimate */
.content-loading-remove > * {
-webkit-transition: opacity 250ms linear;
-moz-transition: opacity 250ms linear;
transition: opacity 250ms linear;
opacity: 1;
}

Categories

Resources