Angular Animations
Matias Niemelä (@yearofmoo)
http:// yom.nu / ng-japan-2017
Animations
Matias
yearofmoo.com
Links / Slides
Slides = http://yom.nu/ng-japan-2017
Demo = http://yom.nu/ng-japan-2017-demo
Github = http://yom.nu/ng-japan-2017-code
Blog = http://www.yearofmoo.com
What to expect
Animations on the web
Basics of Animations in Angular
Overview of New Features
CSS & Web Animations
Animation on the web
How would you animate something
CSS ... Transitions? Keyframes?
A third-party JS library?
What about web-animations?
<div class="animate-me">
...
</div>
/* CSS transitions */
.animate-me {
transition:1s linear all; font-size:200%;
}
/* CSS keyframes */
.animate-me {
animation: 1s expandFontSize linear;
}
@keyframes expandFontSize {
from { font-size: 100%; }
to { font-size: 200%; }
}
The Web-animations API
Create animations using JavaScript
Native browser-level API
Basically keyframes in JavaScript
Chrome, Firefox, Opera and Safari (TP)
var element = document.querySelector('.animate-me');
var player = element.animate([
{ fontSize: '100%', easing: 'linear' },
{ fontSize: '200%' }
], 1000);
//...
//...
player.pause();
player.play();
player.cancel();
player.currentTime = 500; //50% between 0s and 1s
The disconnect from CSS
Web animations are all in JS
No stylesheets
No way to decorate animations
You need to specify every style!!!
Too low level
How Angular Does it
Angular uses web animations
Custom Animation DSL code
Integrates with the component
And is testable
// the same animation
import {style, animate, animation} from "@angular/animations";
const expandFontSize = animation([
style({ fontSize: '100%' }),
animate('1s linear', style({ fontSize: '200%' }))
]);
Animations in Angular
@angular/animations
Not enabled by default
Import BrowserAnimationsModule
Place animations in Components
// inside of your app module
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
@NgModule({
imports: [
CommonModule,
BrowserAnimationsModule
]
})
export class MyAppModule {}
SystemJS
Include the following:
animations.umd
animations-browser.umd
platform-browser-animations.umd
import {Component} from "@angular/core";
import {style, animate, transition, trigger} from "@angular/animations";
@Component({
animations: [
// animations go here...
]
})
class MyComponent {}
Triggers, Transitions and Sequences
Triggers manage animation state
Transitions animate between states
Sequences are step-by-step animations
@Component({
animations: [
trigger('expand', [
transition('closed => open', [
style({ height: '0px' }),
animate('0.5s', style({ height: '100px' }))
]),
transition('open => closed', [
style({ height: '100px' }),
animate('0.5s', style({ height: '0px' }))
])
])
]
})
Animation Bindings
Triggers act as binding properties
Within the component template
Prefixed with an @ sign
<!-- component.template.html -->
<div [@expand]="isOpen ? 'open' : 'closed'">
I will be visible when isOpen is true
</div>
<button (click)="isOpen = !isOpen">Toggle</button>
Animation Events
Events can also be used
Detect when trigger starts/completes
Prefixed with an @ sign
<!-- component.template.html -->
<div [@expand]="isOpen ? 'open' : 'closed'"
(@expand.done)="animationComplete($event)">
I will be visible when isOpen is true
</div>
$event == {
element: any, // div element
fromState: string, // "in" or "out"
toState: string, // "in" or "out"
totalTime: number, // 1000 (milliseconds)
triggerName: string, // "fade"
phaseName: string // "start" or "done"
}
1
Trigger States
Transitions don't save state
Use state(name, styles) to persist styles
@Component({
animations: [
trigger('expand', [
state('closed', style({ height: '0px' })),
state('open', style({ height: '100px' })),
transition('closed => open, open => closed', [
animate('0.5s')
])
])
]
})
Auto Styles
Use * to specify the final style
Helpful to dynamic styles like height
Works on any CSS property
Use style('*') to reset everything
@Component({
animations: [
trigger('expand', [
state('closed', style({ height: '0px' }),
state('open', style({ height: '*' }),
transition('closed <=> open', [
animate('0.5s')
])
])
]
})
1
New Animation Features
Angular 4.2+
A wave of new features
querying, staggering
sub animations
input params
routeable animations
2
query() animations
Animation Querying
multiple element animation
find newly inserted/removed nodes
serial / parallel animations
staggering!
<!-- demo app / list-page.component.html -->
<div class="list-container" [@listAnimation]="items.length">
<div *ngFor="let item of items" class="list-item">
...
</div>
</div>
trigger('listAnimation', [
transition('* => *', [
query(':enter',
style({ height: '0px' }), {optional: true})
query(':leave', [
style({ height: '200px' }),
animate('500ms', style({height: '0px'}))
]), {optional: true}),
query(':enter',
animate('500ms', style('*'))), {optional: true})
]),
]),
stagger() animations
Staggering
query() animates elements in parallel
stagger() spaces them out
which looks nice
query(':leave', stagger(100, [
style({ height: '200px' }),
animate('500ms', style({height: '0px'}))
])), {optional: true}),
query(':enter', stagger(-100, [
animate('500ms', style('*'))
]), {optional: true})
animateChild() animations
Sub Animations
query() can find inner elements
that have animations
and use animateChild() to allow/skip
// some particular animation
query('@someAnimation', [
animateChild()
])
// all sub animations
query('@*', [
animateChild({ duration: '1s' })
])
2
<form [@searchBarAnimation]="state">
<div [@showHideAnimation]="state == 'loading' ? 'on' : 'off'">
<!-- loading animation -->
<div [@showHideAnimation]="state == 'active' ? 'on' : 'off'">
<!-- profile name -->
<div [@showHideAnimation]="state == 'search' ? 'on' : 'off'">
<!-- search field -->
trigger('searchBarAnimation', [
state('active, loading', style({ backgroundColor: '#333', color: 'white' })),
state('search', style({ backgroundColor: '#fff', color: 'white' })),
transition('* => *', [
//...
])
]),
transition('* => *', [
style({ height: '!', position: 'relative', overflow: 'hidden' }),
group([
query('@showHideAnimation', [
style({ position: 'absolute', top:0, left:0, right:0}),
animateChild(),
]),
animate('500ms cubic-bezier(.35,0,.25,1)')
]),
]),
animation { {substitutions} }
Animation Input Parameters
All style data is static
* styles are dynamic
How do we pass in data?
Animation Style Substitutions
trigger('heightChange', [
transition('* => *', [
style({ height: '{{ fromHeight }}' })
animate(1000, style({ height: '{{ toHeight }}' }))
], { params: { fromHeight: '0px', toHeight: '100px' })
])
Reusable Animations
const heightAnimation = animation([
style({ height: '{{ fromHeight }}' })
animate(1000, style({ height: '{{ toHeight }}' }))
], { params: { fromHeight: '0px', toHeight: '100px' })
Animation Style Substitutions
trigger('heightChange', [
transition('* => *', [
useAnimation(heightAnimation, {
params: {
fromHeight: '0px',
toHeight: '100px',
}
])
])
])
<div [@heightAnimation]="{value: heightState, {
params: { fromHeight: '33px', toHeight: '66px' }
}">...</div>
2
<app-modal
*ngIf="modalIsActive"
[startCoordinates]="{x: xValue, y: yValue}">
</app-modal>
@Component({
selector: 'app-modal',
templateUrl: 'modal.component.html
animations: [
//...
]
})
export class ModalComponent {
//..
}
export class ModalComponent {
public modalState: any = '';
@Input('startCoordinates')
public startCoordinates: any = {};
ngOnInit() {
const x = this.startCoordinates['x'] || 0;
const y = this.startCoordinates['y'] || 0;
this.modalState = {
value: 'ready',
params: {x, y}
};
}
}
<!-- modal.component.html -->
<div class="container" [@containerAnimation]="modalState">
...
</div>
trigger('containerAnimation', [
state('*', style({ display: 'none' })),
state('ready', style({ display: 'block' })),
transition('* => ready', [
style({
display: 'block', opacity: 0, transform: 'scale(0)', transformOrigin: '50% 0%', top: '{{ y }}px', left: '{{ x }}px',
}),
animate('300ms 200ms cubic-bezier(.35,0,.25,1)', style(['*', {transformOrigin: '50% 0%'}]))
])
])
route animations
2
Animations + Routes
Each route can have animations
Transitions = route changes
query() can dictate when they happen
export const ROUTES = [
{ path: '',
component: ListPageComponent,
data: {
animation: 'indexPage'
}
},
{ path: 'users/:slug',
component: ProfilePageComponent,
data: {
animation: 'profilePage'
}
}
];
<div [@routeAnimation]="prepRouteState(outlet)">
<!-- ... -->
<div class="router-container">
<router-outlet #outlet="outlet"></router-outlet>
</div>
</div>
Preparing the router state
The component reads the config
Returns the router state
Or creates its own value
class AppCmp {
prepRouteState(r) {
return r.activatedRouteData['animation'];
}
}
The Router Animation
The container handles the change
It can then tell children to animate
Or do its own animation
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
animations: [
trigger('routeAnimation', [
transition('indexPage <=> profilePage', [
// 1. style the containers
// 2. run the removal animation
// 3. run the enter animation
])
])
]
})
transition('indexPage <=> profilePage', [
// 1. style the containers
query('.router-container', style({ position: 'relative '})),
query(':enter, :leave',
style({ position: 'absolute', top: 0, left: 0, right: 0 })),
query(':enter', style({ opacity: 0, transform: 'translateY(400px)' })),
//...
// 2. run the removal function
group([
query(':leave', animateChild()),
query('@searchBarAnimation', animateChild()),
]),
//...
// 3. run the removal function
group([
query(':enter', group([
animate('400ms cubic-bezier(.35,0,.25,1)', style('*')),
animateChild({ delay: '200ms' })
])),
query(':leave', [
animate('400ms cubic-bezier(.35,0,.25,1)', style({ transform: 'translateY(-400px)', opacity: 0 }))
]),
]),
])
Final Notes
Twitter = @yearofmoo
Slides = http://yom.nu/ng-japan-2017
Demo = http://yom.nu/ng-japan-2017-demo
Github = http://yom.nu/ng-japan-2017-code
Blog = http://www.yearofmoo.com
ありがとう!