Skip to content

Instantly share code, notes, and snippets.

@MartinMalinda
Created January 7, 2022 16:40
Show Gist options
  • Save MartinMalinda/12b8cb0c4143c1ae1c961cb397f46c15 to your computer and use it in GitHub Desktop.
Save MartinMalinda/12b8cb0c4143c1ae1c961cb397f46c15 to your computer and use it in GitHub Desktop.
Lazy Image Component
<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