Our Global Presence
Canada
57 Sherway St,
Stoney Creek, ON
L8J 0J3
India
606, Suvas Scala,
S P Ring Road, Nikol,
Ahmedabad 380049
USA
1131 Baycrest Drive,
Wesley Chapel,
FL 33544
Before we get started, let’s first understand what we are building. The goal is to create a Vue.js component to display an image, but it should only download that image when it is in the viewport. In other words, if we have a large list of images, only the ones the user actually needs to see will be downloaded. This will speed up our apps.
Here’s the goals:
<img>
tagsWe know we will need an <img>
tag, so let’s start there. For accessibility purposes, images should always have an alt attribute, even if it’s empty. We can easily pull the alt attribute from the $attrs
or fall back to an empty string.
<template> <img :alt="$attrs.alt || ''" /> </template>
Now, something we need to fix is that we don’t actually want to set the image src attribute right away because that would cause it to download immediately.
Instead, what we want is a tiny transparent PNG that we can use as a placeholder for the image src
. Using a placeholder instead of an empty value will cause the image to take up space on the page while it waits to download the real image.
For this to work properly, we need to hijack the src attribute by expecting it as a prop
. We also need to tap into the <img>
width and height attributes to create a tiny transparent PNG with an HTML canvas, and export it as a computed property called dataUrl
.
<template> <img :src="dataUrl" :alt="$attrs.alt || ''" /> </template> <script> export default { props: { src: { type: String, required: true, }, }, computed: { dataUrl() { const { width, height } = this.$attrs if (!width || !height) return "" // create a tiny png with matching aspect ratio as img const w = 100 const canvas = document.createElement("canvas") canvas.width = w canvas.height = (height / width) * w return canvas.toDataURL() }, }, } </script>
Alright, so we’ve got a transparent image when the component lands on the page. Now we need to figure out how to set our src attribute to the real image when the component is in view.
For that, we will hook into the mounted lifecyle event and use Intersection Observer. Before we get to that, however, we need to do a little refactor.
We need a <div>
to wrap our <img>
(This will make more sense later). Divs and images behave slightly different, so we also nee a little bit of style to make sure it still behaves like a native <img>
.
<template> <div class=" app-img "> <img :src="dataUrl" :alt="$attrs.alt || ''" v-bind="$attrs" /> </div> </template> <script> export default { inheritAttrs: false, props: { src: { type: String, required: true, }, }, computed: { dataUrl() { const { width, height } = this.$attrs if (!width || !height) return "" // create a tiny png with matching aspect ratio as img const w = 100 const canvas = document.createElement("canvas") canvas.width = w canvas.height = (height / width) * w return canvas.toDataURL() }, }, } </script> <style> .app-img { display: inline-block; } </style>
Any attributes it receives should be bound to our tag and not the <div>
. We’ll use v-bind
and $attrs
for the image, and tell the component not to add the default attributes to the root <div>
by setting inheritAttrs
to false.
we’re ready to implement the Intersection Observer for lazy-loading. we won’t go into too much detail on how Intersection Observer works, but in the mounted hook, we create an IntersectionObserver
that watches for the component (via $el
) to enter the viewport. When it does, we add an event handler to the image’s ‘load’ event, and assigns the image’s src attribute (which begins loading it). We also do a bit of cleanup work to the beforeDestroy
hook just in case.
<template> <div class=" app-img "> <img :src="dataUrl" :alt="$attrs.alt || ''" v-bind="$attrs" /> </div> </template> <script> export default { inheritAttrs: false, props: { src: { type: String, required: true, }, }, computed: { dataUrl() { const { width, height } = this.$attrs if (!width || !height) return "" // create a tiny png with matching aspect ratio as img const w = 100 const canvas = document.createElement("canvas") canvas.width = w canvas.height = (height / width) * w return canvas.toDataURL() }, }, mounted() { const { src, $el } = this const observer = new IntersectionObserver(([entry]) => { const img = $el.querySelector("img") if (entry.isIntersecting) { // Element is in viewport img.src = src observer.disconnect() } }) observer.observe($el) this.$once("hook:beforeDestroy", () => { observer.disconnect() }) }, } </script> <style> .app-img { display: inline-block; } </style>
Alright, our image starts on the page as a transparent PNG, and when it enters the viewport, it loads the real image. The next thing we need to make it better some placeholder while it waits to load.
We’ll add two props for the placeholder so it can either be a background color or a different (tiny) image. We’ll also add the placeholder as a <div>
which will be absolutely positioned over the entire component.
We need to know the size of the original image so that we can stretch our placeholder to the same size. If we don’t know the width and height (we’ll cheat and use the dataUrl
computed prop), we just won’t show the placeholder:
<template> <div class=" app-img "> <div v-if="dataUrl" :style="{ background }" class=" app-img__placeholder " > <img :src="placeholder || dataUrl" alt="" v-bind="$attrs" /> </div> <img :src="dataUrl" :alt="$attrs.alt || ''" v-bind="$attrs" class=" app-img__img " /> </div> </template> <script> export default { inheritAttrs: false, props: { src: { type: String, required: true, }, placeholder: String, background: String, }, computed: { dataUrl() { const { width, height } = this.$attrs if (!width || !height) return "" // create a tiny png with matching aspect ratio as img const w = 100 const canvas = document.createElement("canvas") canvas.width = w canvas.height = (height / width) * w return canvas.toDataURL() }, }, mounted() { const { src, $el } = this const observer = new IntersectionObserver(([entry]) => { const img = $el.querySelector(`.app-img__img`) const placeholder = $el.querySelector(`.app-img__placeholder`) img.onload = function() { delete img.onload if (placeholder) { placeholder.remove() } } if (entry.isIntersecting) { // Element is in viewport img.src = src observer.disconnect() } }) observer.observe($el) this.$once("hook:beforeDestroy", () => { observer.disconnect() }) }, } </script> <style> .app-img { display: inline-block; position: relative; } .app-img__placeholder { position: absolute; } </style>
A couple of other things to note are the in the intersectionObserver
. Once the main image has loaded, we want to remove the placeholder.
Theres a few more things we can do to make it a better experience.
We’ll accomplish most of this with CSS, but we do need one class added to the image once it has finished loading. And since we have a CSS transition, we also set a timeout to remove the placeholder. Any time we set a timeout, we also want to make sure we clear it when the component is destroyed.
For more Information and to build the website using Vue.js, Hire Vue.js Developer from us as we give you a high-quality product by utilizing all the latest tools and advanced technology. E-mail us any clock at – hello@hkinfosoft.com or Skype us: “hkinfosoft“.
To develop the custom website using Vue.js, please visit our technology page.
Content Source:
57 Sherway St,
Stoney Creek, ON
L8J 0J3
606, Suvas Scala,
S P Ring Road, Nikol,
Ahmedabad 380049
1131 Baycrest Drive,
Wesley Chapel,
FL 33544
57 Sherway St,
Stoney Creek, ON
L8J 0J3
606, Suvas Scala,
S P Ring Road, Nikol,
Ahmedabad 380049
1131 Baycrest Drive,
Wesley Chapel,
FL 33544
© 2024 — HK Infosoft. All Rights Reserved.
© 2024 — HK Infosoft. All Rights Reserved.
T&C | Privacy Policy | Sitemap