Click Images and Show Them in Full Size — a Reusable Solution in Vue

Mar 18 2019 | By Taha Shashtari

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 }"
>

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"
>

How would expanding work?

Let's explain this in steps:

  1. The user clicks on the image
  2. The expanded property becomes true
  3. The expanded watcher would run with expanded set to true (we'll implement that watcher later)
  4. We'll check if expanded is true. If it is, we'll do the following
  5. Clone the root element of this component and store it into a property named cloned
  6. Store a reference for the close button and add a click event listener to it
  7. Add the cloned element to the <body>
  8. Disable scrolling of the page
  9. 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:

  1. The user clicks on the close button
  2. We hide the expanded image with transition
  3. After that's done, we remove the click event listener from the close button
  4. Remove the cloned element from the <body>
  5. 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.