Created
January 7, 2022 16:40
-
-
Save MartinMalinda/12b8cb0c4143c1ae1c961cb397f46c15 to your computer and use it in GitHub Desktop.
Lazy Image Component
This file contains 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
<script lang="ts"> | |
import { defineComponent, computed, ref } from '@vue/composition-api'; | |
import Intersect from 'vue-intersect'; | |
import { oneOf } from '~/utils/props'; | |
import { | |
responsifyImageSource, | |
responsifyImageSourceSet, | |
ImageSizeOption, | |
ImageParam, | |
} from '~/utils/img'; | |
import { useControlStore } from '~/pinia/control'; | |
enum Variant { | |
small = 'small', | |
medium = 'medium', | |
wide = 'wide', | |
big = 'big', | |
} | |
const sizes = { | |
[Variant.small]: { | |
width: 50, | |
height: 50, | |
}, | |
[Variant.medium]: { | |
width: 250, | |
height: 250, | |
}, | |
[Variant.wide]: { | |
width: 383, | |
height: 255, | |
}, | |
[Variant.big]: { | |
width: 500, | |
height: 500, | |
}, | |
}; | |
export default defineComponent({ | |
props: { | |
src: { | |
required: true, | |
type: String, | |
}, | |
srcset: { | |
type: Array as () => ImageSizeOption[], | |
}, | |
variant: { | |
default: Variant.medium, | |
type: String as () => Variant, | |
validate: oneOf(Variant), | |
}, | |
imgClass: { | |
type: String, | |
default: '', | |
}, | |
alt: { | |
type: String, | |
required: true, | |
}, | |
width: Number, | |
height: Number, | |
lazy: Boolean, | |
crop: Boolean, | |
}, | |
setup(props, { root, attrs }) { | |
// h is common alias for createElement in JSX/render functions | |
const h = root.$createElement; | |
const controlStore = useControlStore(); | |
const _width = props.width || sizes[props.variant]?.width; | |
const baseWidth = sizes[props.variant]?.width; | |
const _height = props.height || sizes[props.variant]?.height; | |
const loaded = ref(controlStore.imageStatuses[props.src] === 'loaded'); | |
const enteredViewport = ref(process.server); | |
const handleError = () => { | |
// TODO: | |
controlStore.imageStatuses[props.src] = 'error'; | |
}; | |
const handleLoad = () => { | |
loaded.value = true; | |
controlStore.imageStatuses[props.src] = 'loaded'; | |
}; | |
if (process.server) { | |
// if on the server, mark the image as loaded right away | |
handleLoad(); | |
} | |
const imageParams = (): ImageParam => ({ | |
src: props.src, | |
width: _width, | |
baseWidth, | |
crop: props.crop, | |
}); | |
const _srcWithSize = computed(() => { | |
return responsifyImageSource(imageParams()); | |
}); | |
const placeholderSrc = computed(() => { | |
return responsifyImageSource({ | |
...imageParams(), | |
width: 5, | |
}); | |
}); | |
const _srcsetWithSizes = computed(() => { | |
if (props.srcset) { | |
return responsifyImageSourceSet(imageParams(), props.srcset); | |
} | |
}); | |
const renderImg = ({ lazy } = { lazy: false }) => | |
h('img', { | |
class: `${props.imgClass} ${loaded.value && 'loaded'}`, | |
attrs: { | |
'data-lazy': lazy, | |
...attrs, | |
alt: props.alt, | |
// add src if image is not lazy or | |
src: | |
!lazy || enteredViewport.value || loaded.value | |
? _srcWithSize.value | |
: null, | |
srcset: | |
!lazy || enteredViewport.value || loaded.value | |
? _srcsetWithSizes.value | |
: null, | |
}, | |
on: { | |
load: handleLoad, | |
error: handleError, | |
}, | |
}); | |
if (!props.lazy) { | |
// if image is not lazy, just render plain image | |
return renderImg; | |
} | |
const renderPlaceholder = () => | |
h('img', { | |
class: `placeholder ${loaded.value && 'hidden'}`, | |
attrs: { | |
'data-lazy': true, | |
alt: props.alt, | |
src: placeholderSrc.value, | |
}, | |
}); | |
const wrapperStyle = `padding-top: calc(${_height} / ${_width} * 100%)`; | |
// wrapper adds padding style and Intersect observer | |
const renderWrapper = (child) => | |
h( | |
Intersect, | |
{ | |
props: { | |
rootMargin: '500px 0px 500px 0px', | |
once: true, | |
}, | |
on: { | |
enter: () => (enteredViewport.value = true), | |
}, | |
}, | |
[ | |
h( | |
'div', | |
{ | |
class: 'image-wrapper', | |
style: wrapperStyle, | |
}, | |
[renderPlaceholder(), child] | |
), | |
] | |
); | |
// if image is lazy, render it wrapped | |
return () => renderWrapper(renderImg({ lazy: true })); | |
}, | |
}); | |
</script> | |
<style lang="scss"> | |
.image-wrapper { | |
position: relative; | |
height: 0; | |
display: inline-block; | |
overflow: hidden; | |
} | |
.image-wrapper > div { | |
width: 100%; | |
height: 100%; | |
position: absolute; | |
top: 0; | |
left: 0; | |
} | |
img[data-lazy] { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: auto; | |
opacity: 0; | |
transition: 0.4s opacity; | |
&.loaded { | |
opacity: 1; | |
} | |
} | |
img.placeholder { | |
opacity: 1; | |
height: 100%; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment