DEV Community

Cover image for All you need to know about Angular animations
Mustapha Aouas
Mustapha Aouas

Posted on • Edited on

All you need to know about Angular animations

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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

ℹ️ 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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  ]
})
Enter fullscreen mode Exit fullscreen mode

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' }))]),
      ]),
    ]),
]
Enter fullscreen mode Exit fullscreen mode

ℹ️ '!' 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>
Enter fullscreen mode Exit fullscreen mode

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%)' }))]),
          ]),
        ]),
      ]),
    ])
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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!


That's it for this post. I hope you liked it. If you did, please share it with your friends and colleagues. Also you can follow me on twitter at @theAngularGuy as it would greatly help me.

Have a good day !


What to read next?

Top comments (4)

Collapse
 
skye profile image
Skye Wu

Math.flour undefined, should be

Math.floor
Collapse
 
mustapha profile image
Mustapha Aouas

Thanks, I made the change

Collapse
 
natsl05164 profile image
Info Comment hidden by post author - thread only accessible via permalink
natsl05164

this does not work

Collapse
 
mustapha profile image
Mustapha Aouas • Edited

Please have a look at the Github repository and compare with your code to find out what you did wrong.

Some comments have been hidden by the post's author - find out more