Implementing Avatar Image Cropping in Vue.js Mobile Projects with CropperJS

Prerequisites

Install the necessary dependencies to handle image manipulation and EXIF data:

npm install cropperjs exif-js

Plugin Architecture

Create a dedicated module to encapsulate cropping logic. This approach attaches methods to the Vue prototype, making them accessible globally.

File location: src/utils/imageClipper.js

import Cropper from 'cropperjs'
import Exif from 'exif-js'

class ImageCropper {
  constructor () {
    this.preview = null
    this.cropperInstance = null
    this.targetElement = null
    this.options = {}
    this.fileReference = null
  }

  install (Vue) {
    Vue.prototype.setupCropper = (config) => {
      const self = this
      self.options = config || {}
      self.renderOverlay()
      self.bindEvents()
    }

    Vue.prototype.renderOverlay = function () {
      let template = `
        <div id="clip_overlay">
          <img id="source_image" src="placeholder.jpg">
          <button type="button" id="confirm_btn">Confirm</button>
          <button type="button" id="cancel_btn">Cancel</button>
          <div class="loading_state"></div>
          <div class="success_state"></div>
        </div>
      `

      const container = document.createElement('div')
      container.id = 'clip_wrapper'
      container.classList.add('overlay-container')
      container.innerHTML = template
      document.body.appendChild(container)
      
      this.preview = document.getElementById('source_image')
      this.targetElement = config && config.targetElement ? config.targetElement : null
    }

    Vue.prototype.bindEvents = function () {
      const self = this
      this.confirmBtn = document.getElementById('confirm_btn')
      this.cancelBtn = document.getElementById('cancel_btn')

      this.confirmBtn.addEventListener('click', () => {
        self.processImage()
      })

      this.cancelBtn.addEventListener('click', () => {
        self.terminateSession()
      })
    }

    Vue.prototype.triggerCropping = function (event, options) {
      const self = this
      this.fileReference = event.target.files[0]

      if (!this.fileReference) return

      // Initialize configuration defaults
      const defaultConfig = { aspectRatio: 1, resultTarget: this.targetElement }
      const config = { ...defaultConfig, ...options }
      self.options = config

      this.generateURL(this.fileReference)
    }

    Vue.prototype.generateURL = function (file) {
      if (window.createObjectURL) return window.createObjectURL(file)
      if (window.URL) return window.URL.createObjectURL(file)
      if (window.webkitURL) return window.webkitURL.createObjectURL(file)
      return null
    }

    Vue.prototype.startSession = function (url) {
      if (this.cropperInstance) {
        this.cropperInstance.replace(url)
      } else {
        this.cropperInstance = new Cropper(this.preview, {
          aspectRatio: this.options.aspectRatio || 1,
          autoCropArea: this.options.autoCropArea || 0.8,
          viewMode: 1,
          zoomable: false,
          ready: () => {
            if (this.options.freeMode) {
              this.cropperInstance.disable()
            }
          }
        })
      }
    }

    Vue.prototype.processImage = function () {
      if (!this.cropperInstance) return

      const croppedCanvas = this.cropperInstance.getCroppedCanvas()
      if (!croppedCanvas) return

      const dataUrl = croppedCanvas.toDataURL('image/jpeg', 0.9)
      
      this.handleUpload(dataUrl)
    }

    Vue.prototype.handleUpload = function (data) {
      document.querySelector('.loading_state').style.display = 'block'
      
      setTimeout(() => {
        if (this.options.resultTarget) {
          this.options.resultTarget.src = data
        }
        
        // Trigger custom upload handler or API call here
        console.log('Processed image data:', data.substring(0, 50))

        document.querySelector('.loading_state').style.display = 'none'
        document.querySelector('.success_state').style.display = 'block'
        
        setTimeout(() => {
          this.terminateSession()
        }, 2000)
      }, 500)
    }

    Vue.prototype.terminateSession = function () {
      if (this.cropperInstance) {
        this.cropperInstance.destroy()
        this.cropperInstance = null
      }

      const wrapper = document.getElementById('clip_wrapper')
      if (wrapper && wrapper.parentNode) {
        wrapper.parentNode.removeChild(wrapper)
      }
      this.preview = null
    }
}

export default {
  install (Vue) {
    Vue.prototype.$ImageCropper = new ImageCropper()
  }
}

Application Entry Configuration

Import the plugin and register it in the main application file.

// main.js
import Vue from 'vue'
import App from './App.vue'
import ImageClipper from '@/utils/imageClipper'

Vue.use(ImageClipper)

new Vue({
  el: '#app',
  render: h => h(App),
})

Component Integrasion

Utilize the injected method within a component to trigger the cropping UI upon file selection.

<template>
  <div class="avatar-upload-section">
    <input type="file" accept="image/*" @change="handleFileSelect" />
    <img :src="userAvatar" alt="User Avatar" ref="avatarPreview" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      userAvatar: null
    }
  },
  methods: {
    handleFileSelect (event) {
      this.$ImageCropper.triggerCropping(event, {
        targetElement: this.$refs.avatarPreview,
        aspectRatio: 1
      })
    }
  }
}
</script>

Styling Setup

Apply CSS styles to manage the overlay appearance and responsiveness. Include these rules in your global stylesheet or component SCSS file.

.overlay-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
  justify-content: center;
  align-items: center;
}

#clip_wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

#source_image {
  max-width: 100%;
  display: block;
}

#confirm_btn {
  position: absolute;
  bottom: 40px;
  right: 10%;
  padding: 10px 30px;
  background: #1AAD19;
  color: white;
  border: none;
  border-radius: 4px;
}

#cancel_btn {
  position: absolute;
  bottom: 40px;
  left: 10%;
  padding: 10px 30px;
  background: #E64340;
  color: white;
  border: none;
  border-radius: 4px;
}

.loading_state,
.success_state {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: none;
  background: rgba(0,0,0,0.7);
  color: white;
  padding: 1rem;
  border-radius: 8px;
}

Tags: Vue.js CropperJS Image Processing Mobile Development frontend

Posted on Thu, 07 May 2026 20:33:05 +0000 by cavey5