Last active
October 7, 2023 12:05
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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