Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Gavin Pickin
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Gavin Pickin@gpickin )

Using No-Op Transitions To Prevent Animation During The Initial Render Of ngFor In Angular 5.2.6

By Ben Nadel on

When Angular 2 first came out, the Animations module didn't block nested transitions (like it did in AngularJS 1.2). So, we had to jump through a bunch of hoops to try and have animation "state" drive the blocking. As of Angular 4.2, however, the revamped Animations module will block nested animations by default. We can now leverage this new behavior to block the initial rendering of ngFor item-animations by adding a no-op (No Operation) transition to one of the parent elements. Just like we did with ngRepeat in Angular 1.2.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Animating new items onto the screen can lead to a very pleasing, very organic user experience (UX). However, when it comes to animating lists, we almost never want to animate the initial rendering of the list - only the new items that are added after the list has been mounted in the DOM (Document Object Model). To block the initial rendering of the ngFor items, we can animate the parent element during the same state-transition; this will naturally block the nested animations. This parent animation can be meaningful; or, it can be a no-op transition:

transition( ":enter", [] )

This no-op transition has no duration and defines no style changes. But, it is sufficient to flag the associated element as being "in transition"; which, in turn, is enough to block the ":enter" animations of its descendants.

To see this in action, I've created a small demo in which I have two lists of friends. In each list, new friend LI elements are transitioned onto the screen. However, one of the lists adds a no-op transition on the parent UL element in order to block the initial render animation of the nested LIs:

  • // Import the core angular services.
  • import { animate } from "@angular/animations";
  • import { Component } from "@angular/core";
  • import { style } from "@angular/animations";
  • import { transition } from "@angular/animations";
  • import { trigger } from "@angular/animations";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • animations: [
  • trigger(
  • "friend",
  • [
  • transition(
  • ":enter",
  • [
  • style({
  • opacity: 0,
  • transform: "translateX( 300px )"
  • }),
  • animate(
  • 1000,
  • style({
  • opacity: 1,
  • transform: "translateX( 0px )"
  • })
  • )
  • ]
  • )
  • ]
  • ),
  • // By default, the "parent" animation in a nested-animation context will take
  • // precedence, blocking the child animation. As such, if we want to block a
  • // given animation, we simply have to create a no-op (No Operation) transition
  • // above it in the DOM (Document Object Model). In this case, by defining a no-op
  • // ":enter" transition on the UL element, we block the INITIAL ":enter"
  • // transitions of the LI elements contained within it. But, ONLY WHEN THE UL IS
  • // TRANSITION (ie, being ":enter"d). Once the parent UL is mounted, subsequent
  • // LI ":enter" animations are allowed to run.
  • trigger(
  • "blockInitialRenderAnimation",
  • [
  • transition( ":enter", [] )
  • ]
  • )
  • ],
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <form (submit)="$event.preventDefault(); processForm()">
  • <input #input type="text" [value]="form.name" (input)="form.name = input.value" />
  • <button type="submit">Add Friend</button>
  • </form>
  •  
  • <p>
  • <a (click)="toggleLists()">Toggle Lists of Friends</a>
  • </p>
  •  
  • <div *ngIf="isShowingLists">
  •  
  • <h2>
  • With Initial Animation Blocking
  • </h2>
  •  
  • <ul @blockInitialRenderAnimation>
  • <li *ngFor="let friend of friends" @friend>
  • {{ friend }}
  • </li>
  • </ul>
  •  
  • </div>
  •  
  • <div *ngIf="isShowingLists">
  •  
  • <h2>
  • Without Initial Animation Blocking
  • </h2>
  •  
  • <ul>
  • <li *ngFor="let friend of friends" @friend>
  • {{ friend }}
  • </li>
  • </ul>
  •  
  • </div>
  • `
  • })
  • export class AppComponent {
  •  
  • public form: {
  • name: string;
  • };
  • public friends: string[];
  • public isShowingLists: boolean;
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • this.form = {
  • name: ""
  • };
  • this.friends = [ "Sarah", "Kim", "Tricia" ];
  • this.isShowingLists = true;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I process the new friend form.
  • public processForm() : void {
  •  
  • if ( this.form.name ) {
  •  
  • this.friends.push( this.form.name );
  • this.form.name = "";
  •  
  • }
  •  
  • }
  •  
  •  
  • // I toggle the visibility of the friend lists.
  • public toggleLists() : void {
  •  
  • this.isShowingLists = ! this.isShowingLists;
  •  
  • }
  •  
  • }

As you can see, one of the UL elements has the @blockInitialRenderAnimation animation trigger and the other does not. And, when we load this page for the first time, we can see that only the LI elements in the second list demonstrate ":enter" transitions:


 
 
 

 
 Blocking the initial rendering of ngFor item animations in Angular 5.2.6. 
 
 
 

As you can see, the LI elements in the first list render instantly during first-load whereas the LI elements in the second list transition onto the screen during first-load. The first list provides a much nicer user experience. And, it doesn't prevent the subsequent friends from transition into place. Once the lists are rendered, if we add a new friend, we get the following output:


 
 
 

 
 Blocking the initial rendering of ngFor item animations in Angular 5.2.6, while still alowing subsequent animations. 
 
 
 

As you can see, both lists are still capable of animating the ":enter" transition on LI elements once the parent UL has been fully rendered on the page.

Animations and transitions are powerful tools in the user experience (UX) toolbox. But, we have to be careful not to use them at the wrong time or in the wrong place or we run the risk of providing an even worse experience for our users. Thankfully, in Angular 5.2.6, we can easily leverage the automatic blocking of nested transitions in order to block the initial rendering of an ngFor loop while still allowing for new ngFor loop items to be transitioned into place.




Reader Comments

Ben. Another great tutorial.

Three questions.

Firstly. So, because, 'UL' is a parent in the DOM to 'LI', it takes precedence in the animation order?

Secondly. Does the order of the trigger blocks make a difference? Would the same result have been achieved, if you had written the 'friend' trigger block second?

And, finally, how would you go about animating the entire list after adding a friend, but without it animating after the initial page load?

Reply to this Comment

@Charles,

Yes, because the UL is a parent, it takes precedence on the animation, blocking nested animations by default. You can, however, choose to allow any child / nested animations to run by using the "animateChild()" method (not shown in this demo) within the transition definition.

As far as the order of the trigger() blocks, I don't believe the order matters. However, I *do believe* the order of the transition() blocks within a trigger() matters. I think that Angular will use the first transition() block that matches the state-change, and then stop looking for transition definitions.

Regarding animating the entire list after a friend is added, I am not sure that I have a good use-case for that in mind. Perhaps you could use a specific state-transition on the parent to block the initial load, and then a different state to allow subsequent changes to cause animations. I'd have to noodle on that some more.

Reply to this Comment

Thanks Ben, for the explanation. Makes sense. I am exploring the Angular [Google] Material module at the moment, which has some cool built in animation effects, although I am just weighing up whether I would actually create an entire UI, using this module. Looks a little too clinical for my liking, although it does implement an extremely modular approach to UI building.

https://material.angular.io/components/icon/overview

Reply to this Comment

@Charles,

Google Material is one of those things I want to look into "eventually." I'm particularly interested in how they approach "themeing", as this topic completely eludes me. I love the simulated-encapsulation of the styled components. But, I have zero understanding of how that can be made to be flexible from a cross-cutting concerns kind of way. I'd be very curious to see how the Material Design library handles it.

One day .... :D

Reply to this Comment

Well, I have only use a couple of Material elements like snackbar. Although, I couldn't get the tooltip to work, because my CLI is not up to date enough. I did watch a tutorial on Material, and it looks like you can create UI quite quickly in Angular 6. Plus, the new Tree element looks very cool. It allows you to create UI directory trees!

The question for me, is do I want my UI to look like Google. I am in 2 minds on this one. Material has some lovely elements but I sometimes think that Google UI looks too clinical & impersonal, so I may just add elements here & there, rather than create an entire Material driven UI! But, I guess this is all very subjective.

But rapid development is definitely one of Material's plus points.

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.