Creating A Simple Slide-Show With Dynamic Keyframe Animations In Angular 10.0.9
The other day at InVision, I came across a View within our AngularJS 1.2.22 SPA (Single-Page Application) that was using a 1,000-line jQuery plug-in to power the most basic of horizontal carousels. Upon further investigation, this was the only UI (User Interface) that was using this jQuery plug-in; so, I ripped it out and replaced it with a 10-keyframe CSS animation. In the AngularJS app, I hand-coded these keyframe animation; but, it got me thinking about whether or not keyframe animations could be more dynamic. To explore this idea, I tried to create a "simple slide-show" directive in Angular 10.0.9.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
When I first approached this problem, I was hoping that I could just insert a <style>
tag with dynamic content right in my Angular view. Unfortunately, this does not work. Angular strips-out the embedded <style>
tag, moves it up to the document head, and consumes it as part of the core component-styling functionality. Any attempt to use "variable interpolation" or dynamic binding within the style tag is ignored (or "escaped" more accurately).
My next idea was to encapsulate the keyframe logic in a directive. To keep things as simple as possible, this "simple slide-show" directive is an "attribute directive" that takes a static object as its value. This object is the configuration for the slide-show and must include the following properties:
count
- The number of slides in the slide-show.pause
- The duration (in milliseconds) that each slide show remain in-view.transition
- The duration (in milliseconds) that it should take to transition from one slide to next.
These three properties are used to dynamically generate an @keyframes
style block and inject it into the document head for the duration of the directives life-cycle. All other styling is deferred to the calling context.
Before we dive into the simple slide-show directive, let's look at how it is being consumed. Here's my App component, which will toggle the directive into and out of existence (so I can see that the dynamic <style>
tag is working):
<p>
<a (click)="( this.isShowingSlideshow = ! this.isShowingSlideshow )">
Toggle Slide-Show
</a>
</p>
<div *ngIf="isShowingSlideshow" class="slideshow">
<!--
The "simple slider" directive attaches a dynamic keyframe animation to the host
element (UL), translating the host over by -100% during each "transition". It
defers all of the styling to the calling context, handling only the animation.
-->
<ul
[simpleSlider]="{
count: 5,
pause: 2000,
transition: 450
}">
<li> Slide One </li>
<li> Slide Two </li>
<li> Slide Three </li>
<li> Slide Four </li>
<li> Slide Five </li>
</ul>
</div>
The simple slide-show directive doesn't define any styles outside of the animation. As such, it defers to the calling context, assuming that the horizontal layout will "just work". In this demo, here's my App component's LESS CSS file - you can see that .slideshow
class works with the ul
and li
to setup a CSS Flexbox layout:
:host {
display: block ;
font-size: 18px ;
}
a {
color: red ;
cursor: pointer ;
text-decoration: underline ;
}
.slideshow {
height: 200px ;
margin: 20px 0px 20px 0px ;
overflow: hidden ;
width: 400px ;
ul {
display: flex ;
height: 100% ;
list-style-type: none ;
margin: 0px 0px 0px 0px ;
padding: 0px 0px 0px 0px ;
width: 100% ;
}
li {
align-items: center ;
border: 2px solid black ;
box-sizing: border-box ;
display: flex ;
flex: 0 0 auto ;
height: 100% ;
justify-content: center ;
margin: 0px 0px 0px 0px ;
padding: 0px 0px 0px 0px ;
width: 100% ;
&:nth-child( even ) {
background-color: #f0f0f0 ;
}
}
}
There's nothing in this CSS that is too surprising. The ul
tag overrides the normal "list" styles, instead laying-out its li
children in a horizontal flex-layout. There's nothing in here about CSS animations or keyframes. And yet, when we run this Angular 10 code in the browser, we get the following output:
As you can see, it's a super simple slide-show. No frills, no real functionality: just one slide after another at a steady pace.
This simple slide-show directive works by generating a dynamic set of keyframes in which two sibling keyframes work to keep the slide either "in view" or "in transition". To understand what I mean, let's look at the <style>
tag that our directive injects into the document:
Notice that each pair of keyframe percentages is about 16% apart. And, each set of pairs is about 4% apart. The 16% is the percentage of the animation-duration
property that is portioned out for any given slide viewing. Similar, the 4% is the percentage of the animation-duration
property that is portioned out for any given transition.
Of course, our directive's input bindings don't mention percentages at all. Instead, the directive takes human-friendly durations (in milliseconds). The directive then translates those durations into keyframe mile-markers (percentages).
With all that said, here's my "simple slide-show" directive:
// Import the core angular services.
import { Directive } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// CAUTION: Animation names / key-frames cannot be scoped to a component. As such, each
// animation name must be globally-unique to the rendered page. We're going to use an
// incrementing key to make sure all simple-slider animations are uniquely named.
var incrementingID = 0;
interface SlideshowConfig {
count: number ;
pause: number;
transition: number;
}
@Directive({
selector: "[simpleSlider]",
inputs: [ "config: simpleSlider" ],
host: {
"[attr.data-simple-slider-id]": "id",
"[class.simple-slider-directive]": "true"
}
})
export class SimpleSliderDirective {
public config!: SlideshowConfig;
public id: string;
private styleElement: HTMLStyleElement | null;
// I initialize the child component.
constructor() {
this.id = `simple-slider-${ ++incrementingID }`;
this.styleElement = null;
}
// ---
// PUBLIC METHODS.
// ---
// I get called once when the component is being unmounted.
public ngOnDestroy() : void {
// CAUTION: The ngOnInit() is NOT ALWAYS CALLED prior to the host being
// destroyed. As such, we have to check to see if our style-element was
// ever created.
if ( this.styleElement ) {
document.head.removeChild( this.styleElement );
}
}
// I get called once after the inputs have been bound for the first time.
public ngOnInit() : void {
// Technically, the inputs can be bound more than once. However, for the sake of
// simplicity, we're going to check the inputs just once and then use them to
// generate static key-frames for our animation.
var count = this.config.count;
var pause = this.config.pause; // In milliseconds.
var transition = this.config.transition; // In milliseconds.
// The key-frames within an animation are defined as percentages, not times. As
// such, we have to calculate the total time so that we can then figure out what
// percentage of the total time each slide and transition will account for.
var total = ( ( count * pause ) + ( count * transition ) );
var percentPause = ( pause / total * 100 );
var percentTransition = ( transition / total * 100 );
// Now that we have the timings translated into percentages, we have to build-up
// the key-frame definitions.
var keyframes: string[] = [];
for ( var i = 0 ; i < count ; i++ ) {
// For each key-frame, the FROM and TO will represent the starting and ending
// percentage of each slide. Then, the difference between each subsequent
// key-frame percentage will account for the transition timing between slides.
var from = ( ( percentPause * i ) + ( percentTransition * i ) );
var to = ( from + percentPause );
// Each key-frame will move the contents left by 100% of the container width
// (since each slide is assumed to take up 100% of the container dimensions).
var offset = ( i * -100 );
keyframes.push(
`
${ from }% , ${ to }% {
transform: translateX( ${ offset }% ) ;
}
`
);
}
// Create a Style Tag with the dynamic key-frames as the tag-content. We're going
// to target the current element based on the CLASS NAME and the DATA attribute
// that has been configured in the Host() bindings.
this.styleElement = document.createElement( "style" );
this.styleElement.type = "text/css";
this.styleElement.textContent =
`
@keyframes ${ this.id }-keyframes {
${ keyframes.join( "" ) }
}
.simple-slider-directive[ data-simple-slider-id = '${ this.id }' ] {
animation-duration: ${ total }ms ;
animation-iteration-count: infinite ;
animation-name: ${ this.id }-keyframes ;
}
`;
document.head.appendChild( this.styleElement );
}
}
Setting up the math may not be clear - it took me a bit of trial-and-error to get it working. But, it's basically just a for
-loop that iterates over the slides and keeps a running-tally (so to speak) of how the ms-durations map to keyframe percentages. Then, once the keyframes have been defined, the directives creates a <style>
tag, sets its textContent
, and injects it into the document.
One of the frustrating parts of CSS keyframe animations in Angular is that they don't adhere to the "scopes CSS" rules. As such, each @keyframe
identifier has to be globally-unique within the rendered document. To work within these constraints, I am keeping an internal counter that is used to name each unique animation.
To be clear, there are several other more official ways to perform animations in Angular. This was just a fun experiment to see how I might be able to use dynamic keyframes to power some simple animations. I don't use @keyframes
all that much (mostly due to my lack-of-skills); so, if nothing else, this was a chance for me to try and build a better mental model of how CSS keyframe animations work.
Want to use code from this post? Check out the license.
Reader Comments
@All,
In the video demo for this post, I think I mentioned that the
animation-timing-function
is applied to the entire animations. As it turns out, that assumption was wrong. As I just learned from Una Kravets on the CSS Podcast, theanimation-timing-function
is actually applied on a per-keyframe basis:www.bennadel.com/blog/3885-animation-timing-functions-get-applied-per-keyframe-in-css.htm
This definitely changes the perspective on what you can get with a dynamic keyframe animation.