Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber and Gunnar Lieb and Thilo Hermann
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Andy Weber , Gunnar Lieb@akitogo ) , and Thilo Hermann ( @thfusion_de )

Blocking Nested Animations In Angular 2 RC 6

By Ben Nadel 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.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.