Archives for June 2018

June 14, 2018 - No Comments!

Animate common interactions with Angular Animations


Angular offers
@angular/animations package that enables you to write complex, hierarchical transitions in a declarative way. The package is still in the experimental state as of Angular 6, yet it’s totally usable, and it’s not like such fact has stopped us before, is it?

Low-intermediate understanding of Angular is assumed.

The very first animation

Well, there is no good way to describe UI animations with words so below I present to you an editable example. We have buttons that can change the Angular version number very rapidly, modeled after admin dashboard in Angular HQ 🌞.

I wanted to capture few nice things about Animations:

  • It’s simple to define multi-step transitions that look natural, just add more items to an array.
  • You can duly on computed values with *
  • Inserted and removed elements can be animated easily
  • With state(), you can apply inline supporting styles that are active as long as the specified state. They sit next to transitions and are grouped conceptually
  • Framework provides many common state change expressions like :increment

Why should I care?

True, we already have many ways of creating motion on the web and Angular apps support them all. Animations package seems somewhat redundant at first sight, but it exists for a good reason. It provides seamless integration of many animations with themselves and also with the framework. You can of course mix them with other solutions where appropriate.

Previously, to achieve hardware-accelerated motion you could use:

  • CSS transitions that offer a simple way of declaring basic, two-state changes with constant duration. You can make duration variable by binding to style.transition-duration but it feels hacky at best.
  • CSS animations - more complex, you can describe keyframe sequences, declare (possibly infinite) repetitions and so on but you can’t express relations between various elements of the page.
  • requestAnimationFrame that allows you to make imperative animations, and you certainly could use power of RxJS to create neat reactive animations but it will require additional glue to tie everything back into framework in many cases. (Nevertheless it’s a viable, novel solution for certain problems, greatly explained in this talk by Ben Lesh, creator of RxJS)

Angular Animations in a nutshell

  • It’s a bunch of functions that you compose in a certain way (a Domain Specific Language if you prefer).
  • Animations are triggered by changing value bound to @triggerName. They mix well with change detection, component lifecycle, and other framework concepts.
  • Can be externally disabled for whole element subtrees with @.disabled synthetic property. You can for example disable animations for initial page load and later transition between routes.
  • You can pass parameters along with state values and interpolate them inside animation definitions.
  • They offer callbacks named @triggerName.start and @triggerName.done that greatly improve over for example transitionend event.
  • You can save your declarations in separate files and reuse them in many components.

BTW

The official guide for animations was, let’s say, not sufficient, during Angular 5 era and before, so reading is discouraging. Animation parameters is one example of poorly documented, yet powerful feature.

 

Setup

There are some things you must ensure when setting up your project with Angular Animations

  • First of all check if @angular/animations package is installed
  • Import BrowserAnimationsModule in your AppModule.
  • Now you can add @-prefixed properties to your templates.

BTW

There is also NoopAnimationsModule for when you want to disable animations globally. Without importing either animation module any template with animation-specific symbols will fail to compile.

Wiring everything up

In the Component decorator you can pass an array of animation definitions. Most examples do it inline, but it can become messy very fast and it doesn’t support reusability, so we will go with the good practices from the start.

import { squash } from './squash.animation';

@Component({
 animations: [squash],
})

You can see a shortened squash.animation.ts file below. Basic animation usages will start with topmost trigger() containing array of state() and transition() functions that I’ll explain later in the article.

export  const squash = trigger('squash',  [
 state('*', style({
 'text-transform':  'uppercase',
 })), 
 transition(':decrement',  [
 animate('100ms', style({
 transform:  'scale(0.9, 0.9)',
 })),
 animate('300ms'),
 ]),
]);

Finally, you need to bind something to animation property. Value can be anything as long as it makes sense for the animation definition.

export  class AppComponent {
 version =  6;
}

<p [@squash]="version">...</p>

The DSL

import {  
  trigger,  
  state, transition,  
  style, animate, keyframes,
  group, sequence,
  query, stagger, animateChild,
  animation, useAnimation,  
} from '@angular/animations';

Whoah, it’s very intimidating at first and I probably shouldn’t have frightened you with this complete list of DSL functions, but don’t worry as you’ll only need to grasp 4-5 of them to begin with. I’ve grouped them into related groups for your convenience.

NOTICE

Some time ago there was another way of importing animation building blocks but it was removed as of Angular 6 and deprecated earlier.

One of the pain points of Animations package is that all of the above functions return internal metadata and sadly it won’t tell you much. The same thing is with overly complicated input parameters. That’s where tutorials come into play.

