Building an Image Slider with Vue.js
Here is the demo and the final code on JSFiddle.
Update April 16th, 2018: this tutorial and the accompanying code have been brought up to date with more recent Vue.js updates.
An image slider is the quintessential use case for vue.js. It's a single, distinct component that requires data management, DOM updates, and user interaction. I recently needed a very simple image slider on a marketing page I was developing for a project built with vue.js. I rarely use jQuery any more, and since I was already using Vue for the rest of the project it seemed like a perfect opportunity to build one.
If you have never used Vue before, the official guide is quite good. I recommend understanding the basics before reading this tutorial. At the very least you should follow the installation instructions.
Our slider will be very simple. It will take an array of image URLs and cycle through them every 3 seconds. There will be previous/next controls which manually advance the images.
First we create our Vue object. We know we're going to need some images so we'll create an array of them in our object. We will also need a way to keep track of the active image, so we're going to create that variable too.
new Vue({
el: 'image-slider',
data: {
images: ['http://i.imgur.com/vYdoAKu.jpg', 'http://i.imgur.com/PUD9HQL.jpg', 'http://i.imgur.com/Lfv18Sb.jpg', 'http://i.imgur.com/tmVJtna.jpg', 'http://i.imgur.com/ZfFAkWZ.jpg'],
currentNumber: 0
}
});
The element is named image-slider
, which means in our HTML we can use the tag <image-slider></image-slider>
to insert the slider anywhere we want to show it. Let's go ahead and do that, and also show the current image.
Notice also the colon before the image's src
attribute. That is Vue shorthand notation for `v-bind:src`.
You can use v-bind
with almost any attribute. It tells Vue that the attribute value should not be read literally, but that its contents should be evaluated.
We are passing both the images
array and the currentNumber
variable to our component via its data object, making them available in the template element.
<body>
<image-slider>
<img :src="images[currentNumber]" />
</image-slider>
</body>
That's all that is needed to show the first image! Of course it doesn't do anything yet. We could allow the user to rotate through the images, but first we want to put them on a timer. Eventually we will want to stop the timer also, so we're going to add that now.
You expose functions to your component through the methods object. These are available in the object by using this.methodName()
, and in the template by simply calling methodName
in a bound attribute. Since we want the slider to start when the component is loaded, we hook into the Vue lifecycle's
mounted method to call trigger the rotation.
new Vue({
el: 'image-slider',
data: {
images: ['http://i.imgur.com/vYdoAKu.jpg', 'http://i.imgur.com/PUD9HQL.jpg', 'http://i.imgur.com/Lfv18Sb.jpg', 'http://i.imgur.com/tmVJtna.jpg', 'http://i.imgur.com/ZfFAkWZ.jpg'],
currentNumber: 0,
timer: null
},
mounted: function () {
this.startRotation();
},
methods: {
startRotation: function() {
this.timer = setInterval(this.next, 3000);
},
stopRotation: function() {
clearTimeout(this.timer);
this.timer = null;
},
next: function() {
this.currentNumber += 1
}
}
});
We used setInterval
to run a 3 second timer. Every interval the next()
method is called, which simply increments the currentNumber
. Without changing the HTML, you will now see the page cycling through the images.
When the slider hits the end of the images it breaks. We need a way to cycle back to the start. The most direct way to do this is to check in the next
method if the current number is larger than the length of the images array and set it back to zero if it is. If you've programmed for awhile you have probably discovered that the modulo operator is great for cycling. We're going to use it here, and also show an important Vue feature - that bound attributes are actually Javascript expressions.
<body>
<image-slider>
<img :src="images[currentNumber % images.length]" />
</image-slider>
</body>
We now have a fully functioning image slider with just a few lines of JavaScript and some extremely simple HTML. And the vue.js library, of course. It's extremely simple, yes, but it is also a great foundation for building more features. Right away I'm thinking it could use some sliding effects; it should pause when you hover over it with your mouse; and some users might want to click through the images. Let's tackle the user interaction first, then we'll add an effect to it.
There are already methods to start and stop the rotation, and to progress to the next slide. All that is needed to rotate to the previous slide is to duplicate the next
method, except decrease the current slide number.
...
next: function() {
this.currentNumber += 1
},
prev: function() {
this.currentNumber -= 1
}
}
});
Do you see any problems with this? Since we are using the modulo operator for cycling our images, we're going to run into problems with negative numbers. We'll fix that by using the absolute value of the current number. We will also now add controls for clicking through the slides, and go ahead and add our pause on hover.
<body>
<image-slider>
<p>
<a @click="prev">Previous</a> || <a @click="next">Next</a>
</p>
<img
:src="images[Math.abs(currentNumber) % images.length]"
v-on:mouseover="stopRotation"
v-on:mouseout="startRotation"
/>
</image-slider>
</body>
Notice that I use @click
in the links, and v-on:mouseout
for the image. These are the same thing! The @
notation is shorthand for v-on
, similar to how the :
notation is shorthand for v-bind
.
Our image src
is starting to get a little unwieldy. We could move it to a component method, but it is a prime candidate for a computed property. The main difference between a method and a computed property is that computed properties are cached until their dependencies change (in this case, the images
array and the currentNumber
property. It is good to get in the habit of using computed properties instead of methods whenever possible, especially as your application grows in complexity. It looks just like a method.
...
computed:
currentImage: function() {
return this.images[Math.abs(this.currentNumber) % this.images.length];
}
}
});
There is also an issue with the slider timing when clicking next/prev. Since we don't reset the timer, sometimes it jumps immediately after clicking one of the controls. The simplest fix for this is to stop and start the rotation every time next/prev is called. At that point, we are not really using setInterval
, and it would be simpler to use setTimeout
since we then wouldn't have to stop the interval in the next
and prev
methods. I'll leave this decision to the reader.
Finally we want to add a small slide effect to the rotator. Vue already provides a transition system that does most of the complicated work for us. We just need to hook into it and add a little bit of CSS. Most of the details of how the transition system, and the CSS transitions work are already documented, so I won't go into a lot of detail here.
This transition does involve a little bit of trickery... I mean, clever coding. Other solutions may be more straight forward, but also less concise.
First we setup the CSS.
.fade-enter-active, .fade-leave-active {
transition: all 0.8s ease;
overflow: hidden;
visibility: visible;
opacity: 1;
position: absolute;
}
.fade-enter, .fade-leave-to {
opacity: 0;
visibility: hidden;
}
Within the component we will call our transition fade
, so Vue has us declare four classes which it will apply/remove based on the status of the transition.
This is the new HTML required.
<body>
<image-slider>
<p>
<a @click="prev">Previous</a> || <a @click="next">Next</a>
</p>
<transition-group name='fade' tag='div'>
<div
v-for="number in [currentNumber]"
:key='number'
>
<img
:src="currentImage"
v-on:mouseover="stopRotation"
v-on:mouseout="startRotation"
/>
</div>
</transition-group>
</image-slider>
</body>
Vue only runs transitions when an element is being inserted/removed, or shown/hidden. In our previous code we were not inserting a new picture to the DOM, but changing the src
attribute for the same img
element. Now we are telling Vue to cycle through an array - always containing a single value, set to the currentNumber
value. When currentNumber
changes Vue detects that change and redraws the v-for
loop. We're basically tricking Vue into removing the old img
element, and replacing it with the new. Since the fade transition is applied, and the divs are absolutely positioned, one image fades out while the next fades in.
Vue uses the transition-group
element for adding/inserting, or moving list elements. Don't forget to set the :key='number'
to your loop or it will not work with transitions.
Here is the demo and the final code on JSFiddle.