Skip to content

EpImage

Props

NameDescriptionTypeDefault
srcThe source URL of the image.string-
altThe alt text for the image.string''
widthThe width of the image.`stringnumber`
heightThe height of the image.`stringnumber`
classNameAdditional CSS class name for the image element.string''
placeholderURL of the placeholder image to display while loading.string''
placeholderColorThe background color of the placeholder.string'#f5f5f5'
placeholderOpacityThe opacity of the placeholder.number1
lazyIf true, enables lazy loading using Intersection Observer.booleantrue

INFO

This component does not use events, slots.

Component Code

vue
<template>
  <div
    class="ep-image"
    ref="imageEl"
  >
    <img
      v-if="isLoaded"
      :src="src"
      :alt="alt"
      :width="width"
      :height="height"
      :class="className"
    >
    <div
      v-else
      class="image__placeholder"
      :style="placeholderStyle"
    />
  </div>
</template>

<script setup>
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue'

  const props = defineProps({
    /**
     * The source URL of the image.
     */
    src: {
      type: String,
      required: true,
    },
    /**
     * The alt text for the image.
     */
    alt: {
      type: String,
      default: '',
    },
    /**
     * The width of the image.
     */
    width: {
      type: [String, Number],
      default: '100%',
    },
    /**
     * The height of the image.
     */
    height: {
      type: [String, Number],
      default: '100%',
    },
    /**
     * Additional CSS class name for the image element.
     */
    className: {
      type: String,
      default: '',
    },
    /**
     * URL of the placeholder image to display while loading.
     */
    placeholder: {
      type: String,
      default: '',
    },
    /**
     * The background color of the placeholder.
     */
    placeholderColor: {
      type: String,
      default: '#f5f5f5',
    },
    /**
     * The opacity of the placeholder.
     */
    placeholderOpacity: {
      type: Number,
      default: 1,
    },
    /**
     * If true, enables lazy loading using Intersection Observer.
     */
    lazy: {
      type: Boolean,
      default: true,
    },
  })

  const isLoaded = ref(false)
  const imageEl = ref(null)
  let observer = null

  const placeholderStyle = computed(() => {
    return {
      width: props.width,
      height: props.height,
      backgroundColor: props.placeholderColor,
      opacity: props.placeholderOpacity,
      backgroundImage: props.placeholder ? `url(${props.placeholder})` : '',
      backgroundSize: 'cover',
    }
  })

  const addLazyLoadListener = () => {
    observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          loadImage()
          observer.unobserve(entry.target)
        }
      })
    })

    if (imageEl.value) {
      observer.observe(imageEl.value)
    }
  }

  const loadImage = () => {
    const img = new Image()
    img.src = props.src
    img.onload = () => {
      isLoaded.value = true
    }
  }

  onMounted(() => {
    if (props.lazy) {
      addLazyLoadListener()
    } else {
      loadImage()
    }
  })

  onBeforeUnmount(() => {
    if (observer) {
      observer.disconnect()
      observer = null
    }
  })
</script>

<style lang="scss" scoped>
  .image {
    display: inline-block;
    position: relative;
    overflow: hidden;

    img {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    .image__placeholder {
      position: absolute;
      top: 0;
      left: 0;
    }
  }
</style>