Skip to main content
Ben Nadel at Endless Sunshine 2017 (Portland, OR) with: Bryan Stanley and Brian Blocker
Ben Nadel at Endless Sunshine 2017 (Portland, OR) with: Bryan Stanley Brian Blocker ( @brianblocker )

Blocking Nested Animations In Angular 2 RC 6

By on

In Angular 1.x, the default behavior for the animations module was to block any nested animations and any animations during the initial rendering of the application. I believe that this was a smart move and demonstrated an holistic view of the application; animations weren't just an elemental consideration but, rather, part of a cohesive understanding about user experience. In Angular 2, this is no longer the default behavior. In my opinion, this is a bug. And, I wanted to see if I could come up with a stop-gap solution to block nested animations until this bug is fixed.

Run this demo in my JavaScript Demos project on GitHub.

First, let's look at the default behavior in Angular 2 RC 6. To do this, I've created a small demo in which we have a view that has both a Container element and a nested Box element. Both of these elements can be toggled on-and-off within the view. But, only the nested Box element has any animation meta-data attached to it:

// Import the core angular services.
import { animate } from "@angular/core";
import { Component } from "@angular/core";
import { state } from "@angular/core";
import { style } from "@angular/core";
import { transition } from "@angular/core";
import { trigger } from "@angular/core";

@Component({
	selector: "my-app",
	animations: [
		// Notice that we are setting up an animation for the Box, but not for the
		// container. In this demo, the container doesn't have any animation; but,
		// it is still added and removed from the view as well (along with its Box
		// descendant element).
		trigger(
			"boxAnimation",
			[
				state(
					"void",
					style({
						borderRadius: 50,
						opacity: 0.0,
						transform: "rotate( 900deg )"
					})
				),
				state(
					"*",
					style({
						borderRadius: 4,
						opacity: 1.0,
						transform: "rotate( 0deg )"
					})
				),
				transition(
					"void => * , * => void",
					[
						animate( "1000ms ease-in-out" )
					]
				)
			] // End: boxAnimation.
		)
	],
	template:
	`
		<p>
			<a (click)="toggleContainer()">Toggle Container</a>
			&mdash;
			<a (click)="toggleBox()">Toggle Box</a>
		</p>

		<div *ngIf="isShowingContainer" class="container">

			<div *ngIf="isShowingBox" @boxAnimation class="box">
				Box
			</div>

		</div>
	`
})
export class AppComponent {

	public isShowingBox: boolean;
	public isShowingContainer: boolean;


	// I initialize the component.
	constructor() {

		this.isShowingBox = true;
		this.isShowingContainer = true;

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I add or remove the box from the view, depending on the current rendering.
	public toggleBox() : void {

		this.isShowingBox = ! this.isShowingBox;

	}


	// I add or remove the container from the view, depending on the current rendering.
	public toggleContainer() : void {

		this.isShowingContainer = ! this.isShowingContainer;

	}

}

If you run this version of the demo, what you'll see is that the Box element animates at several unintended times in the life-cycle of the application:

  • On initial rendering of the demo.
  • Any time the Container is toggled into view.

Again, this is just my opinion, but I think this is a bug; I don't believe that this is the user experience (UX) that the developer intending to create. Ideally, the Box should only animate during view-model changes that take place after the Container has been rendering. Meaning, whenever the Container is toggled into view, the Box should render instantly.

To try and stop-gap this behavior, I'm going to use animation state and explicit change-detection. As I discovered the other day, we can use the ChangeDetectorRef and explicit change-detection to gain finer control over when and how animation state is applied to the view. In this demo, we're going to use this functionality to put the nested Box element into a "blocked" state while the container is rendering.

Essentially, we're going to take the following steps:

