Patterns for Using GreenSock in Angular
Thankfully, GreenSock is dynamic enough that there are patterns and principles that can be employed to make it fit the Angular Way rather than working against it. I’m going to take you through a whole bunch of these concepts that I’ve worked out while using Angular and GreenSock together in actual projects. Here’s a high-level overview:
- Encapsulation: Using GreenSock safely with Angular components, without performing global-scope DOM manipulation.
- Decoupling: Keeping our animations DRY and orthogonal to support future changes to our code.
- State Management: Maintaining a Single Source of Truth so Angular always knows what it needs to know.
- Sequencing: Safely sequencing animations across components.
Now that’s a lot of ground to cover, but don’t worry, I’m going to keep you engaged (or at least, I’m going to try very hard).
Our demo app: gsap-lights
I’ve created a fun little demo app that illustrates everything we’re going to cover, and I’ll be tapping it for code examples throughout. A lot of you may be familiar with the classic electronic puzzle game, Lights Out. Or, more likely, you’re familiar with the basic concept of the puzzle from the many, many video games that have copied it over the years. You’re given a square grid of lights, some of which are lit. Clicking any light toggles it, but also toggles each of the immediately adjacent lights. The goal is to get all of the lights turned off.
I’ve created a simple, browser-based version of this puzzle using Angular and GreenSock, just fancy enough to be useful for our animation practice. You can find the source code on GitHub ready to be cloned and run on your machine.
If you’re using Angular 6 in your project (and you should be) like I do in gsap-lights, there are a couple compatibility hiccups you’ll come across.
- The npm/yarn version of GreenSock was only refactored as ES6 modules in version 2.0.0. If you try to use a 1.x version with Angular 6, it won’t work; you’ll get console errors and tweens or timelines just won’t do anything. The move to ES6 modules is the only major change in 2.0.0.
- The Angular 6 build optimizer that’s used for
--prodbuilds doesn’t work with GreenSock. This Stack Overflow post explains how to turn off the optimizer.
In the context of Angular components, view encapsulation refers to Angular’s practice of using shadow DOM to keep each component’s template and styles from colliding with other components on the page. GreenSock’s approach of direct DOM manipulation allows the developer to essentially bypass/avoid these mechanisms by using page-wide selector querying to target elements for tweening. This is a bad idea in an Angular app. Reaching outside of Angular to manipulate the page DOM breaks view encapsulation, which can create race conditions and generally destabilize things.
As it turns out, selector strings aren’t the only way to set up GreenSock animations. All of the tween and timeline methods can also accept a direct reference to an element. And wouldn’t you know it, Angular provides us with some excellent tools for obtaining just that: the
We’ll start with
@ViewChild. This decorator can be placed on a property of a component to obtain an
ElementRef, an object consisting of just one property
nativeElement which is of type
HTMLElement. Let’s look at how that works:
Here we have a property getter that retrieves the
HTMLElement from within the
ElementRef given by the
@ViewChild. You might be wondering about that
"light" string we’re passing into the decorator. This is simply the name of a template variable we’ve placed in our component’s HTML to identify the div we want:
This is a requirement when we’re trying to retrieve a reference to an ordinary element rather than a component. Once we’ve done all this, we can use
this.light as our target for GreenSock animations within this component class:
Now, let’s consider what this means. If we were using selectors as our GreenSock targets, we’d do something in our template like
<div class="light"> and then use
'.light' as our target. This would work fine … unless we used the component more than once on the page, which is one of the main benefits of using components. Because GreenSock will target all elements on the whole page that match the
'.light' selector, using it in any one instance of the component would target the corresponding divs in all instances, obviously not the intended behavior. But
@ViewChild uses an instance scope, so whether you have one instance or 100, each instance’s property will only refer to the child of that instance.
There’s also a special way to use
@ViewChild to retrieve the host element of the component itself. All we have to do is put an
ElementRef in our constructor:
We then set up a getter just like usual, and the element we get is the host of our component. This lets us animate our entire component without needing a wrapper div.
@ViewChild works a little differently when used to retrieve child components instead of elements. In addition to using template variables, we can just pass the component class we’re looking for into the decorator, like so:
When used this way, the property will not retrieve an
ElementRef, but rather the actual component class instance. That means we can actually access the child component’s properties and methods, which can come in very handy when building interactive animations like we’re doing.
If you have a finite number of unique elements to work with, you can set up each one with its own template variable. But what if you’re using an
ngFor loop, possibly without even knowing at compile-time how many elements it will generate? That’s where
@ViewChildren comes in. As you might guess, this decorator is essentially a plural version of
@ViewChild. It retrieves a
QueryList of the matching components or elements. We can iterate over the
QueryList to read or manipulate data with a dynamic number of instances of the same component:
As a side note,
QueryList has a bonus feature, too: Angular updates it anytime something happens in the DOM to change the result set. For example, if the iterable of an
ngFor changes in contents or length. Every
QueryList has a
changes Observable that can be used to subscribe to these updates. We don’t have a use for this in gsap-lights, but it can come in handy when working with more complex or database-driven apps.
document.querySelectorAll methods to retrieve elements by CSS selector. You shouldn’t use those in Angular, but there are also
querySelectorAll methods on the
HTMLElement type. This version only returns elements that are children of the element you call the method on. Remember when we used constructor injection to get the host element of our component? You can call
querySelectorAll on that element, and your results will be limited to children of that instance only:
You might be tempted to wonder why we’d even bother with all the
@ViewChild stuff when we can just do this. Selector querying is still reaching around Angular to access the DOM, no matter how carefully you protect it. There may be very simple use-cases where it’s the most efficient approach, but once you start working animations into increasingly complex components, you really should stick to using
@ViewChildren to keep your component tree clean and DRY.
So, at this point, we know how to get our GreenSock targets in an encapsulation-safe way using the mechanisms provided by Angular. Next up, let’s see what this means for how we should organize our animations.
One of the most important programming principles out there (at least in the humble opinion of this admitted major fan of said principle) is modularity. It’s the guiding principle behind the component-centric architecture of modern front-end frameworks like Angular and React. It paints a beautiful picture of a system that is in fact an intricate assemblage of discrete pieces, each one responsible for specific small functions, communicating with each other to achieve larger tasks.
In a truly modular system, each piece has its own business to mind; it need not mind the business of other pieces, nor do other pieces need to mind its business. Each piece provides an API for other pieces to utilize, and that’s all the information that needs to be exchanged. In such a system, you can make changes or improvements to one piece without breaking others, so long as you don’t have to change those API’s.
Again, this is the guiding principle behind Angular’s use of components. A well-written component in Angular should perform specific, closely related functions, without requiring any more information than is necessary to do so. Applying our context for this article, each component should be responsible for setting up its own animations. A component should care about animating itself, period. The other components can worry about their own animations.
For an example of this, let’s take a look at our lights puzzle. We have a
LightsOutPage that holds a
LightsGridComponent, which has several instances of
LightsRowComponent as children, each of which in turn has
LightComponent children. If you examine the TypeScript classes for each of these four components (pages are also components), you will notice that only the
LightComponent actually does any tweening. Why? Because the lights are the only things actually animating. The page, the grid, and the rows aren’t ever the targets of any animation, only the lights themselves are visibly changing.
An important rule for decoupling involves how to manage responsibilities for sequencing. We’ll discuss different approaches for sequencing later, but this rule applies to all of them. Just as a component should only animate itself, it should never be responsible for figuring out animation sequences with its siblings or parent. Its own animations should be passed out to some other entity, such as a parent component or a service, to handle that sequencing. This is important because breaking this rule will require you to violate modularity in other ways. You’ll end up needing to pass superfluous information between components just for the sake of the animation.
Angular is a very state-based world. For optimal performance, properties, bindings, events, and other Angular mechanisms should be updating and reacting to the states of the various directives. Animations are great for transitioning between states. But if the animation is actually the one managing the state changes, you can end up introducing states that are actually dependent on the associated animations, not associated with any logic in Angular, and not easily reachable in a non-linear way. In other words, your animations, rather than your component logic, are driving your state.
This is backwards, and it creates a lot of problems. Animations can respond to states, trigger states, and transition between states, but they shouldn’t unilaterally assemble new states that don’t otherwise exist. In the future, you might find yourself needing to access one of those states without running a complex animation sequence. Or you might need that state to “mean something” to some other part of the system. To do this, you need your state to drive your animations, not the other way around.
First, it’s helpful to understand the difference between a state and a transition. A state has some sort of logical value to the system, while a transition is just the process of moving from one state to another. Every developer needs to figure out this distinction for their own app. Perhaps a component has an “active” state and an “inactive” state, like the
LightComponent in our demo app. The question is, does it need additional states for “activating” and “inactivating,” or can these just be transitions? It largely depends on whether any app behavior must be linked to those conditions. In our case, the extra two states are not necessary, because they have no logical meaning for our app.
Animations work differently for states versus transitions. If an animation is truly associated with a state, it’s going to be some sort of repeating (or, at the very least, infinite) animation that plays as long as the state is active. The
ButtonComponent has an example of this:
startFlashing method initiates an infinitely repeating animation, and saves it to a property on the component so we can access it later. The
stopFlashing method uses that property to kill the flashing animation, and then kicks off a transition to make sure the button returns fully to its original appearance.
With a transition, the component is simply moving from State A to State B, which would typically mean a unidirectional animation that plays for a finite amount of time and then completes. This is how the animations work on the
LightComponent since it has no state animations:
toggle method uses the state of the component to determine which transition to play:
This method also contains a bit of “magic” code that’s very important when working with state and animations. By default, functions that run in GreenSock timelines execute outside of the “Angular Zone.” I’m not going to get into the details of Zone.js in this post, but suffice it to say that if a function executes outside the Angular Zone, nothing it does will trigger Angular’s change detection. Obviously, this is a problem when updating state. Fortunately, Angular does give us a means to force our code to execute in the Angular Zone: the
NgZone class. We can import it from
@angular/core and then inject it like a service into our constructor:
Then, we wrap any state-changing code we want to run in a call to our NgZone’s
run method, as in the snippet above. Now, that line updating our component’s property will trigger Angular’s change detection, ensuring that we can base other, non-animation logic on that property if we want to.
Also notice that we have the ability to change the state without playing the full animation, which we use for resetting both on demand and on initialization:
This is a fairly simple example; things would be more complicated if state animations were involved, as we’d also have to make sure we could clean those up whenever the state changes, like we do in the
ButtonComponent. Storing such animations in the component after their creation, and using GreenSock’s kill methods to prevent collisions, are both useful for this purpose.
One of GreenSock’s coolest features is the Timeline. Timelines allow you to sequence animations together in amazingly dynamic ways. But when working with Angular components, you have to deal with animations potentially coming from many different places, and the need to tie them together without violating modularity or encapsulation. I’ve identified some repeatable patterns that achieve these goals. Each has its own pros and cons, with the best one for any given situation being determined by a combination of complexity and the number of components involved. The most widely applicable patterns involve using the
EventEmitter class in parent-child relationships to construct GreenSock timelines.
Angular provides the
EventEmitter class and the
@Output decorator to fire events from a component that can be picked up by its parent. In this way, we can have our parent react to the things happening in its children:
What we can’t do is assemble complex sequences involving multiple children at once. At least, not without breaking many of the rules we established above and hurting maintainability. Even overly complicated timing involving just one child can require moving too much logic where it doesn’t belong.
If we want more flexibility, we can make a relatively minor adjustment to our pattern. A basic
EventEmitter passes “empty” events containing no data, but the generic
EventEmitter<T> can pass any object up to the parent with the event. If we use an
EventEmitter<TimelineMax>, we can pass an animation timeline from one component up to its parent, where we can do more work with it:
Since we have the full timeline object in the parent, we can add things at any point in time, adjust easing and speed, or do anything else GreenSock supports with timelines.
There are other methods of sequencing I’ve come up with, but they are not well-suited to use-cases as simple as gsap-lights. Since I’m not a fan of unnecessary complexity, I’ll save those approaches for a separate post in the future.
I love working with both Angular and GreenSock, and I find that by using these patterns I can get them to work together instead of working against each other. This leads to cleaner, more maintainable code which in turn is more compatible with other frameworks or libraries one might wish to use.
I hope this article proves insightful for anyone who’s looking to use Angular and GreenSock together in their front-end.