Angular — Applying Motion principles to a listing

Gerard Sans
Google Developer Experts
8 min readAug 21, 2017

--

How to apply Motion principles using Angular Animations

Image by thedotisblack. Credits

In this article I am going to describe how you can apply Motion principles to different interactions using a list of items as a base. We will cover:

You can explore the source code by using these:

Initial | Final

Find the latest Angular content following my feed at @gerardsans.

This example was part of a demo to support a talk at Women Who Code London. In this talk, I did an introduction to the same principles I am going to be using here. Feel free to check the slides for an introduction.

Dependencies Setup

First of all, we need to do some preparations and make sure we have all the dependencies in place. To use Angular Animations, we need: @angular/animations and @angular/platform-browser/animations. For Angular Material: @angular/material and @angular/cdk. Besides these we will also import Indigo Pink theme and Material Icons (used in buttons). See how below.

// style.css
@import
'https://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css';
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';

On our Root Module we will add the above modules like this:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule, MdButtonModule } from '@angular/material';
@NgModule({
imports: [ BrowserModule, BrowserAnimationsModule, MaterialModule, MdButtonModule ],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule { }

For more information on how to setup Angular Material check out this tutorial by Loiane Groner @loiane.

Solution

The image below alternates both, the version without animations and the final version. Things to notice here are how the list displays pre-existing items initially and the effects of adding and removing items.

Initial version followed by final version.

We are going to use a basic listing setup to apply our animations using ngFor.

@Component({
selector: 'my-app',
template: `
<div class="buttons-container">...</div>
<div class="list-container">
<div class="box" *ngFor="let item of list"></div>
</div>`
})
export class App {
list = [1,2,3,4];
}

This will render a list of 4 elements. For the layout we are using: buttons-container, list-container and box.

.buttons-container, .list-container {
display: flex;
align-items: center;
justify-content: center;
}

.buttons-container {
flex-direction: row;
}

.list-container {
flex-direction: column;
}

.box {
width: 100px;
height: 50px;
background-color: #9c27b0;
margin: 5px;
}

We used flex to style our containers. Buttons use row (left-to-right) and the list column (top-to-bottom) flex-direction respectively. To style the items we used the box class.

Styling Material Buttons

In order to style our Add and Remove buttons we used the following code.

<button md-fab>
<i class="material-icons">&#xE147;</i>
</button>
<button md-fab [disabled]="list.length===0">
<i class="material-icons">&#xE872;</i>
</button>

We chose md-fab which is the circular button with elevation. For the icons we searched the Icons library and used the version to maximise browser compatibility with IE9 or below. Notice the code to disable the Remove button when there are no more items in the list.

Angular Animations

Let’s briefly introduce Angular animations. Angular is based on top of the Web Animations API, we can use triggers to define a series of states and transitions between states. We use styles that help us build the desired effect using CSS properties.

The main principle behind animations is that once we set the initial and final state using styles, the intermediate states are being calculated for us by the browser.

Who else better to explain animations than the party parrot!? 😃

We can set the duration of our animations by using a value in seconds or milliseconds. Ex: 1s, 1.2s, 200ms. Sometimes we may also want to control the timing function which sets the pace in which the intermediate steps, tweens, are calculated. The default timing is ease; other common values are linear or ease-in-out.

Since Angular v4.2+ we can also use sequence and group to run animations one after the other or in parallel; and query to access child elements and stagger to create nicely chained choreographies.

Animating new items

Let’s explore the code involved in adding new items:

@Component({
template: `
<div class="top-buttons">
<button md-fab (click)="add()"></button>
</div>
<div class="list-container">...</div>`
})
export class App {
counter = 5;
list = [1,2,3,4];

add(){
this.list.push(this.counter++);
}
}

We used an event binding to the first button to call the add method on click. This will add a new number starting from where we left using a counter.

So far this will display the new items immediately which can feel a little rough. To improve the User Experience, we are going to apply the following principles: anticipation, exaggeration and composition.

Anticipation Principle

This principle adds realism and prepares the viewer for the action before it takes place. In our example, we don’t want the items just to pop up from thin air. We will use a fade in effect using opacity to help with anticipation.

Exaggeration Principle

The exaggeration principle is all about making the action feel more extreme. This will increase the appeal and enhance the action. To achieve this, we are going to play with the size of the item being added. What we can do is scale it up and down to its regular size using a cubic-bezier timing function. This usually requires some fiddling but we can use this tool online by Lea Verou.

Composition Principle

This is also known as secondary or layered animation. This is the result of combining separate animations into one. In our example, we are going to compose the previous two animations into one so they happen concurrently.

Let’s add these animations to our list!

<div class="list-container">
<div @items class="box" *ngFor="let item of list"></div>
</div>

First we will add the trigger @items to tell Angular that we want to handle the animations affecting each div item in the list. After that, we will add the trigger definition into the animations component metadata. These two need to use matching names.

@Component({
animations: [
trigger('items', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }), // initial
animate('1s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })) // final
])
])
]
})

For our trigger we defined a transition using :enter to capture new DOM Elements being added to the view. In our code we are using a custom timing function. This is how our bezier curve looks like. When using cubic-bezier values that go beyond the 0–1 intervals, we allow our CSS styles to go beyond the values we used for the initial and end states. For our trigger, this means, that we will get values going from a little less than scale(0.5) and a little over scale(1) creating a tiny bouncing effect.

Animating removing items

Let’s look at the code used for removing items from the list.

@Component({
template: `
<div class="top-buttons">
<button md-fab (click)="remove(0)"></button>
</div>
<div class="list-container">
<div @items class="box" (click)="remove(i)"
*ngFor="let item of list; let i=index;">
</div>
</div>`
})
export class App {
remove(index) {
if(!this.list.length) return;
this.list.splice(index, 1);
}
}

Similarly as we did before, we used an event binding to call the remove method with index 0. On each click we will remove the first element of the list using splice and passing the index 0. We can also use this new method to be able to remove individual items. To get the current index within the list we can use a template variable i and pass it as an argument to remove. For the first div the resulting code will be (click)="remove(0)" and so on.

This will remove the items immediately. As we did before, we are going to apply the following principles: anticipation, exaggeration and composition. We will use the same states as before but in reverse.

transition(':leave', [
style({ transform: 'scale(1)', opacity: 1, height: '*' }),
animate('1s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({
transform: 'scale(0.5)', opacity: 0,
height: '0px', margin: '0px'
}))
])

For this transition we used:leave to capture DOM Elements being removed from the view. Everything else should look familiar this time but the height and margin. These are to fix an issue with the animation leaving a gap where the element was and then dropping to close the gap with the rest of the items. See below:

Animations without handling “height” and “margin”.

The asterisk in height is to tell Angular to use the current value of the DOM element for this property.

We could have used 50px instead of * but the latter will allow us to change the CSS value without having to worry about the value in the animation.

Animating the initial display of the list

We are almost done. There’s one last thing to do. When the list shows for the first time all pre-existing items are displayed at once. That’s not very nice, is it? Let’s fix that!

<div @list class="list-container">
<div @items class="box" *ngFor="let item of list"></div>
</div>

This time we want to be able to control not one DOM Element but a list. We can do this by adding the trigger to the parent and using query to dynamically animate each item separately.

trigger('list', [
transition(':enter', [
query('@items', stagger(300, animateChild()))
]),
])

As we have seen before, we used :enter to capture when the div is first added to the view. Note that this trigger applies to the div with list-container class. The query command uses a selector, in this case a trigger selector, that will match child nodes using the @items trigger. The query together with stagger and animateChild will result in the list of pre-existing 4 div items, running their individual animations with a 300ms delay. You can see the resulting animation below.

That’s all! Think I missed something? Contact me on @gerardsans or gerard.sans_at_gmail.com. Thanks for reading!

Further Reading

--

--

Gerard Sans
Google Developer Experts

Helping Devs to succeed #AI #web3 / ex @AWSCloud / Just be AWSome / MC Speaker Trainer Community Leader @web3_london / @ReactEurope @ReactiveConf @ngcruise