  • Toggle view-model for Container (to show container).
  • Put Box element animation into a "blocked" state.
  • Manually trigger change-detection to render Container in view.
  • Put Box element animation into an "active" state (for subsequent transitions).

This is a messy approach; but, at least we don't have to deal with setTimeout() in order to create cascading view-model changes. The ChangeDetectorRef's .detectChanges() method is synchronous, so this entire transaction should be completely seamless.

In the following code, notice that our animation meta-data no longer deals with the catch-all, non-void "*" state. Instead, we are explicitly transitioning to and from the "active" state. This way, the Box will never animate when it's in the "blocked" state.

// Import the core angular services.
import { animate } from "@angular/core";
import { ChangeDetectorRef } from "@angular/core";
import { Component } from "@angular/core";
import { state } from "@angular/core";
import { style } from "@angular/core";
import { transition } from "@angular/core";
import { trigger } from "@angular/core";

@Component({
	selector: "my-app",
	animations: [
		// Notice that we are setting up an animation for the Box, but not for the
		// container. In this demo, the container doesn't have any animation; but,
		// it is still added and removed from the view as well (along with its Box
		// descendant element).
		trigger(
			"boxAnimation",
			[
				state(
					"void",
					style({
						borderRadius: 50,
						opacity: 0.0,
						transform: "rotate( 900deg )"
					})
				),

				// Notice that we are explicitly using an "active" state rather than
				// the catch-all, non-void "*" state from the previous demo.
				state(
					"active",
					style({
						borderRadius: 4,
						opacity: 1.0,
						transform: "rotate( 0deg )"
					})
				),

				// Notice that we are only animating transitions to and from the "active"
				// state - we are no longer using the "*" as an animated state.
				transition(
					"void => active , active => void",
					[
						animate( "1000ms ease-in-out" )
					]
				)
			] // End: boxAnimation.
		)
	],
	template:
	`
		<p>
			<a (click)="toggleContainer()">Toggle Container</a>
			&mdash;
			<a (click)="toggleBox()">Toggle Box</a>
		</p>

		<div *ngIf="isShowingContainer" class="container">

			<div *ngIf="isShowingBox" [@boxAnimation]="boxAnimationState" class="box">
				Box
			</div>

		</div>
	`
})
export class AppComponent {

	public boxAnimationState: string;
	public isShowingBox: boolean;
	public isShowingContainer: boolean;

	private changeDetectionRef: ChangeDetectorRef;


	// I initialize the component.
	constructor( changeDetectionRef: ChangeDetectorRef ) {

		this.changeDetectionRef = changeDetectionRef;
		this.isShowingBox = true;
		this.isShowingContainer = true;

		// In this version of the demo, we are going to take steps to counteract the
		// BUGGY BEHAVIOR with initial and nested animations. To prevent the initial
		// rendering from animation, we'll put the animation state in a "blocked" state.
		// Then, we'll configure our animation meta-data to use an "active" state rather
		// than the catch-all non-void "*" state (see component meta-data above).
		this.boxAnimationState = "blocked";

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I add or remove the box from the view, depending on the current rendering.
	public toggleBox() : void {

		this.isShowingBox = ! this.isShowingBox;

	}


	// I add or remove the container from the view, depending on the current rendering.
	public toggleContainer() : void {

		this.isShowingContainer = ! this.isShowingContainer;

		// If we are showing the Container then it means we are implicitly showing the
		// Box as well. As such, we have to take some special steps to make sure we don't
		// get the BUGGY nested animations.
		if ( this.isShowingContainer ) {

			// First, put the Box into a "blocked" state. This will prevent it from
			// animating since none of the animation meta-data deals with this state.
			this.boxAnimationState = "blocked";

			// Now, we tell Angular to run the change-detection. Since the
			// "isShowingContainer" view-model has changed, this will cause the Container
			// to be rendered. The box will also be rendered; however, since it is in a
			// "blocked" state, it will NOT BE animated.
			this.changeDetectionRef.detectChanges();

			// Once the Container and the box have been rendered, we can put the box
			// into the active state so that it can be animated on subsequent view-
			// model changes.
			this.boxAnimationState = "active";

		}

	}

}

By putting the Box into a "blocked" state while the Container is rendering, we put it into a state that has no associated transitions. As such, when the Container renders, the Box will render instantly along with it. Then, by putting the Box back into an "active" state when the rendering is complete, we set the Box up for subsequent transitions.

This approach definitely requires a lot more footwork. But, at least there is a viable stop-gap solution to block nested animations in Angular 2 RC 6. I truly believe this delivers a better user experience (UX) - though, of course, this is just my opinion.

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

Reader Comments

15,674 Comments

@All,

It looks like this "bug" has been fixed in the Animation Module revamp in Angular 4.2. Now, nested animations block by default. However, dynamic elements are not "animated" by default; so, an ngIf won't inherently block a contained animation -- you have to add an animation to the ngIf in order to get it to block. I'll work on a demo for that.

15,674 Comments

@All,

A quick follow-up to my previous comment:

www.bennadel.com/blog/3417-using-no-op-transitions-to-prevent-animation-during-the-initial-render-of-ngfor-in-angular-5-2-6.htm

This demonstrates how to use a No-Op transition on a parent-element in order to block the initial rendering of ngFor item elements. This approach is basically the same that approach that we took in AngularJS 1.2:

www.bennadel.com/blog/2765-preventing-animation-during-the-initial-render-of-ngrepeat-in-angularjs.htm

I'm sure there are other ways to accomplish, such as with more intricate "state management"; however, this seems to be the lowest-effort approach.

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