Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Brad Wood
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Brad Wood@bdw429s )

Creating A Pandora Radio Station List Animation In Vue.js 2.5.21

By Ben Nadel on

In the morning, when I'm doing my Research & Development, I tend to listen to a Pandora radio station based on the song, Say My Name by Odesza. Then, when it comes to doing work, I switch over to either classical music or Gregorian chant. The other morning, while making this switch, I started to think about the way in which Pandora's "recent stations" list animates the newly-selected station to the top of the list. It occurred to me that I didn't know how to build an animation like this; so, I wanted to give it a go.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.


 
 
 

 
 Pandora Radio station list animation. 
 
 
 

My first intention was to build this animation in Angular 7.1.4. So, I started with an *ngFor loop and attempted to apply some sort of ":increment" and ":decrement" transitions to the list items. But, as I quickly found out, there are several open GitHub issues regarding *ngFor loops and animations. In fact, I couldn't even get basic states to apply properly inside of an *ngFor loop.

So, I decided to scrap the idea in Angular and try building it in Vue 2.5.21. And, after reading through the Vue.js documentation on Animations and Transitions, it appeared that Vue.js natively supported exactly this kind of list animation. In fact, it looked like it would only take 3-lines of code.

It turns out, this entire post is somewhat moot because this kind of animation is demonstrated right there in the Vue.js documentation. It's the "move" animation for lists. And, all I had to do, more or less, was provide the duration of the animation.

That said, I already have the code, so I might as well share it. In the following demo, I have a list of Arnold Schwarzenegger movies. And, within each list item there is an option to move the item "up", "down", "top", or "bottom". When the ranking of the movie is changed, the movie will slide up or down to the new location.

What you'll see is that the bulk of this code is just calculating the new index of the selected item in the collection. I'm barely doing anything with animation: I'm just wrapping the items in a transition-group component and I'm applying a little CSS to make sure the selected item has the highest z-index:

  • <style scoped src="./app.component.less" />
  • <style scoped lang="less">
  •  
  • .movie {
  • // Needed to get the z-index to change the stack-order during animation.
  • position: relative ;
  •  
  • // This is the CSS animation class added by Vue.js during animation life-cycle
  • // (based on the 'name' property of the transition-group element).
  • &-move {
  • transition-duration: 400ms ;
  • transition-timing-function: ease-in-out ;
  • z-index: 2 ;
  • }
  •  
  • // Since all moving elements have z-index of 2, the stack-order will follow the
  • // natural order of elements in the HTML. However, we want the "target" movie to
  • // always be above all other elements, regardless of which direction its moving.
  • // As such, we'll bump the selected movie up one more level.
  • &--selected {
  • z-index: 3 ;
  • }
  • }
  •  
  • </style>
  •  
  • <template>
  •  
  • <div class="app">
  •  
  • <!-- NOTE: Transition-Group will render our UL element. -->
  • <transition-group name="movie" tag="ul" class="movies">
  • <li
  • v-for="movie in movies"
  • :key="movie.id"
  • class="movie"
  • :class="{ 'movie--selected': ( movie === selectedMovie ) }">
  •  
  • <div class="title">
  • <a :href="movie.imdbUrl" target="_blank" class="link">
  • {{ movie.title }}
  • </a>
  • <span class="release-date">
  • ( {{ movie.releasedAt }} )
  • </span>
  • </div>
  •  
  • <div class="tools">
  • <div class="tools__group">
  • <a @click="move( movie, 'up' )" class="move-up">up</a>
  • <a @click="move( movie, 'top' )" class="move-top">top</a>
  • </div>
  • <div class="tools__group">
  • <a @click="move( movie, 'down' )" class="move-down">down</a>
  • <a @click="move( movie, 'bottom' )" class="move-bottom">bottom</a>
  • </div>
  • </div>
  •  
  • </li>
  • </transition-group>
  •  
  • </div>
  •  
  • </template>
  •  
  • <script>
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // Import application classes.
  • import movies from "./data";
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • export default Vue.extend({
  • data() {
  •  
  • return({
  • movies: movies,
  • selectedMovie: null
  • });
  •  
  • },
  • methods: {
  • // I move the given movie to the given destination in the list.
  • move( movie, destination ) {
  •  
  • var index = this.movies.indexOf( movie );
  •  
  • // Calculate the next index based on the target location.
  • switch ( destination ) {
  • case "up":
  • var nextIndex = Math.max( 0, ( index - 1 ) );
  • break;
  • case "top":
  • var nextIndex = 0;
  • break;
  • case "down":
  • var nextIndex = Math.min( this.movies.length, ( index + 1 ) );
  • break;
  • case "bottom":
  • var nextIndex = this.movies.length;
  • break;
  • }
  •  
  • // Splice the selected movie out of the collection and then insert it
  • // back into the collection at the target location.
  • this.movies.splice( index, 1 );
  • this.movies.splice( nextIndex, 0, movie );
  •  
  • // Most of the animation can be handled directly in the HTML / CSS.
  • // However, in order for the movie-in-question to be visually above all
  • // the other movies (in the stack-order) during the animation, we have to
  • // explicitly track the movie that is being ordered.
  • // --
  • // NOTE: This selection will persist beyond the end of the animation.
  • // But, this has no down-side, so don't sweat it.
  • this.selectedMovie = movie;
  •  
  • }
  • }
  • });
  •  
  • </script>

As described in the documentation, Vue.js uses the FLIP (First, Last, Invert, Play) technique to animate items in a list. As such, the only think I really have to do is provide the duration of the transition that powers the FLIP animation. I do this with the ".movie-move" class that Vue.js automatically applies to each transitioning element in the list.

Now, if we run this in the browser and start moving some of the movies around in the list, we get the following output:


 
 
 

 
 Performing list move animations in Vue.js 2.5.21 based on Pandora Radio Station list animation. 
 
 
 

Holy cow! It "just works"! How crazy is that? I really didn't do anything in this post except shuffle some items around in an array. The rest of the functionality is just built directly into Vue.js 2.5.21.

I have to say, on first glance, the Animation API in Vue.js look kind of amazing. Not only does it have animation like this supported natively, it also provides a hooks API such that some of the animation modes can be data-driven. In contrast, the Angular animations API is vastly more complicated and rather buggy.



Reader Comments

@All,

After seeing how powerful the "move" transition was, I wanted to try something a bit more complicated: performing the "enter" animation from the x,y coordinates of a user's mouse-click event:

https://www.bennadel.com/blog/3559-animating-elements-in-from-a-mouse-event-location-in-vue-js-2-5-21.htm

The beauty of this experiment is that it demonstrates that both CSS and JavaScript hooks can work hand-in-hand to power dynamic, data-driven animations!

Reply to this Comment

@Chris,

Thank you good sir. It's funny, when I saw "transition" groups in React, I was totally confused by them. But, somehow, when I saw them in Vue.js, I just went with it (without trying to resist). Seems like it has some cool stuff.

I hear lots of good things about the react-beautiful-dnd library!

Reply to this Comment

Ben. This looks like magic! I have very little knowledge of Vue, but I really don't understand how you can change the vertical position of an element, by changing the z-index? And also what does the '-move' identifier do, in the CSS? Baffled!

Reply to this Comment

OK. I think I am beginning to wrap my head around this. It is like one of those hologram pictures. The more you look at the code, the more is revealed! It looks like the movement is based on the order of the items in the array. So something behind the scenes is listening for changes in this order & animating the movement from current to target. It seems that 'Vue' is doing most of the hard work by calculating the position change & then the CSS takes over and animates it via a transition.

Can I ask what the '&- -selected' means? Is this SASS/SCSS or CSS? And why do you want the target element to be above all others in the 'z-index' stacking order. In fact, what has the 'z-index' got to do with any of this?

Reply to this Comment

@Charles,

You are correct, in that Vue is basically doing all the hard work for you. In the documentation, it states that the "move" animation is performed using FLIP:

  • F (first)
  • L (last)
  • I (invert)
  • P (play)

The "First" and "Last" portions are automatically calculating the current and final positions automatically. As such, it's just up to me (as the developer) to provide the duration for the move and any additional CSS transformations.

The --selected stuff is just "BEM" (Block Element Modifier) inspired CSS notation - nothing special. But, using &--selected is applying that BEM notation to the CSS using LESS (as you guessed).

And, as far as the z-index of 3, this gets around the natural stacking order of the DOM. So, consider that you have several list elements:

  • A
  • B
  • C

... and they all have z-index : 2. If they overlap, the browser still needs to decide which ones are higher or lower, despite the fact that they all have the same z-index. To do this, is uses the order of the elements in the DOM. The lower the sibling element in the DOM, the higher the implicit stacking order. So, C will stack visually over B if they overlap since C is lower in the DOM.

What this means is that I try to animate C from the 3rd position to the 1st position, it will suddenly have a lower stacking order than A or B. As such, when an element is selected for animation, I bump its z-index up to 3 in order to ensure that it visually stacks above all other sibling items in the list.

Reply to this Comment

Great answer. Thanks! The FLIP thing certainly makes the animation mechanics a lot easier than Angular, although I did enjoy working this stuff out manually, in my demo!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.