Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Amsterdam) with: Tom de Manincor
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Amsterdam) with: Tom de Manincor@tomdeman )

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

By Ben Nadel on

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.
  • Step 2: 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:


 
 
 

 
 Using change-detection to setup dynamic void transitions in Angular 2 RC 6. 
 
 
 

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.




Reader Comments

Thank you for this!

I find forcing a detectChanges() more like a workaround than an actual solution of the problem going on. I opened up an issue in the angular repo about this though it's a bit more related to the animation callbacks. It's been apparently accepted as a bug and added to the v2.0.1 milestone. https://github.com/angular/angular/issues/11712

That said, this little hack is still very useful and saved me tons of hours. Thank you!

Reply to this Comment

@Carlos,

Thank you for the kind words! Glad that I could help; and, glad that the team is viewing some of this kind of stuff as buggy and not just as a fact of life.

The animation stuff is really interesting in Angular 2. I get the sense that it is very powerful; but, at the same time, I feel like the vast majority of my use-cases will be probably be Void <===> Non-Void transitions. That said, as the power becomes more obvious and the functionality becomes more polished, I look forward to seeing what people do with it.

Reply to this Comment

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.