Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Page transition animations with Angular 2.0 router and component interface promises

In Angular 1.x we can use ngAnimate to detect when we are leaving or entering a particular route. Furthermore we are able to apply behaviors to them:

animateApp.animation('.myElement', function(){

    return {

        enter : function(element, done) {
            //Do something on enter
        },

        leave : function(element, done) {
            //Do something on leave
        }
    };

)};

Resulting in a product like this: http://embed.plnkr.co/uW4v9T/preview

I would like to do something similar with Angular 2.0 and I feel like I'm pretty close...

So here goes, I've created a simple router in the main application component that controls the navigation between the home and about components.

import { bootstrap, bind, Component, provide, View } from 'angular2/angular2';
import {RouteConfig, RouteParams, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, APP_BASE_HREF, ROUTER_BINDINGS} from 'angular2/router'




/////////////////////////////////////////////////////////////////
// Home Component Start
/////////////////////////////////////////////////////////////////
@Component({
  selector: 'home-cmp'
})

@View({
  template: `
    <h2 class="title">Home Page</h2>
  `
})

class HomeCmp implements OnActivate, onDeactivate{

  onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
    console.log("Home Page - initialized");
  }

  onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
    console.log("Home Page - destroyed");
  }

}
/////////////////////////////////////////////////////////////////
// Home Component End
/////////////////////////////////////////////////////////////////




/////////////////////////////////////////////////////////////////
// About Component Start
/////////////////////////////////////////////////////////////////
@Component({
  selector: 'about-cmp'
})

@View({
  template: `
    <h2 class="title">About Page</h2>
  `
})

class AboutCmp implements OnActivate, onDeactivate {

  onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
    console.log("About Page - initialized");
  }

  onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
    console.log("About Page - destroyed");
  }

}
/////////////////////////////////////////////////////////////////
// About Component End
/////////////////////////////////////////////////////////////////




/////////////////////////////////////////////////////////////////
// Main Application Componenent Start
/////////////////////////////////////////////////////////////////
@Component({
  selector: 'my-app'
})

@View({
  template: `
    <div>
      <h1>Hello {{message}}!</h1>
      <a [router-link]="['./HomeCmp']">home</a>
      <a [router-link]="['./AboutCmp']">about</a>
      <hr>
      <router-outlet></router-outlet>
    </div>
  `,
  directives: [ROUTER_DIRECTIVES]
})

@RouteConfig([
  {path: '/', component: HomeCmp, as: 'HomeCmp'},
  {path: '/about', component: AboutCmp, as: 'AboutCmp'}
])

export class App {
}
/////////////////////////////////////////////////////////////////
// Main Application Componenent End
/////////////////////////////////////////////////////////////////




bootstrap(App, [
  ROUTER_BINDINGS,
  ROUTER_PROVIDERS,
  ROUTER_DIRECTIVES,
  provide(APP_BASE_HREF, {useValue: '/'})
])

At the moment I am able to capture when the router has instantiated or destroyed a particular component when it moves from one to the next. This is great, but when the the previous component is destroyed I am not able to apply an on leave transition animation before the next component is initialized.

class HomeCmp implements OnActivate, onDeactivate{

    onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
        //This works
        TweenMax.fromTo($(".title"), 1, {opacity: 0}, {opacity: 1});
    }

    onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
        //This get ignored
        TweenMax.fromTo($(".title"), 1, {opacity: 0}, {opacity: 1});
    }

}

It seems like there is a solution to this using promises. Angular.io's API preview they state:

If onDeactivate returns a promise, the route change will wait until the promise settles.

and

If onActivate returns a promise, the route change will wait until the promise settles to instantiate and activate child components.

https://angular.io/docs/ts/latest/api/

I am super brand new to promises so I mashed this together into my code which solved the problem of my current component being destroyed on initialization of the next one, but then it never gets destroyed, it only creates a new instance of it. Every time I navigate back to it, it will create a new instance resulting in multiple copies.

onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {

    function ani(){
      TweenMax.fromTo($(".title"), 1, {opacity: 1}, {opacity: 0});
    }

    var aniPromise = ani();

    aniPromise.then(function (ani) {
        ani();
    });

}

So to recap, the router should be able to wait for the current component to finish it's business before destroying it and initializing the next component.

I hope that all makes sense and I really appreciate the help!

like image 955
SimonHawesome Avatar asked Nov 05 '15 20:11

SimonHawesome


2 Answers

As you quoted from the docs, if any of this hooks returns a Promise it will wait until its completed to move to the next one, so you can easily return a Promise that basically does nothing and wait a second (or as many time as you need).

 onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
    TweenMax.fromTo($(".title"), 1, {opacity: 0}, {opacity: 1});
    return new Promise((res, rej) => setTimeout(() => res(1), 1000));
  }

  onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
    TweenMax.fromTo($(".title"), 1, {opacity:1}, {opacity: 0});
    return new Promise((res, rej) => setTimeout(() => res(1), 1000));
  }

Note that I'm returning a Promise which runs a setTimeout. We wait a second to give the animation time enough to be completed.

I don't really like using setTimeouts, so we can use Observables as well, that personally I like the best.

return Rx.Observable.of(true).delay(1000).toPromise();

Here I'm passing a random value (true in this case) and delay it a second and finally cast it to Promise. Yes, it ends up being a Promise but I don't use it directly.

Here's a plnkr with an example working (expecting to be what you are looking for).

PS: If sometimes it complains about that it can't find a path to Rx, just keep refreshing until it works (I added Rx.js manually and it's a little heavy for plnkr apprently).

like image 198
Eric Martinez Avatar answered Nov 16 '22 03:11

Eric Martinez


Angular 2 final solution:

plunk

In a nutshell, we can to use the @routeAnimation built-in directive to achieve this. Each of our components representing a child route will be decorated with something like:

@Component({
  selector: 'app-pageone'
  host: { '[@routeAnimation]': 'true' },
  styles: [':host { width: 300px; display: block; position: absolute; }']
  animations: [
    trigger('routeAnimation', [
      state('*', style({transform: 'translateX(0)', opacity: 1})),
      transition('void => *', [
        style({transform: 'translateX(-100%)', opacity: 0}),
        animate('0.5s cubic-bezier(0.215, 0.610, 0.355, 1.000)')
      ]),
      transition('* => void',
        animate('0.5s cubic-bezier(0.215, 0.610, 0.355, 1.000)', style({
          transform: 'translateX(100%)',
          opacity: 0
        }))
      )
    ])
  ]
})
like image 9
Stephen Paul Avatar answered Nov 16 '22 02:11

Stephen Paul