Skip to content

Instantly share code, notes, and snippets.

@jd1378
Last active October 7, 2023 12:05
Show Gist options
  • Save jd1378/4e9a968f746e53eaca4ef00a3e9f0328 to your computer and use it in GitHub Desktop.
Save jd1378/4e9a968f746e53eaca4ef00a3e9f0328 to your computer and use it in GitHub Desktop.
a lazy <img/> loader component for modern browsers with common requirements for Vue 3
/**
* Author: @jd1378 (https://github.com/jd1378)
* License: MIT
*/
<template>
<div class="relative" :class="wrapperClass">
<div
class="pointer-events-none absolute bottom-0 left-0 right-0 top-0 flex select-none items-center justify-center">
<slot v-if="loading" name="loading">
</slot>
<slot v-else-if="errored" name="errored">
</slot>
<slot v-else-if="isEmpty" name="empty">
</slot>
<slot name="state"></slot>
</div>
<img
ref="img"
decoding="async"
class="relative"
:class="[
(hideTillLoaded && loading) || isEmpty ? 'opacity-0' : undefined,
]"
:src="emptySvg"
loading="lazy"
v-bind="$attrs"
:data-src="src"
:title="errored ? 'loading' : undefined"
:alt="alt ? alt : isEmpty ? 'no image' : undefined"
@error="handleError"
@load="handleLoad"
@click="handleImgClick" />
<slot></slot>
</div>
</template>
<script lang="ts">
const emptySvg =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='50' height='50' /%3E";
/**
* Note about why we are not using native lazy loading:
* Initially, it was using lazy="loading" attribute, while in theory it seems great,
* It was delaying load of essential files (js, css, etc) for website to work.
* so we switched to the old fashion way of using data-src and setting src to it when component is mounted.
* we still use the lazy="loading" attribute, but not alone anymore. it still helps with deferring the
* load of images that are not in view yet.
*/
export default {
inheritAttrs: false,
props: {
src: {
type: String,
required: false,
default: undefined,
},
wrapperClass: {
type: [String, Object, Array],
default: undefined,
},
hideTillLoaded: {
type: Boolean,
default: false,
},
retry: {
type: Number,
default: 1,
},
alt: {
type: String,
required: false,
default: undefined,
},
},
setup(props) {
const loading = ref(true);
const errored = ref(false);
const isEmpty = computed(() => !props.src);
const img = ref<HTMLImageElement | null>(null);
const retryCount = ref(0);
function replaceWithSvg() {
if (img.value) {
img.value.src = emptySvg;
}
}
function init() {
img.value?.removeAttribute('src');
loading.value = true;
errored.value = false;
if (props.src && img.value) {
img.value.src = props.src;
} else {
handleLoad();
replaceWithSvg();
}
}
function handleLoad() {
loading.value = false;
}
function handleError() {
if (retryCount.value < props.retry) {
retryCount.value++;
init();
} else {
errored.value = true;
loading.value = false;
replaceWithSvg();
}
}
onMounted(init);
function handleImgClick() {
if (errored.value && img.value && props.src) {
errored.value = false;
loading.value = true;
img.value.src = props.src;
}
}
watch(() => props.src, init);
return {
loading,
errored,
isEmpty,
handleLoad,
handleError,
handleImgClick,
img,
emptySvg,
};
},
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment