Angular — Applying Motion principles to a listing
How to apply Motion principles using Angular Animations
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:
- Dependencies Setup and final Solution
- Styling Material Buttons and introducing Angular Animations
- Applying Motion: adding, removing and initial display.
You can explore the source code by using these:
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.
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"></i>
</button>
<button md-fab [disabled]="list.length===0">
<i class="material-icons"></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.
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:
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
- Creating Usability with Motion: The UX in Motion Manifesto by Issara Willenskomer @UX_in_Motion
- Getting Started with Angular Material 2 by Loiane Groner @loiane.