Whether you’re building a simple blog or an e-commerce, you might need a way to view images in window’s full size when clicking on them.
While it might look like a not-so-difficult task, it’s a little bit tricky to create a reusable solution for it in Vue. The reason for that is because you should be able to use this solution on any image no matter how deep it’s nested in the DOM tree.
If you just want to use this solution right away without following the tutorial, check out this plugin I created.
Before moving onto the next section, take a look at how the end result would look like in CodePen:
How the reusable solution would look like?
It should be as easy as replacing <img>
tags with <expandable-image/>
component!
The Main Page
Let’s quickly start a new project with Vue CLI 3’s Instant Protoyping. So, create App.vue
and ExpandableImage.vue
in a new directory, then run this from the terminal:
vue serve App.vue
Then, put this into App.vue
:
<template>
<div id="app">
<expandable-image
class="image"
src="https://images.unsplash.com/photo-1550948537-130a1ce83314?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2552&q=80"
alt="dog"
title="dog"
/>
</div>
</template>
<script>
import ExpandableImage from './ExpandableImage'
export default {
components: { ExpandableImage }
}
</script>
<style>
#app {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 600px;
max-width: 100%;
margin: 50px auto;
position: relative;
}
.image {
width: 400px;
max-width: 100%;
}
</style>
We’re just displaying an image with <expandable-image/>
, which we haven’t created yet.
Implementing ExpandableImage.vue
First of all, we want this component to be a wrapper for an <img>
element, to make sure it’s treated as a normal image element.
To do that, we need to pass all attributes used on <expandable-image/>
directly to <img>
using $attrs. Here’s how:
<template>
<div class="expandable-image">
<img v-bind="$attrs" />
</div>
</template>
Now whatever (except for style
and class
) you use on <expandable-image/>
would be passed to <img>
. But if you check the browser, you would see that not only <img>
is using those attributes, but also the wrapper component: <div class="expandable-image">
. To fix that, add inheritAttrs: false
to the component’s root object. Like this:
<script>
export default {
inheritAttrs: false
}
</script>
Now we need a way to tell if the image is in the expanded state or not. So, let’s add expanded
to the component’s data list.
data () {
return {
expanded: false
}
}
We need to use that property to style the component differently. So, add :class
to the root element, like this:
<div class="expandable-image" :class="{ expanded }"></div>
Also, based on that property, we’ll either display the expand icon or the close icon (check the demo if you don’t know what I’m talking about).
Let’s modify the template section to include those icons.
<template>
<div class="expandable-image" :class="{ expanded }">
<i v-if="expanded" class="close-button">
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path
fill="#666666"
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
/>
</svg>
</i>
<i v-else class="expand-button">
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path
fill="#000000"
d="M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z"
/>
</svg>
</i>
<img v-bind="$attrs" />
</div>
</template>
Before we dive into how this would work, let’s toggle the expanded
property to true
when the user clicks on the image. We’ll do that by adding @click
to the root element, like this:
<div
class="expandable-image"
:class="{ expanded }"
@click="expanded = true"
></div>
How would expanding work?
Let’s explain this in steps:
- The user clicks on the image
- The
expanded
property becomestrue
- The
expanded
watcher would run withexpanded
set totrue
(we’ll implement that watcher later) - We’ll check if
expanded
istrue
. If it is, we’ll do the following - Clone the root element of this component and store it into a property named
cloned
- Store a reference for the close button and add a
click
event listener to it - Add the cloned element to the
<body>
- Disable scrolling of the page
- Finally, show the expanded image with a fading transition
These steps are for showing the expanded image (they should become clearer when you see the code).
Now, let’s see the steps for closing the expanded image:
- The user clicks on the close button
- We hide the expanded image with transition
- After that’s done, we remove the click event listener from the close button
- Remove the cloned element from the
<body>
- Finally, re-enable the scrolling of the page
The expanded watcher
Let’s see how the expanded
watcher looks in the code.
watch: {
expanded (expanded) {
this.$nextTick(() => {
// Run this if when we're expanding the image
if (expanded) {
// Clone the entire expandable-image element
this.cloned = this.$el.cloneNode(true)
// Store a reference for the close button
this.closeButtonRef = this.cloned.querySelector('.close-button')
// Call closeImage when the close button is clicked
this.closeButtonRef.addEventListener('click', this.closeImage)
// Add the cloned element into <body>
document.body.appendChild(this.cloned)
// Prevent the page from scrolling
document.body.style.overflow = 'hidden'
setTimeout(() => {
// Show the cloned element
this.cloned.style.opacity = 1
}, 0)
} else {
// This section will run when the image is closing
// Hide the expanded image
this.cloned.style.opacity = 0
setTimeout(() => {
// Then, remove the click event listener from the close button
this.closeButtonRef.removeEventListener('click', this.closeImage)
// Remove the cloned element and the references
this.cloned.remove()
this.cloned = null
this.closeButtonRef = null
// Re-enable the scrolling
document.body.style.overflow = 'auto'
}, 250)
}
})
}
}
Add styling for the component
Our last step is to add the styling for the component. This step is very important to make our component function correctly, so you might want to read and understand the css of this component.
<style>
.expandable-image {
position: relative;
transition: 0.25s opacity;
cursor: zoom-in;
}
body > .expandable-image.expanded {
position: fixed;
z-index: 999999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: black;
display: flex;
align-items: center;
opacity: 0;
padding-bottom: 0 !important;
cursor: default;
}
body > .expandable-image.expanded > img {
width: 100%;
max-width: 1200px;
max-height: 100%;
object-fit: contain;
margin: 0 auto;
}
body > .expandable-image.expanded > .close-button {
display: block;
}
.close-button {
position: fixed;
top: 10px;
right: 10px;
display: none;
cursor: pointer;
}
svg {
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.5));
}
svg path {
fill: #FFF;
}
.expand-button {
position: absolute;
z-index: 999;
right: 10px;
top: 10px;
padding: 0px;
align-items: center;
justify-content: center;
padding: 3px;
opacity: 0;
transition: 0.2s opacity;
}
.expandable-image:hover .expand-button {
opacity: 1;
}
.expand-button svg {
width: 20px;
height: 20px;
}
.expand-button path {
fill: #FFF;
}
.expandable-image img {
width: 100%;
}
</style>
I think most of these should be easy to understand if you know css, but one important note to make, though, is that we’re using this selector: body > .expandable-image.expanded
to target the expanded image when displayed in the <body>
.
We’re done! Now, whenever you want to allow your images to be viewed in full size, just use <expandable-image/>
instead of <img>
.
Also, don’t forget that you can save yourself all of this work and use vue-expandable-image instead. But it’s still good to learn how to implement it from scratch.