Using ChangeDetection With Animation To Setup Dynamic Void Transitions In Angular 2 RC 6

Yesterday, I looked at creating conditional Enter and Leave animations in Angular 2 RC 6. As part of that exploration, I stumbled over the problem of dynamic "leave" states; or rather, starting a "void transition" from a dynamically selected state. To solve that problem, I used explicit change-detection in my component. Since this is a non-obvious approach, I thought it would be worth recapping just that concept in its own follow-up post.

Run this demo in my JavaScript Demos project on GitHub.

Sometimes, in a given user experience (UX), you need to remove an element from the Document Object Model (DOM) based on a user's action. In my previous post, this was necessary to make a carousel widget "cycle" in the appropriate horizontal direction. The problem with this requirement is that you have to set two different view-model values at the same time:

Set animation state (from which you are leaving).

Remove the datum from the view rendering.

By default, the latter change prevents the first change from taking place. In other words, when Angular 2 sees that the given data's corresponding DOM element is being destroyed and removed from the view rendering, it won't bother updating the animation state. To fix this problem, we have to run an explicit change-detection in between the two view-model changes:

Step 1 : Change the animation state.

: Change the animation state. Step 2 : Run explicit change-detection (which applies the new animation state).

: Run explicit change-detection (which applies the new animation state). Step 3: Remove the datum from the view rendering.

To see this in action, I've created a small demo that removes a Box element from the view. In all cases, a dynamic animation state is being set as part of the removal process; but, change-detection is only being run in the last 2 removal calls:

// 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"; @Component({ selector: "my-app", animations: [ trigger( "boxAnimation", [ // In this collection of transitions, the initiate state of the animation // is determined by the boxState expression that is being driven by the // user interaction. transition( "withOpacity => void", [ style({ opacity: 1.0 }), animate( "1000ms ease-in", style({ opacity: 0.0 }) ) ] ), transition( "withRotation => void", [ style({ opacity: 1.0, transform: "rotate( 0deg )" }), animate( "1000ms ease-in", style({ opacity: 0.0, transform: "rotate( 1000deg )" }) ) ] ) ] ) ], template: ` <ul> <li> <a (click)="removeBox( 'withOpacity' )">Remove w/ Opacity</a> </li> <li> <a (click)="removeBox( 'withOpacity', true )">Remove w/ Opacity + ChangeDetection</a> </li> <li> <a (click)="removeBox( 'withRotation', true )">Remove w/ Rotation + ChangeDetection</a> </li> </ul> <div class="container"> <template [ngIf]="isShowingBox"> <div [@boxAnimation]="boxState" class="box"> Box </div> </template> </div> ` }) export class AppComponent { public boxState: string; public isShowingBox: boolean; private changeDetectorRef: ChangeDetectorRef; // I initialize the component. constructor( changeDetectorRef: ChangeDetectorRef ) { this.changeDetectorRef = changeDetectorRef; this.boxState = "none"; this.isShowingBox = true; } // --- // PUBLIC METHODS. // --- // I remove the box by first putting the box animation into the given state and then // updating the flag that removes the box from the view. The `runChangeDetection` // argument determines whether or not a change-detection is run in between these // two steps. public removeBox( fromState: string, runChangeDetection: boolean = false ) : void { console.group( "removeBox()" ); console.log( "Setting state to:", fromState ); // STEP 1: Set animation state. // --- // Set the state that will determine which animation transition will take place // when the box is removed from the view ( boxState => void ). this.boxState = fromState; // STEP 2: Run change detection. // --- // Run change-detection if requested. Doing this will apply the boxState change // BEFORE we try to remove the box from the view. if ( runChangeDetection ) { console.log( "Running change-detection." ); this.changeDetectorRef.detectChanges(); } // STEP 3: Remove box. // --- // Remove the box from the view. this.isShowingBox = false; console.groupEnd(); // In a few seconds, reset the demo. setTimeout( () => { this.isShowingBox = true; this.boxState = "none"; }, ( 2 * 1000 ) ); } }

As you can see, the box element is bound to the @boxAnimation trigger. This trigger has two different leave transitions configured:

withOpacity => void

withRotation => void

By default, the boxAnimation state is "none" - it only gets set to "withOpacity" or "withRotation" based on the user's action as part of the removal process. If we run this code and use the first removal option, which doesn't run change-detection, we get no transition. That's because Angular never changes the animation state, leaving us with a "none => void" transition, which is not configured in our animation meta-data.

If, however, we choose one of the latter options which does use change-detection, we can clearly see that an animation transition is taking place:

This meta-data based animation stuff is totally new to me; so, I am not quite sure how I feel about it yet. Clearly, there's going to be a large learning curve; and, we have to keep in mind that Angular 2 is still evolving as well. The Angular 2 docs say that animations will eventually be CSS driven [optionally]; part of me hopes that they bring back the "ng-enter" and "ng-leave" CSS classes - I really liked that animation configurations in Angular 1.x were external to the component itself.

Tweet This Fascinating post by @BenNadel - Using ChangeDetection With Animation To Setup Dynamic Void Transitions In Angular 2 RC 6 Woot woot — you rock the party that rocks the body!







