- // Force the Template to apply the new animation state before we actually
- In Angular 1.2+, transitions between these two states were facilitated by the “leave” and “enter” animations.
- Meaning, when entering an element, I only had the initial styles defined; and, when existing an element, I only had the final styles defined.
- In Angular 2, the transition into and out of this “void” state can change depending on what state an element is being transitioned from or to, respectively.
- // change the rendered element view-model.
Ben Nadel experiments with conditional Enter and Leave animations in Angular 2 RC 6 in which the visual direction of a transition depends on the user interactions.
@BenNadel: Creating conditional “Enter” and “Leave” animations in #Angular2 based on user interactions.
While Angular 2 now has support for animations, the approach taken in Angular 2 is very different from the approach taken in Angular 1.2+. In Angular 1.2+, the animations are driven by special CSS classes that are parsed at runtime; in Angular 2 (at least as of RC 6), all animations are driven by animation meta-data that is attached to the component. To start wrapping my head around this new animation framework, I wanted to try and recreate my ngRepeat-based animation “hack” in Angular 2 RC 6, using conditional “Enter” and “Leave” animations based on user interaction.
NOTE: The animation framework in Angular 2 uses the Web Animations API which still needs to be polyfilled in most browsers – I have added it to my package.json for RC 6.
In Angular 2, the animation framework is really a state-transition framework. However, when I think about animations for dynamically-rendered elements, there are really only two states that I care about: “existing” and “not existing”. In Angular 1.2+, transitions between these two states were facilitated by the “leave” and “enter” animations. In Angular 2, this becomes a transition into and out of the “void” state:
In Angular 2, the transition into and out of this “void” state can change depending on what state an element is being transitioned from or to, respectively. To experiment with this, I’m going to use the ngFor directive to hack a “cycle” widget that will show the selected-friend in a collection of friends. As the user cycles through this collection, they can navigate to the previous-friend or the next-friend. Depending on which operation is chosen, I want the animating elements to move in a visual direction that aligns properly with the user’s own mental model.
In other words, when the user navigates to the previous-friend, I want the elements on the page to animate right; and, when the user navigates to the next-friend, I want the elements on the page to animate left. This means that the void-related transitions will depend on the state of the animation trigger at the time of the navigation.
In the following demo, there are four transition that an element can have:
The first two are used when the user navigates to the previous-friend, “sliding” the collection to the right. The second two are used when the user navigates to the next-friend, “sliding” the collection to the left. Let’s take a look at the code:
// Import the core angular services.
import { animate } from “@angular/core”;
import { ChangeDetectorRef } from “@angular/core”;
import { Component } from “@angular/core”;
import { style } from “@angular/core”;
import { transition } from “@angular/core”;
import { trigger } from “@angular/core”;
interface Friend {
id: number;
name: string;
favoriteMovie: string;
type Orientation = ( “prev” | “next” | “none” );
@Component({
selector: “my-app”,
animations: [
trigger(
“friendAnimation”,
transition(
“void => prev”, // —> Entering —>
// In order to maintain a zIndex of 2 throughout the ENTIRE
// animation (but not after the animation), we have to define it
// in both the initial and target styles. Unfortunately, this
// means that we ALSO have to define target values for the rest
// of the styles, which we wouldn’t normally have to.
style({
left: -100,
opacity: 0.0,
zIndex: 2
animate(
“200ms ease-in-out”,
style({
left: 0,
opacity: 1.0,
zIndex: 2
transition(
“prev => void”, // —> Leaving —>
animate(
“200ms ease-in-out”,
style({
left: 100,
opacity: 0.0
transition(
“void => next”, // <--- Entering <--- // In order to maintain a zIndex of 2 throughout the ENTIRE // animation (but not after the animation), we have to define it // in both the initial and target styles. Unfortunately, this // means that we ALSO have to define target values for the rest // of the styles, which we wouldn’t normally have to. style({ left: 100, opacity: 0.0, zIndex: 2 animate( “200ms ease-in-out”, style({ left: 0, opacity: 1.0, zIndex: 2 transition( “next => void”, // <--- Leaving <--- animate( “200ms ease-in-out”, style({ left: -100, opacity: 0.0 template:
«
Previous Friend
—
Next Friend
»
public orientation: Orientation;
public selectedFriend: Friend;
private changeDetectorRef: ChangeDetectorRef;
private friends: Friend[];
// I initialize the component.
constructor( changeDetectorRef: ChangeDetectorRef ) {
this.changeDetectorRef = changeDetectorRef;
this.orientation = “none”;
// Setup the friends collection.
this.friends = [
id: 1,
name: “Sarah”,
favoriteMovie: “Happy Gilmore”
id: 2,
name: “Joanna”,
favoriteMovie: “Better Than Chocolate”
id: 3,
name: “Tricia”,
favoriteMovie: “Working Girl”
id: 4,
name: “Kim”,
favoriteMovie: “Terminator 2”
// Randomly(ish) select the initial friend to display.
this.selectedFriend = this.friends[ Math.floor( Math.random() * this.friends.length ) ];
// PUBLIC METHODS.
// I cycle to the next friend in the collection.
public showNextFriend() : void {
// Change the “state” for our animation trigger.
this.orientation = “next”;
// Force the Template to apply the new animation state before we actually
// change the rendered element view-model. If we don’t force a change-detection,
// the new [@orientation] state won’t be applied prior to the “leave” transition;
// which means that we won’t be leaving from the “expected” state.
this.changeDetectorRef.detectChanges();
// Find the currently selected index.
var index = this.friends.indexOf( this.selectedFriend );
// Move the rendered element to the next index – this will cause the current item
// to enter the ( “next” => “void” ) transition and this new item to enter the
// ( “void” => “next” ) transition.
this.selectedFriend = this.friends[ index + 1 ]
? this.friends[ index + 1 ]
: this.friends[ 0 ]
// I cycle to the previous friend in the collection.
public showPrevFriend() : void {
// Change the “state” for our animation trigger.
this.orientation = “prev”;
// Force the Template to apply the new animation state before we actually
// change the rendered element view-model. If we don’t force a change-detection,
// the new [@orientation] state won’t be applied prior to the “leave” transition;
// which means that we won’t be leaving from the “expected” state.
this.changeDetectorRef.detectChanges();
// Find the currently selected index.
var index = this.friends.indexOf( this.selectedFriend );
// Move the rendered element to the previous index – this will cause the current
// item to enter the ( “prev” => “void” ) transition and this new item to enter
// the ( “void” => “prev” ) transition.
? this.friends[ index – 1 ]
: this.friends[ this.friends.length – 1 ]
When I was first coding this demo, I only had half of the [current] styles defined. Meaning, when entering an element, I only had the initial styles defined; and, when existing an element, I only had the final styles defined. Somehow, Angular 2 just knew how to automatically transition between the “transition” styles and the “static” styles.
But, when I added the need for the transitioning element to maintain a zIndex:2 throughout the transition (so that the entering element was always on the “top” of the z-index stack), things got more complicated. Once I added the zIndex, I had to define it in both the initial and the target styles of the transition; otherwise, it would end-up transitioning the value from 2-to-0. Unfortunately, this prevented the other styles – like opacity – from being transitioned automatically; as such, I had to end-up explicitly defining all the initial and target styles. Ultimately, however, I think this explicit styling makes it more clear to the developer.
The other challenge that I faced in this demo was that Angular wouldn’t apply an animation trigger state-change to an element that was being removed from the DOM (Document Object Model). Luckily, I discovered that if I requested a .detectChanges() event on the ChangeDetectorRef after defining a state-change in the view-model, Angular 2 would apply the state change to the template before it removed the DOM element.
All in all, I think I got it working quite nicely. And, when we run this in browser, we can clearly see the elements entering and leaving from the correct orientation based on the user interactions:
The shift from CSS-based animations in Angular 1.2+ to meta-data-based animations in Angular 2 is a rather large one. Definitely a lot to learn, especially when it comes to nested animations. But, at least I was able to figure out how to create conditional Enter and Leave animations based on user interactions.