Skip to main content
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Andy Matthews and Steve Withington and Rob Huddleston and Dee Sadler
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Andy Matthews Steve Withington Rob Huddleston Dee Sadler

Experimenting With Conditional Enter-Leave Animations In Angular 2 RC 6

By
Published in Comments (20)

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.

Run this demo in my JavaScript Demos project on GitHub.

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:

  • Enter: "void" => "*"
  • Leave: "*" => "void"

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:

  • void => prev - an element enters left-to-right.
  • prev => void - an element leaves left-to-right.
  • void => next - an element enteres right-to-left.
  • next => void - an element leaves right-to-left.

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:
	`
		<div class="container">
			<template ngFor let-friend [ngForOf]="[ selectedFriend ]">

				<div [@friendAnimation]="orientation" class="friend">

					<div class="name">
						{{ friend.name }}
					</div>
					<div class="avatar"></div>
					<div class="meta">
						ID: {{ friend.id }}
						&mdash;
						Favorite Movie: {{ friend.favoriteMovie }}
					</div>

				</div>

			</template>
		</div>

		<p class="controls">
			&laquo;
			<a (click)="showPrevFriend()">Previous Friend</a>
			&mdash;
			<a (click)="showNextFriend()">Next Friend</a>
			&raquo;
		</p>
	`
})
export class AppComponent {

	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.selectedFriend = this.friends[ index - 1 ]
			? 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:

Conditional enter and leave transition animations based on user interactions in Angular 2 RC 6.

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.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Hi bro,
I wrote an article explaining animations in Angular 2 on my site; and I kind of felt that I couldn't explain this phenomenon well to my readers. Thanks for enlightening me about this and how to handle this using change detection. Great article. (And great video too..!!)

1 Comments

Hi Ben,

I've followed the article and did implementation according to it. I'm getting next item and previous item, but animation is not working somehow. Tried many changes, with & without state, using just linear instead of ease-in-out, but none of animation is working.

Any clue what I might be missing... I'm using chrome browser.

1 Comments

Hello, Ben
First of all congrats for your blog, I always end somehow landing around here, hehe...
My question:
I am trying to use animations for enter-leave (slide-in, slide-out) routing components, I mean: the user gets there using the router and the particular component will be loaded inside that <outlet> thing-o...
For whatever reason it animates when loading, but it doesn't when leaving the component to another route.
Do you know if that is even possible to achieve?
Thank you!

27 Comments

That actually would make for a great post, how to make the router simply fade in and fade out outlets as a default behavior...

15,848 Comments

@Paresh,

Hmm, if you're using Chrome, then the animation API should be supported natively - my first thought was that you weren't using the polyfill, but you shouldn't need it.

Perhaps you are missing CSS information about the positioning of the cards. You'll only get the animation if the "left" CSS actually applies; which will only apply if the card is positioned absolutely (in this case):

https://github.com/bennadel/JavaScript-Demos/blob/master/demos/directional-animation-angular2/demo.css

Hopefully that is why. Otherwise, I am not sure.

15,848 Comments

@Marcos, @Sean,

That's a great idea for a post. I spent about 6 weeks digging into the router (both the Component Router and the now-defunk ngRx Router); but that was before I started looking at any animation stuff. I'll definitely do a follow-up post on that. This is something I've been asked to do (in general) for work as well (though we're still on ng 1.2 at work :D ).

15,848 Comments

@Sean,

It's funny, I go back and forth on using the <template> tag instead of using the *-sugar. Sometimes, I find that using the Template tag really helps to separate out the concerns of what is going on. And, also puts fewer attributes on any single element.

One of the places where I've really started to use it is with the *ngIf, especially when there are two block for True/False conditions:

<template [ngIf]="someExpression">
. . . <div>
. . . . . . Some dynamic content for TRUE.
. . . </div>
</template>

<template [ngIf]="! someExpression">
. . . <div>
. . . . . . Some dynamic content for FALSE.
. . . </div>
</template>

This way, as the inner Div elements get more "stuff" on them, like Class and [style] attributes, for example, the ngIf is clearly pulled out.

This is just an evolving personal preference - not saying it is right or wrong. But, just something that I'm starting to do more often.

15,848 Comments

Man, I really need to implement some sort of back-tick code-block functionality :| Actually some sort of light markdown functionality is really just ... mandatory these days.

1 Comments

Gaahhh, forcing the "detectChanges" was exactly what I needed, gladly you figured that out somehow and shared with us, thank you for that! Keep it coming please :)

2 Comments

Hey Ben many thanks for the article, fantasitic read!!

I was just wondering if in Angular 2.1.0 is it possible to detect (via callback) when a transition becomes into and out of the "void" state:?

15,848 Comments

@Bromel,

That's a great question. I know that you can listen for end-of-transition events now; but, I haven't actually experimented with it yet. I think that would be helpful for complex, serialized animations (ie, wait for this thing to hide, then show this thing, then when done, show this other thing). But, I haven't had much use-case for that kind of stuff. But, it is something that I'd like to play with.

2 Comments

Hi ben
Yes that is exactly what I had in mind. I tend to use other javascript libraries for doing animation sequences e.g (Greensock Application Platform) which has a lot more scope to do the complex and serialised animations. So if it was possible to detect into and out of the void state it would allow you to decouple from the web animation api and use whatever library/platform of you choosing. Don't get me wrong I appreciate what the Angular team have done so far by incorporating an emerging standard but UI/UX animations do tend to have complexity about them and I can see that creating complex animations in Angular today would incorporate a lot of code. But if you get chance to have a play them give me a shout and i'll show the power of GSAP.

regards ;)

1 Comments

Hi ben,

Thank you ver much for this post. The "changeDetectorRef" solution for apply changes on stages saved me a lot of waste time. I think this is not very well explain in the docs. Where do you find it?

1 Comments

Hi Ben, loving the creative ideas! I'm still learning Angular but I've been trying to get something similar for my route component//page animations.

Say I have a five 'page' app, with each 'page' stacked up so that it's kind of a set of five vertical slides (e.g. Home // About // Contact // Gallery // FAQ). I'm passing states into the transition, and clicking the link to About from Home animates Home out to the top and About in from the bottom (via a state "scrollUp"). However, with the current state already "scrollUp", when I then click Contact from About for the same transition again it doesn't animate as the 'current' and 'target' state are the same ("scrollUp")..

Do you know of a way to force the current state to be void so that 'page' transitions can be repeated? Cheers man, really appreciate it! Let me know if the above doesn't make sense - might not have articulated it the best!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel