Interactive, Ordered, Animated List in Vue.js with Vuetify.js and Nuxt.js

A short tutorial about building an interactive list component as a simple alternative to drag & drop.

Photo by Kolar.io on Unsplash

TL;DR

Here’s the component in action:

And here’s the final code on Codesandbox:

Introduction

There are many situations where it’s necessary to ask the user to enter a list of items, for example their social profiles, postal addresses, contacts, etc. Implementing a list supporting just add and remove operations is relatively easy. When it comes to changing the order of the items, Drag & Drop seems like an obvious solution. However, unfortunately D&D is not officially supported by Vuetify.js and custom-made solutions can be tedious to develop, are not reliable in all possible situations and they require additional maintenance. To overcome these challenges, I would like to present you with a simple trick that works great if the number of items on the list is small.

Static List

Let’s begin with a static list.

To clearly separate the items, each one will be wrapped in a card component. Here’s the CardList component:

<v-card v-for="(item, index) in value" :key="index" outlined class="mt-3">
<v-card-text>
<slot :item="item" :index="index" />
</v-card-text>
</v-card>

It can be used as follows:

<card-list v-model="items" #default="{ item }">
<v-row>
<v-col cols="12">
<v-text-field v-model="item.name" label="Name" hide-details />
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-text-field v-model="item.email" label="Email" hide-details />
</v-col>
</v-row>
</card-list>

Add and Remove Items

Next, let’s add the possibility to add and remove items:

Here’s the remove item code, I have skipped the add item code for brevity:

<v-card v-for=...>
<v-card-title class="justify-end pb-0">
<v-btn @click="remove(index)" icon>
<v-icon>
mdi-close
</v-icon>
</v-btn>
</v-card-title>
...
</v-card>
...
methods: {
remove (index) {
const newValue = [...this.value.slice(0, index), ...this.value.slice(index + 1)]
this.$emit('input', newValue)
}
}

Reorder Items

Finally, let’s implement the list reordering. We will add two buttons on each item, one to swap it with the top item and one to swap it with the bottom one:

<v-card-title ...>
<v-btn :disabled="index + 1 >= value.length" @click="down(index)" icon>
<v-icon>
mdi-arrow-down
</v-icon>
</v-btn>
<v-btn :disabled="index === 0" @click="up(index)" icon>
<v-icon>
mdi-arrow-up
</v-icon>
</v-btn>
</v-card-title>
...
methods: {
...
up (index) {
const newValue = [...this.value]
newValue[index] = this.value[index - 1]
newValue[index - 1] = this.value[index]
this.$emit('input', newValue)
},
down (index) {
const newValue = [...this.value]
newValue[index] = this.value[index + 1]
newValue[index + 1] = this.value[index]
this.$emit('input', newValue)
}
}

When you try to run the program now you will be disappointed — it won’t work. You need to provide a unique and stable key property, so Vue.js knows which item is which, even after they change order (see documentation):

<v-card v-for="(item, index) in value" :key="item[itemId]" outlined class="mt-3">props: {
...
itemId: {
type: String,
default: 'id'
}
}

After adding the id field to each newly created item it works as expected:

add () {
this.items.push({ id: this.counter++ })
}

Transition

However, you will quickly notice that the reorder action is not very obvious to the end-user. After clicking the arrow button they may be thinking: Did anything change? Did I really click the button? Should I click it again?:

To make it clearer we will use transitions (see documentation)— a perfect tool to make user interactions explicit. First let’s add an appear / hide effect when adding / removing an item.

Since we are dealing with multiple components, we will use <transition-group> built-in component (see reference):

<transition-group name="list" tag="div">
<v-card v-for=...>
</v-card>
</transition-group>
...
.list-enter, .list-leave-to {
opacity: 0;
}
.list-enter-active, .list-leave-active {
transition: opacity 0.5s ease;
}

Then, let’s use a move transition when reordering items:

.list-move {
transition: transform 0.5s ease-out;
}

Conclusion

Voilà! I call the solution a poor-man’s Drag & Drop, since it’s not as intuitive and fast to use, but does the job perfectly well if the number of list elements is limited. Deciding whether to use it is like choosing between bubblesort and quicksort — both have their optimal uses.

You can find a complete code example in the following repository:

Full-Stack Developer / Agilist / Free and Open Source Software Fan.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store