loading...
Cover image for All you need to know about Angular animations

All you need to know about Angular animations

mustapha profile image Mustapha Aouas Updated on ãƒģ9 min read

Animation is a design tool we can use to lend physicality and tangibility to our abstract digital creations by making them feel more familiar and friendly. A lot of components library take full advantage of animations to make their components more relatable to audiences. The good news is, as an Angular developer, we can have great components almost out of the box. The bad news is, that we almost solely rely on those libraries to do the animations. To the point where the Angular animation API is one of the most, if not the most underused Angular API.

To change that, I'll try my best, in this post, to show you all you need to know about Angular animations to get you started. To do that, we will build together this awesome component:

demo

As they say, practice makes perfect, so if you feel like it, you can follow along by cloning this repository and get rolling (each section of this post represents a git branch of the repository).

Note -> If you are not interested in following along, you could jump right to the section 3, where you will find about the superpowers Angular animation API offers you.

1. Creating the main component đŸ“Ļ

Let's do this. After making sure you have Node, NPM, and the Angular CLI installed on your machine, create a new project and call it "awesomeContacts" or any other clever name you have in mind, then create a "list" component:

ng new awesomeContacts --minimal=true --style=scss --routing=false --skip-install
cd awesomeContacts && yarn # or npm i
# wait for the install to finish, then create the 'list' component
ng g c components/list --spec=false && ng serve -o

Once this is done, remove everything in the AppComponent's template and replaced with our ListComponent (<app-list></app-list>).

One more thing before we jump right to the next section, we have one last thing to do. For the animations to work we need to import the BrowserAnimationModule in our AppModule imports. So you should have something like this:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
//...

@NgModule({
  declarations: [AppComponent, ListComponent],
  imports: [BrowserModule, BrowserAnimationsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

2. Implementing the list component (without animations for the moment)

Now that we have our application and our list component up and running, let's implement the component quickly.
Since this post is about animation we won't focus much on this implementation.

First, we create an interface of a Contact object, then we create a set of contacts to play with:

interface Contact {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
}

const CONTACTS_MOCK: Contact[] = new Array(5)
  .fill({})
  .map(
    (c: Contact, i: number) =>
      ({
        id: i,
        name: `Contact ${i}`,
        email: `email${i}@provider.com`,
        avatarUrl: `https://api.adorable.io/avatars/100/${~~(Math.random() * 100)}`,
      } as Contact),
  )
  .reverse(); // * to have them sorted in DESC order

ℹī¸ adorable io is a great public API that serves placeholder avatars.
ℹī¸ ~~ (two Bitwise NOT operators) is equivalent to Math.floor :)

Once that's done, the implementation for list.component.ts should look something like this:

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
})
export class ListComponent {
  contactList: Contact[] = CONTACTS_MOCK;
  selectedContact: Contact;

  onSelectItem(contact: Contact) {
    // * selecting a contact to focus on
    this.selectedContact = contact ? { ...contact } : null;
  }

  onAddItem() {
    const rndNum = Date.now();
    const newContact: Contact = {
      id: this.contactList.length * rndNum,
      name: `Contact ${this.contactList.length}`,
      email: `email${this.contactList.length}@provider.com`,
      avatarUrl: `https://api.adorable.io/avatars/285/${rndNum}`,
    };
    // * adding a new contact to the list
    this.contactList = [newContact, ...this.contactList];
    // * selecting the newly created contact
    this.onSelectItem(newContact);
  }

  onDeleteItem(contact: Contact) {
    // * removing a contact from the list
    this.contactList = this.contactList.filter(c => c.id != contact.id);
    if (this.selectedContact && this.selectedContact.id == contact.id) {
      // * if the removed contact is beaing focused on, then we remove the focus
      this.onSelectItem(null);
    }
  }
}

The HTML file:

