Stateless animations in Angular 2

Angular is a popular web framework developed by Google. We opted to use it in our web application development because of it's advanced feature set and active development.

One major challenge with Angular is the integration of D3 visualizations into Angular. This is mostly due to the fact that Angular likes to have complete control over DOM tree manipulations and some of D3's appeal lies in it's powerful and easy to use tools to manipulate said tree. Hence integrating existing D3 code provides some obstacles. One artifact that we encountered was that visualizations were loaded twice creating as it seems two disjointed parts of the DOM tree for the same visualization.

Our approach to overcome this, was to delegate all the DOM manipulations to Angular and only use D3 for it's math library (providing layout, simulation and such). One consequence was that we needed to abandon D3's slick animation facilities and use Angular's.

Angular 2 animations

As it stands Angular's animations revolve around defining different component states - a fixed set of properties - and transitions between them. For example:

animations: [

    trigger( componentState, [

        state('inactive', style({ backgroundColor: '#00ee00' })),

        state('hover', style({ backgroundColor: '#ee0000' })),

        transition('inactive => 'hover', animate('50ms ease-in')),

        transition('hover' => 'inactive', animate('50ms ease-out'))

    ])

]

The problem: This animation is bound to the property componentState which can have two values - inactive and active - corresponding to the two states. To tie it all up transition between the states are defined. This is Angular animations in a nutshell - states and transitions. Now any change of componentState to any value corresponding to a state will trigger the transition between them.

This approach to animations works very well when animating components like buttons and menus. But what about zooming and panning in a visualization or other component? In such a case we simply lack predefined states that we can use.

We don't even have more than one state to transition between. We need dynamic states that can be generated at runtime and fed into the animation engine.

Currently the devs are working on it to make transition between dynamic states possible but there's no ETA and we're stranded without any built in solution.

One possible solution

Angular animations a built upon the (experimental) Web Animations API which is far more flexible. The irony is that using this API requires to manipulate the DOM tree outside of Angulars confines - what we initially set out to avoid. The upside is that Web Animations are much closer to Angular than D3's animations.

// Web Animations

var g = this.elem.nativeElement.querySelector('g');

var anim = g.animate([

    { transform: `translate(${oldTranslate[0]}px,${oldTranslate[1]}px) scale(${oldScale})` },

    { transform: `translate(${this.translate[0]}px,${this.translate[1]}px) scale(${this.scale})` }

], { duration: 1000, fill: 'forwards' });

// D3

svg.transition().duration(transition_speed).attr("transform", "translate(" + translate + ")scale(" + scale + ")").on('end', drawAnimation);

So all is good now, right? No!

A central concept in Angular is data binding - the ability to reflect changes in data to our view of the website without the need for the developer to handle all the messy updates. We just bind values to our components and let Angular do the rest.

Consider this:

// after the animation

anim.onfinish = () => { boundPieceOfData = true; };

After our animation finishes we change a bound piece of data and expect the view to updated accordingly. But this won't be the case. Because our code does not run inside a zone.

In the zone

To achieve it's coupling of data and the view Angular uses zones. These are execution contexts that track a given set of operations - even asynchronous ones - until they complete. The rationale is that the view needs to updated only if something happens (like user interaction or callbacks). In this case track all ensuing operations with a zone and update the view when something finishes.

The problem with our animation code is that it runs outside any zone. A way to change that is to include it in the component's zone for tracking:

// get Angular to inject the zone and the component's DOM element

constructor(dataProvider : VisualizationDataProvider, color: ColorService, elem: ElementRef, private _ngZone: NgZone) { ... }

// do stuff inside the zone

anim.onfinish = () => { _ngZone.run( () => { boundPieceOfData = true; ); };

ConclusionTada! Now it works.

Doing animations in a truly Angular way has it's limitations but until more features arrive we can rely on Web Animations and zone.js to get the job done.

 

  • No comments found

Leave your comments

Post comment as a guest

0 / 600 Character restriction
Your text should be less than 600 characters