I’ve listed below some simplified definitions of most common functions in Typescript pseudocode. You can actually omit some things, for example treat some single-item arrays as just items, but they needlessly make everything harder to grasp.

trigger(name: string, definitions: Array<state() | transition()>)

It will be the outermost function you’ll use most of the time, at least at the beginning. It defines name of the property you will use to bind your state variable. trigger('example', ...) <=> <div [@example]=value></div> .

definitions is an array of composed state() and transition() functions. Conflicting definitions are evaluated from left to right.

state(name: '*' | string, style: style(), options?)

It applies inline styles when bound value matches name argument. Wildcard * always matches.

transition(stateChangeExpr: string | Function, steps: Array<Step>, options?)

Defines visuals that happen after bound value changes.

stateChangeExpr in it’s simplest form is a string in the form of fromState => toState, but there are some special rules. Keep in mind that state values are converted to strings before comparison.

  • <=> to defines two-way transition.
  • * matches any state.
  • void is a special state that means nonexistence. It’s something different than null (here we go again 🙄…). The elements are in this state before insertion into DOM and after removal.
  • :enter and :leave are quite handy shortcuts for void => * and * => void, respectively. You can pair them nicely with *ngIf and others.
  • :increment and :decrement are nice example of behavior that otherwise would be only possible with custom function. They match numeric changes, I’ve used them in an example at the top of the article.

In contrast to state(), applied styles are removed after animation is done, for example transition(..., [style(...)]) does nothing, because it lasts for 0 frames.

style(css: object)

It defines actual styles. css properties can be written in camelCase or dash-case. You can use value computed from the element with * - it enables for example expanding accordion menu. For example:

{
  height: '*',
  transform: 'scale(1.3, 1.2)',


  lineHeight: 3,
  'text-transform': 'uppercase',
}

animate(timings: string | number, styles: style() | keyframes())

First function in this listing that actually creates motion. timings is a number of milliseconds or string in format duration delay easing. Only duration is required. Parts of the format behave like CSS transition counterparts, but the order is different. In the basic scenario you compose animate() with style(), and transition happens from the state existing before animate() to the one specified with style(). You can chain multiple animate()together and mix them with standalone style() to create multi-step transitions. You can omit second argument in the last animate() to transition to styles declared elsewhere, i.e. plain CSS.

keyframes(steps: Array<style()>)

Instead of chaining multiple animate() together you may wish to specify many steps as an array in composed keyframes(). By default they will stretch evenly over duration of animate(), but you can override it with special style property offset taking 0-1 ratio value.

animate('300ms', keyframes([
 style({color: 'indianred', offset: 0}),
 style({color: 'cadetblue', offset: 1}),
]))

It's the same as

animate('300ms', keyframes([
 style({color: 'indianred'}),
 style({color: 'cadetblue'}),
]))

or

animate('150ms', style({color: 'indianred'})),
animate('150ms', style({color: 'cadetblue'})),

Random yet useful info

Endless animations

That’s a bummer with Angular Animations - there isn’t a built in way of declaring them, but you can emulate it in many ways, for example by declaring universal transition * => * and changing trigger in @triggerName.donehandler. I won’t go into details here but you get the idea.

Order of execution

Animations happen after things that trigger them. For example, when animating :leave state change, ngOnDestroy gets called just before animation starts. Similarly :enter transition happens after ngOnInit and whole first change detection cycle.

Animation parameters

Take a look at the following example. Template-style {{interpolation}} can be used to bind dynamic values, for example mouse coordinates. You may need to provide defaults as params key in - previously omitted - last argument to almost all DSL functions.

someControl: FormControl;

@HostBinding('@triggerName') animation;

handleClick() {
 this.animation = {
 value: 'state',
 params: {  
   offset: this.someControl.value,  
 },
 }
}

trigger('triggerName`, [
 state('state', style({  
 transform: 'translateX({{offset}}%)',  
 }), {params: {offset: 0}}),
]);

 

BTW

In Angular 5 and before, Animations were fully powered by Web Animations API, whose support is currently spotty. Dynamic features of Angular Animations still rely on this API, but it’s something out of scope for this article. There is a polyfill available if you need it.

That’s all

I hope that this article explained briefly the basics of Angular Animations. There are many more nice bits about this package, unfortunately scattered around many places. I highly encourage you to read API docs. Play with more advanced functions - I haven’t even touched query() and hierarchical animations. Check source code if you wish. Happy hacking.


Tomasz Błachut

 

Published by: admin in blog
Tags: , ,