<!-- * side list -->
<aside class="side-list">
  <!-- * side list toolbar -->
  <div class="side-list-toolbar">
    <span class="title">Contacts</span>
    <button class="btn" (click)="onAddItem()">Add</button>
  </div>
  <!-- * side list items-->
  <div class="side-list-items">
    <div
      class="side-list-item"
      *ngFor="let contact of contactList; trackBy: contact?.id"
      [ngClass]="{ 'side-list-item-selected': contact.id == selectedContact?.id }"
      (click)="onSelectItem(contact)"
    >
      <div class="side-list-item-avatar">
        <img [src]="contact.avatarUrl" loading="lazy" alt="profile picture" />
      </div>
      <div class="side-list-item-info">
        <p class="side-list-item-info-name">{{ contact.name }}</p>
        <p class="side-list-item-info-email">{{ contact.email }}</p>
      </div>
    </div>
  </div>
</aside>
<!-- * content wrapper -->
<section class="side-list-content">
  <!-- * content -->
  <div class="side-list-content-data" *ngIf="!!selectedContact">
    <div class="side-list-content-data-inner">
      <div class="side-list-content-data-overview">
        <div class="side-list-content-data-overview-info">
          <h3>{{ selectedContact.name }}</h3>
          <span>{{ selectedContact.email }}</span>
        </div>
        <div class="side-list-content-data-overview-avatar">
          <img [src]="selectedContact.avatarUrl" loading="lazy" alt="profile picture" />
        </div>
      </div>
      <div class="side-list-content-data-separator">
        <h4>Overview</h4>
        <div>
          <button class="btn danger outline" (click)="onDeleteItem(selectedContact)">
            Delete
          </button>
        </div>
      </div>
      <p>Lorem</p>
    </div>
  </div>
  <!-- * empty selection -->
  <div class="side-list-content-empty" *ngIf="!selectedContact">
    <div>
      <img alt="empty-selection" loading="lazy" src="https://img.icons8.com/ios/100/000000/nui2.png" />
      <p>Select an item from the list.</p>
    </div>
  </div>
</section>

Now, let's quickly copy some CSS so our component looks pretty:

  • in the list.component.scss copy this.
  • in the styles.scss copy this.

Great, we are ready to tackle the animations!

3. Animating 🧙‍♂ī¸

Animating elements of the list

First, we will start by animation the addition and suppression of elements of the list. Right now we have this:

item list

and we want this (focus on the list at the left):

animated item list

In the component file, we need to import animation functions and add a metadata property called animations: within the @Component() decorator:

import { animate, style, group, query, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
    // we will implement the animations here
  ]
})

Let's look at the animations APIs we'll use here:

function what does it do?
trigger() Kicks off the animation and serves as a container for all other animation function calls. HTML template binds to triggerName. Use the first argument to declare a unique trigger name.
transition() Defines the animation sequence between two named states. Special values :enter and :leave initiate a transition on the entry and exit states
style() Defines one or more CSS styles to use in animations. Controls the visual appearance of HTML elements during animations.
animate() Specifies the timing information for a transition. Optional values for delay and easing. Can contain style() calls within.
group() Specifies a group of animation steps (inner animations) to be run in parallel. Animation continues only after all inner animation steps have completed.

We will use trigger() to bind our animation to an animation name, we will call it "listItemAnimation". Then we will defines the animation sequence on the addition and suppression with transition(':enter') and transition(':leave'). After that we will define the starting style with style() and finally wi will animate the height with animate().

It should look like this:

animations: [
   trigger('listItemAnimation', [
      transition(':enter', [
        style({ height: '0px', overflow: 'hidden' }),
        group([animate('250ms ease-out', style({ height: '!' }))]),
// although group is useless here (since we have only one animation)
// i like to leave it anyway just in case i want to add another 
// parallel animation in the future
      ]),
      transition(':leave', [
        style({ height: '!', overflow: 'hidden' }),
        group([animate('250ms ease-out', style({ height: '0px' }))]),
      ]),
    ]),
]

ℹī¸ '!' is a special token used to use auto-styling, where styles are derived from the element being animated and applied to the animation when it starts. In simpler words, it's the state before the animation is applied.

Now that we have our animation implemented, let's bind it to the HTML element. Add @listItemAnimation to the "side-list-item" element:

    <div
      class="side-list-item"
      @listItemAnimation
      ...
    >
...
</div>

It's great we are almost done, and we have our contact list animated đŸ’Ē!

PS: If you want to disable this animation on the first print, you need to add an empty :enter animation like this trigger('noEnterAnimation', [transition(':enter', [])]) to the parent element (the list element). When you place an :enter animations on a component it will disable all it's children :enter animations, while it is triggered.

Animating the content

The content animation is a little bit trickier. If we animate the content container from left to right we have two options: either we animate the width or we translate the element. Sadly none of those solutions works. If we manipulate the width we will have this:

width animation

The content appears as the width grows, but it doesn't feel right because the content is not sliding from left to right. To fix it, we can try to translate the content from left to right, but look at what happens:

translate animation

It creates a flash effect (of the empty selection component) because while we are translating the content element, we are not animating its width, so it will go from its current value to 0 in a blink of an eye. The solution then is that we have to translate the content while animating the width.

In the figure below, we have an outer container in black and the inner container in blue:

inner and outer element

So we will animate the width of the black container while we are translating the blue one. That should look like this:

good animation

To do this, we need to animate the content element (black container: "side-list-content-data") and its child element (blue container: "side-list-content-data-inner"). For that we need to use another angular animation API:

function what does it do?
query() Use to find one or more inner HTML elements within the current element.

With the help of this API the implementation should look like this:

   trigger('sideContentAnimation', [
      transition(':enter', [
        // we set the width of the outer container to 0, and hide the
        // overflow (so the inner container won't be visible)
        style({ width: '0px', overflow: 'hidden' }),
        group([
          // we animate the outer container width to it's original value
          animate('250ms ease-out', style({ width: '!' })),
          // in the same time we translate the inner element all the
          // way from left to right
          query('.side-list-content-data-inner', [
            style({ transform: 'translateX(-110%)' }),
            group([animate('250ms ease-out', style({ transform: 'translateX(-0%)' }))]),
          ]),
        ]),
      ]),
      transition(':leave', [
        style({ overflow: 'hidden' }),
        group([
          animate('250ms ease-out', style({ width: '0' })),
          query('.side-list-content-data-inner', [
            style({ transform: 'translateX(-0%)' }),
            group([animate('250ms ease-out', style({ transform: 'translateX(-110%)' }))]),
          ]),
        ]),
      ]),
    ])

You can find the complete source file here.

The HTML file updated with the sideContentAnimation animation:

  <!-- * content -->
  <div 
      class="side-list-content-data" 
      *ngIf="!!selectedContact" 
      @sideContentAnimation
  >
  ...
  </div>

That's it, we've completed the desired behavior, and we learned how to use Angular animation API to do these pretty complex animations.

Conclusion

To conclude, we learned how to use trigger() to define animations names, then we used transition() to define animations between different states (example :enter and :leave). After that, we used style() to define the style at the beginning of the animation, we used animate() to change that style, and we also saw that we could do multiple animations at the same time with group(). Finally, we used query() to attach an animation to a child element of the one we were animating.

I hope you liked this post, and i'll see you in the next one.
Happy coding!


Hey, let's keep in touch!

I'm working on a lot of awesome posts and tutorials to come. If you liked this one, make sure to follow me on Twitter to get updated on when the next one might come out.

What to read next?

Buy Me A Coffee

Posted on by:

mustapha profile

Mustapha Aouas

@mustapha

Angular lead developer — I like sharing what I know, and learning what I don't 👨đŸģ‍đŸ’ģ

Discussion

markdown guide
 

Math.flour undefined, should be

Math.floor