I am new to AngularJS and would like to implement route dependent page transitions. For example, I would like the page to slide left, slide right or fade depending on the route.
My 'Plunker' below achieves this by listening to the $routeChangeSuccess event and then applying a transition style specific CSS class to the entering and leaving view (inspired by http://phillippuleo.com/articles/scalable-approach-page-transitions-angularjs):
http://plnkr.co/edit/ee4CHfb8kZC1WxtDM9wr?p=preview
However, the call to $scope.$apply() in the event listener makes AngularJS issue an error message '$digest already in progress'. But if I don't call $scope.$apply() the CSS class of the leaving view is not updated and the animation does not work correctly.
What is going on here?
I looked in your plunker. The problem is with the way you use classes to animate your views.
When the $routeChangeSuccess event is fired, ngView had already removed the class before you get the chance of changing the direction. You override it by applying the new class so quickly so it would not be noticed but then you get the digest in progress error.
My solution (plunker):
I came up with a directive:
app.directive('animClass',function($route){
return {
link: function(scope, elm, attrs){
var enterClass = $route.current.animate;
elm.addClass(enterClass);
scope.$on('$destroy',function(){
elm.removeClass(enterClass);
elm.addClass($route.current.animate);
})
}
}
});
Declare an animate option for each route:
app.config(function($routeProvider) {
$routeProvider.
when("/page1", {
templateUrl: "page1.html",
controller: "Page1Ctrl",
animate: "slideLeft"
}).
when("/page2", {
templateUrl: "page2.html",
controller: "Page2Ctrl",
animate: "slideRight"
}).
otherwise({
redirectTo: "/page1"
});
});
And just add it to ngView like so:
<div ng-view ng-controller="ViewCtrl" anim-class class="view"></div>
css:
.view {
width: 100%;
padding-left: 1em;
position:absolute;
top: 0;
left: 0;
}
.slideLeft.ng-enter, .slideLeft.ng-leave, .slideRight.ng-enter, .slideRight.ng-leave {
-webkit-transition:all 1s;
transition:all 1s;
}
.slideLeft.ng-enter {
left:100%;
}
.slideLeft.ng-enter.ng-enter-active {
left:0;
}
.slideLeft.ng-leave.ng-leave-active {
left:-100%;
}
.slideRight.ng-enter {
left:-100%;
}
.slideRight.ng-enter.ng-enter-active {
left:0;
}
.slideRight.ng-leave.ng-leave-active {
left:100%;
}
Currently my approach to this is to check whether angular is already within a digest cycle or not by using this snippet:
if (!($scope.$$phase)) $scope.$apply();
That's not very pretty, but unfortunately it's the only approach I've discovered so far for exactly the problem you're describing.
Related
I'm triying to add an nganimation for my main loader page with no luck :(. This is how I'm trying to achieve this. Im using a directive:
<html>
...
<loader-gui></loader-gui>
</html>
which has this code inside:
<div class="loader-gui" loader>
<img src="img/loader.gif"/>
</div>
with this directive:
angular.module('myapp.directives', [])
.directive('loaderGui', function() {
return {
restrict: 'E',
templateUrl: 'partials/loader-gui.html'
}
})
.directive('loader', ['$http', function ($http) {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
$scope.isLoading = function () {
return $http.pendingRequests.length > 0;
};
$scope.$watch($scope.isLoading, function (value) {
if (value) {
element.removeClass('ng-hide');
} else {
element.addClass('ng-hide');
}
});
}
};
}]);
So, this is working perfect, after load all the resources and http requests my loader-gui dissapears. But I want to make it more fancy using nganimate to make a fadeout effect, simple.
I've added the library, the module into my app with out any errors and my custom CSS.
myapp.css
.ng-hide {
opacity:0;
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
myapp.js
angular.module('myapp', ['ngAnimate', 'myapp.directives']);
But it's not working, it's just dissapear without any effect. Am I missing something? Any advice? I've read samples with methods fired up trough a $scope but not using a directive.
If you haven't included ngAnimate as a dependency in your main module, you certainly would want to do so here.
angular.module('myapp.directives', ['ngAnimate'])
Nowadays the best practice to make animation for ngHide and some other directives is by handling the "transition" classes which automatically added and removed during transition by the ngAnimate. For ngHide and ngShow the transition classes is named .ng-hide-add and .ng-hide-remove.
On the official angular documentation for ngHide they give good example to playing with those 2 classes. Please check this out for more detail example how to do that : https://docs.angularjs.org/api/ng/directive/ngHide#example . Please pay attention at the animate-hide which they add to the element that need to be animate. And also the CSS file which contain detail of class styling for the animation.
I'm using state router to transition between pages.
I need to add a class to the <body> while the animation is running and remove it once the enter and leave animations are completed.
I tried to create a directive an inject the $animate service.
Then I started listening for enter and leave events as suggest in documentation.
The html:
<div class="ui-view-container">
<div ui-view style="height:100%;" class="suffle-page" suffle-page></div>
</div>
The directive:
;(function(){
angular.module('app')
.directive('sufflePage',function($animate){
var $body = $('body');
return {
link: function (scope, element) {
//var $el = $('[ui-view]');
$animate.enter(element,
function callback(element, phase) {
//$body.addClass('animating');
}
);
$animate.leave( element, function(){
function callback(element, phase) {
//$body.removeClass('animating')
}
})
}
}
});
})();
Then I have the CSS that animates those views
//prevents animation in mobile devices to faster performance
.ui-view-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}
[ui-view].ng-enter, [ui-view].ng-leave {
...
}
[ui-view].ng-enter {
..
}
[ui-view].ng-enter-active {
..
}
[ui-view].ng-leave {
...
}
[ui-view].ng-leave-active {
...
}
body.animating{
/*this element is outter of the animation that's why i must append a class to the top level element. in this case body*/
.special-element{
display: none;
}
}
At $animate.enter(element...) an error is thrown:
TypeError: Cannot read property 'createDocumentFragment' of null
Any help?
I was misunderstanding the use of $animate.enter and $animate.leave and **I also did use an incorrect version of angular because the $animate.leave are part of 1.4.x versions an my project was built on top of version 1.3.0.
After updating the angular.js and angular-animate.js all i had to do was
1) create the directive that will monitor enter:start and enter:end events
2) load the directive into the project
3) and write the piece of code that adds the class to the body during the animation.
I hope it helps.
.directive('sufflePage',function($animate){
var $body = $('body');
return {
link: function (scope, element) {
if (!element){
return;
}
/***
* when the ui-view that is `entering` the page stars it adds the animating class to body
* when it leaves it removes the animating from the body class
*
* IMPORTANT: this works because the enter and exit animation are triggered in parallel with the same duration
*
*/
$animate.on('enter', element,
function callback(element, phase) {
if (phase == 'start'){
$body.addClass('animating');
} else {
$body.removeClass('animating');
}
}
);
scope.$on('$destroy', function(){
$animate.off('enter',element);
});
}
}
I'm playing with transitions and directives. I've created a Card directive that should show a clone of it self in fullscreen when clicked. The transition doesn't happen if I don't apply the altering css class in a timeout. Is that how it should be done?
<div ng-app='trans'>
<div data-card class='card'>timeout</div>
<div data-card='notimeout' class='card'>not timeout</div>
</div>
Between to original position and the fullscreen mode it should transition with a spin. The goto class is just so that i can add/remove transitions so that the card doesn't transition widht/height when the window is resized. I think it reads nice too =)
.card {
width:10vh;
height:14vh;
background-color:pink;
margin: 10px;
}
.card.goto.fullscreen {
transition: all 0.6s linear;
}
.card.fullscreen {
height:95vh;
width: 68vh;
position:absolut;
position: absolute;
top: 50% !important;
left: 50% !important;
margin: 0;
transform: translate(-50%, -50%) rotateY(360deg);
}
This is a simplified version of my directive.
var app = angular.module('trans', []);
app.directive('card', ['$document', '$timeout', function ($document, $timeout) {
return {
restrict: 'A',
link: link,
scope: {}
};
function link(scope, element, attrs) {
var clone;
element.on('click', function () {
if (clone) {
clone.off().remove();
}
clone = element.clone();
var spec = getCardSpecifications();
clone.css({
'margin': '0',
'top': spec.top + 'px',
'left': spec.left + 'px',
'position': 'absolute'
});
$document.find('body').append(clone);
clone.addClass('goto');
if (attrs.card == 'notimeout') {
clone.addClass('fullscreen');
} else {
$timeout(function () {
clone.addClass('fullscreen');
}, 0);
}
});
function getCardSpecifications() {
var spec = {};
spec.top = element.prop('offsetTop');
spec.left = element.prop('offsetLeft');
spec.height = element[0].offsetHeight;
spec.width = element[0].offsetWidth;
return spec;
}
}
}]);
I've created this jsfiddle that demonstrates the problem.
The problem doesn't have anything to do with Angular itself, but with creating a new DOM node and setting a class on it right after. Such a problem is described e.g. here, and it uses the same solution as yours in the first example.
DISCLAIMER: The real Angular way of doing this would be ngAnimate. What follows is a solution that is almost the same as the OP's, and one you'd only want to use if you don't want to depend on that module – but it's only ~11kb uncompressed, and 4kb gzipped. Choose wisely!
What also worked for me is waiting for the DOM node to be ready:
clone.ready(function() {
clone.addClass('fullscreen');
});
This amounts to almost the same thing as using a 0ms timeout, but is a. more descriptive and b. works in all cases, while the timeout solution apparently sometimes fails in Firefox (see linked article).
The second solution given in the article also reads a little more hackish (matter of opinion, really), and you'll have to retrieve the actual DOM element instead of the jqLite wrapper around it to use it.
Why exactly this happens, even though you are adding the class "after appending", I wasn't able to quickly find out. Perhaps appendChild, which append most likely uses internall, is asynchronous (i.e. pushes the DOM manipulation task onto the event queue)? Some more googling might be useful if you're really interested in the cause of this problem.
You should probably use animate to do an animation
$animate.addClass(clone, 'fullscreen'
I had issues trying to get the dependency for animate in fiddle so
I made a Plunker
When changing the DOM via the directive with methods like css, you have to inform the digest loop of these changes.
In order to accomplish that you should add scope.$apply() after adding the css class inside your if statement. The reason why $timeout works for you is that because it calls $apply after the timeout executes.
i would like to make my navigation menu that is fixed to the top of my page to auto-hide the same way you can make the taskbar in windows hide when you have "auto-hide taskbar" enabled
I would like it to hide and then when you move your mouse close to the top of the screen for it to become visible again and then hide again when you move your mouse away from the top.
What is the best way i can make this happen?
Thanks in advance for your answers!
So many different ways to do this but a very quick think about it ...
You have your HTML nav bar...
<div nav-directive>
<div class="nag" ng-class="{ 'visible': visible }"></div>
</div>
Directive
.directive('navDirective', function() {
return {
restrict: 'EA',
link: function(scope, el) {
scope.visible = false;
el.bind('mouseover', function() {
scope.visible = true;
// You shouldn't do but may need a scope apply here, not sure...
});
el.bind('mouseout', function() {
scope.visible = false;
// again not sure scope apply?
});
}
}
});
This will get you your basic adding and removing the class visible.
Then you can use some CSS3 to get some sliding motion in.
.nav {
top: 0px;
position: absolute;
transition: transform 1s ease-in;
transform: translate(-100%, 0);
}
.nav.visible {
transform: translate(0, 0);
}
So I've been working with AngularJS for a decent amount of time, yet still have to understand the scenes behind directives.
I am trying to build a directive which attaches a modal window on demand and detaches it from the DOM if not needed anymore.
So i did this:
app.directive('myDirective',function($document){
return{
restrict: 'E',
templateUrl: 'partials/modules/template.html',
link: function($scope,$element){
var body = $document.find('body').eq(0);
$element.remove();
$scope.create = function(){
body.append($element);
};
}
}
});
And found that it will cause the loss of linking between view and controller.
So far, so bad.
But what basic concept am i missing here? What would be a proper way to accomplish this?
I've got a few, messy (and hacky) options in my head, including
using ng-show
setting a CSS class of hide manually
Re-linking the stuff back together after append
They seem weird and simply wrong to me, and i especially don't want to use a style attribute to do this.
I also don't want to use Angular-UI's modal module.
you should definitely re-consider your view about using a style attribute. It is the angular recommended way to go:
'One of the major design goals of AngularJS is to allow application developers to build web apps with little or no direct manipulation of the DOM. In many cases this also leads to a much more declarative style of programming. This allows business logic to be easily unit tested and greatly increases the rate at which you can develop applications.' What is the AngularJS way to show or hide a form element?
I do this kind of thing all the time. Using ng-class and json, it is in my view the simplest way of doing it and the easiest way to test. Here's a rough idea. Also you don't need to append the element to the body, that is the whole purpose of the link phase:
app.directive('myDirective',function($document){
return{
restrict: 'E',
templateUrl: '<div myDirective ng-class="{\'hideClass\':object.hide===true, \'showClass\':object.show===true "></div>',
link: function($scope,$element){
scope.element= {hide:false, show:true}
if(someCondition) {
scope.element.hide = true;
}
if (anotherCondition) {
scope.element.show = true;
}
}
}
});
Then, in your test:
it('should be hidden if...', function () {
angular.mock.inject(function ($compile, $rootScope) {
var scope = $rootScope.$new();
var elem = $compile('<div myDirective></div>')(scope);
// ... some conditional code to manipulate scope.element json, you may need timeout to wait for DOM to load so you can check that the class is present
expect(elem.hasClass('hideClass')).toBe(true);
});
});
The problem is when you call $element.remove() and then body.append($element) in the create method, it is no longer "compiled" angular. This is why the linking is broken. Compiling and appending everytime you want to show isn't the most efficient solution (as you mention above).
Why is it considered hacky to use CSS for display and hiding of the element/modal? This is how I've seen it done in most UI frameworks.
I've put together a jsfiddle of what I believe your problem is (button in template can't call hideMe function) and an example using CSS class.
Ignoring the simplicity of the styles:
.modal.show {
display: block;
}
.modal {
display: none;
position: absolute;
height: 100px;
width: 100px;
margin: 15px auto auto;
border: 1px solid blue;
padding: 5px;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: white;
z-index: 200;
}
Use addClass and removeClass within your scope methods for creating/hiding the modal:
mod.directive("myDirective", function () {
return{
restrict: 'E',
replace: true,
template: '<div class="modal">Hello<button ng-click="hideMe()">×</button></div>',
link: function($scope,$element){
var shade = angular.element('<div class="shade"></div>');
$scope.create = function(){
$element.addClass("show");
$element.after(shade);
};
$scope.hideMe = function () {
$element.removeClass("show");
shade.remove();
}
}
}
});
Hiding/showing elements is one of the thing CSS does really well. You can also get some nice animations and transitions if you wanted using CSS with minimal extra work.