Creating a Cloudinary Image Uploader With Crop Support in Vue
Tags: cloudinary, vue, webdev, showdev
Published: 2021-01-03 04:18:01

---
title: Creating a Cloudinary Image Uploader With Crop Support in Vue
published: true
description: A tutorial on how to make a cloudinary image uploader component with crop support in VueJs.
tags: cloudinary, vue, webdev, showdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/9o63bkhofhqqni1il6a8.jpg
canonical_url: https://jamesinkala.com/blog/creating-a-cloudinary-image-uploader-with-crop-support-in-vue/
---

I came across cloudinary about a year plus ago and since then it has been my primary image delivery network for most of my personal projects as it provides a very extensive free plan besides having fair paid plans. Another plus for me is that it's easy to set up and use.

Since I've been using it for some time and given that I work on lots of Vue projects, I decided to create a vue component that takes care of that for me.
In it I've added a cropping feature that you can apply before uploading the image to cloudinary.

On completion the expectation of how the component will function is as demonstrated below.
![Component Demonstration](https://res.cloudinary.com/djx5h4cjt/image/upload/v1609935316/vue-cloudinary-uploader/clodinary-vcu-preview.gif)

I have also created a similar Vue component that works with laravel that I will also be writing about.

Before proceeding, you first need to create an account on [cloudinary](https://cloudinary.com/).

So let's create our uploader.

## Setting up cloudinary ##
Log into the cloudinary account and create an upload preset that will be handling the images being uploaded. Before setting up an upload preset, first create a media library folder that will hold the images being uploaded.
Go to the Media Library
![Go to media library](https://dev-to-uploads.s3.amazonaws.com/i/olpmj08dpkzxit08jg4z.gif)
<br/>

Create a new folder
![Create a new folder](https://dev-to-uploads.s3.amazonaws.com/i/ifdssxj9hexf0gsfizgd.gif)
<br/>

Then proceed with creating the upload preset.
Go to settings > upload (tab) 
![Settings > Upload](https://dev-to-uploads.s3.amazonaws.com/i/k3rvktihl1ia8qxnlzi0.gif)
<br/>

Scroll down to and click on ***add upload preset***.
![Add upload preset](https://dev-to-uploads.s3.amazonaws.com/i/quaj764g5zvdav9ajh0j.gif)
<br/>

Set the ***Upload preset name***, select the ***unsigned*** signing mode and fill in the name of the folder that you created above.
![Set upload preset name and signed type](https://dev-to-uploads.s3.amazonaws.com/i/w6k9t6ljnt5noeofn1u0.gif)
*__Note:__ Unsigned upload presets are used when implementing upload capabilities from client-side apps.*
<br/>

Go to Upload control and switch the return delete token to on if you want to be able to delete uploaded images.
![Switch delete token on](https://dev-to-uploads.s3.amazonaws.com/i/git85cfe09l01dvn6b40.gif)

*__Note:__ You can only use the delete token to delete an uploaded file within a span of ten minutes after uploading it.*
<br/>

Set up the rest of the settings to your preferences and click on 'save' to save this new upload preset.   

Back on the settings page and upload tab the new upload preset will be listed amongst the existing presets
![Presets list](https://dev-to-uploads.s3.amazonaws.com/i/t4cp1pbkhac8gxrwjefx.gif)<br/>

### The Vue Component ###
Onto creating the Vue component that will carry out uploading images to cloudinary throught the created upload preset.

#### The Template ####
```html
<template>
  <div id="vue-cloudinary-uploader">
      <input type="hidden" :value="uploadedImageData.secureUrl" :name="inputName">

      <button v-if="uploadedImageData.secureUrl" class="vcu-button button-danger" type="button" @click="deleteImageFromCloud()">Change Image</button>
      <button id="uploader-button" class="vcu-button button-info" type="button" v-else @click.prevent="showModal()" :disabled="processingUpload || modelVisible">Select Image File</button>

    <div id="modal-wrapper" v-show="modelVisible">
      <div class="image-cropper">
        <div class="editor">
          <div class="input">
            <div>
              <input type="file" ref="photo" accept="image/*" @change="addLocalImage()" id="vcu-file-input">
            </div>
          </div>
          <div v-if="showUploadProgress" class="vcu-progress-wrapper">
            <div class="vcu-progress" :style="'width: ' + uploadProgress + '%'"></div>
          </div>
          <div class="img">
            <img ref="working_image" id="image" :src="localFileDataUrl">
          </div>
        </div>
        <div class="options">
          <div>
            <button class="vcu-button button-danger" type="button" @click="destroyUploaderInstance(true)" :disabled="processingUpload">CANCEL</button>
          </div>
          <div>
            <button class="vcu-button button-info" type="button" @click="getCroppedCanvas()" :disabled="processingUpload">CROP</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
```

The above template contains the buttons that trigger the launching of the uploader modal, within this modal the cropper container that offers image cropping capabilities is placed.
<br>

#### The Javascript ####
Install [cropperjs](https://fengyuanchen.github.io/cropperjs) which will be used to crop the images and [axios](https://github.com/axios/axios) that will be the http client used to upload the images.  
```sh
#install cropperjs
npm i cropperjs

#install axios
npm i axios
```
<br/>
Next is a brekdown of the js code.
<br/>

```js
  import axios from 'axios'
  import Cropper from 'cropperjs'
  import 'cropperjs/dist/cropper.css'
  export default {
    name: "CloudinaryVueUploader",
    data(){
      return {
        showFileSelector: true,
        showImageCropper: false,
        showUploadProgress: false,
        modelVisible: false,
        processingUpload: false, // this will be true when image is being uploaded to prevent any other upload request

        cropperInstance: null,
        uploadProgress: 0,
        localFileDataUrl: '',
        cloudinaryUploadUrl: '',
        cloudinaryDeleteUrl: '',
        uploadedImageData: {
          deleteToken: '',
          publicId: '',
          secureUrl: ''
        }
      }
    },
```
Initiate the variables to use within the component.
<br/>

```js
    props: {
      CloudinaryCloudName: {
        type: String,
        default: 'CLOUDINARY_CLOUD_NAME',
        validator: (x) => x !== ''
      },
      cloudinaryUploadPreset: {
        type: String,
        default: 'CLOUDINARYY_UPLOAD_PRESET_NAME',
        validator: (x) => x !== ''
      },
      aspectRatio: {
        type: Number,
        default: 0
      },
      inputName: {
        type: String,
        default: 'imageToUpload'
      },
    },
```
Since this will be a Vue component, the above props will simplify passing in the ***CloudinaryCloudName*** and ***cloudinaryUploadPreset*** variables data.
The ***CloudinaryCloudName*** is the first item under Account Details on cloudinary's dashboard.
<br/>

```js
    mounted(){
      this.cloudinaryUploadUrl = `https://api.cloudinary.com/v1_1/${this.CloudinaryCloudName}/upload`
      this.cloudinaryDeleteUrl = `https://api.cloudinary.com/v1_1/${this.CloudinaryCloudName}/delete_by_token`
    },
```
Populate the ***cloudinaryUploadUrl*** and ***cloudinaryDeleteUrl*** variables when the compoonent is mounted.
<br/>

```js
    methods: {
      showModal(){
        this.modelVisible = true
      },
      hideModal(){
        this.modelVisible = false
      },
```
These two methods will be responsible with the toggling of the component's modal.
<br/>

```js
      editImage(){
        this.showImageCropper = true
        if(this.cropperInstance){
          this.cropperInstance.destroy()
          this.showImageCropper = false
        }
        this.$nextTick(() => {
          this.cropperInstance = new Cropper(this.$refs.working_image, {
            aspectRatio: this.aspectRatio,
            viewMode: 2,
            background: false,
            crop(event) {},
            ready(){
            this.showImageCropper = true
          }
          });
        })
      },
```
**editImage():** Initiates cropper instance on the selected image.
<br/>

```js
      async addLocalImage(){
        if(this.$refs.photo.files.length < 1){
          console.log('No photo selected')
          return false
        }
        let photo = this.$refs.photo.files[0];
        this.localFileDataUrl = window.URL.createObjectURL(photo)
        this.$nextTick(this.editImage())
      },
```
**addLocalImage():** Calls **editImage()** when an image has been selected.
<br/>

```js
      async getCroppedCanvas(){
        if(!this.cropperInstance){
          alert("Select Image File!")
          return false
        }
        if(!this.cropperInstance.getCroppedCanvas()){
          alert("No Image Detected!")
          return false
        }
        if(this.processingUpload){ // don't initiate another upload while one is running
          alert("Previous upload not completed!")
          return false
        }
        let canvas = this.cropperInstance.getCroppedCanvas()
        await canvas.toBlob( (blob) => {
          let formData = new FormData()
          formData.append('file', blob)
          formData.append('upload_preset', this.cloudinaryUploadPreset)
          this.uploadImageToCloud(formData)
        })
      },
```
**getCroppedCanvas():** Get's cropper's canvas and passes that data to **uploadImageToCloud()**.
<br/>

```js
      destroyUploaderInstance(closeCropper = false){
        // destroy cropper instance
        if(this.cropperInstance && closeCropper){
          this.cropperInstance.destroy()
        }
        // set all other variables to their defaults
        this.cropperInstance = null
        this.localFileDataUrl = ''
        this.processingUpload = false
        this.showFileSelector = true
        this.showImageCropper = false
        this.showUploadProgress = false
        this.uploadProgress = 0
        document.getElementById("vcu-file-input").value = "";
        this.uploadedImageData = { deleteToken: '', publicId: '', secureUrl: '' }
        this.$emit('uploaderDestroyed', "" )
        if(closeCropper){
          this.hideModal()
        }
      },
```
**destroyUploaderInstance():** Resets the component's variables.
<br/>

```js
      uploadImageToCloud(formData){
        this.showUploadProgress = true
        this.processingUpload = true
        this.uploadProgress = 0
        axios.post(this.cloudinaryUploadUrl, formData, {
          onUploadProgress: (progressEvent) => {
            this.uploadProgress = progressEvent.lengthComputable ? Math.round( (progressEvent.loaded * 100) / progressEvent.total ) : 0 ;
          }
        })
        .then( (response) => {
          this.uploadedImageData = {
            secureUrl: response.data.secure_url,
            deleteToken: response.data.delete_token,
            publicId: response.data.public_id
          }
          this.showUploadProgress = false
          this.processingUpload = false
          this.$emit('imageUrl', response.data.secure_url )
          this.hideModal()
        })
        .catch( (error) => {
          if(error.response){
              console.log(error.message)
          }else{
              console.log(error)
          }
          this.showUploadProgress = false
          this.processingUpload = false
        })
      },
```
**uploadImageToCloud():** This method uploads the modified image and obtains the uploade image url, delete token and public id from cloudinary.
<br/>

```js
      deleteImageFromCloud(){
        if(this.uploadedImageData.deleteToken === ''){ // if delete token is not provided
          console.log("uploadedImageData ", this.uploadedImageData)
          alert("No Delete token")
        }
        let formData = new FormData()
        formData.append('token', this.uploadedImageData.deleteToken)
        axios.post(this.cloudinaryDeleteUrl, formData)
          .then(response => {
            if(this.cropperInstance){
              this.cropperInstance.destroy()
            }
            this.destroyUploaderInstance()
            this.showModal()
            this.$emit('remoteImageDeleted')
          })
          .catch(error=>{
            console.log(error)
            return false
          })
      }
    }
  }
```
**deleteImageFromCloud():** This method uses the ***deleteToken*** obtained on uploding the image to delete the previously uploaded image. 
**Note**: Upload deletes by the ***deleteToken*** can only be done no more than ten minutes after the image has been uploaded.
<br/>

### The Style ###
After, add the following style or modify it to your liking at the end of the component. 
```css
  :root{
    --default-font-family: Arial, Helvetica, sans-serif;
    --default-font-weight-small: 300;
    --default-font-weight-medium: 600;
    --default-font-weight-heavy: 900;

    --default-space-x-small: 5px;
    --default-space-small: 10px;
    --default-space-medium: 15px;
    --default-space-large: 25px;
    
    --color-primary: rgb(233,233,239);
    --color-secondary: rgb(248,248,250);
    --color-tertiary: rgb(255,255,255);
    --color-danger: rgb(220, 20, 60);
    --color-danger-dark: rgb(200, 20, 60);
    --color-danger-light: rgb(240, 20, 60);
    --color-info: rgb(13, 125, 216);
    --color-info-dark: rgb(10, 94, 196);
    --color-info-light: rgb(10, 94, 236);
    --color-success: rgb(16, 190, 10);
    --color-text-button: aliceblue;
  }

  * {
    font-family: var(--default-font-family);
  }

  .vcu-progress-wrapper{
    height: 30px;
    width: 100%;
    padding: 0;
  }
  .vcu-progress{
    margin: 0;
    height: inherit;
    background: var(--color-success)
  }

/*button styles*/
  .vcu-button{
    position: relative;
    background: var(--color-primary);
    border: none;
    border-radius: 2px;
    color: var(--color-text-button);
    font-weight: 500;
    padding: var(--default-space-small);
    margin-left: 5px;
    cursor: pointer;
  }
  .vcu-button:disabled{
    background: var(--color-primary) !important;
    color: black !important;
  }

  .close-button{
    position: absolute;
    top: 0; right: 0;
    margin: var(--default-space-x-small);
    padding: var(--default-space-small);
    font-weight: var(--default-font-weight-medium);
    cursor: pointer;
    z-index: 5;
  }
  .close-button:hover{
    background-color: var(--color-danger-light);
  }
  .close-button:active{
    background-color: var(--color-danger-dark);
  }

  .button-danger{
    background-color: var(--color-danger);
  }
  .button-danger:hover{
    background-color: var(--color-danger-light);
  }
  .button-danger:active{
    background-color: var(--color-danger-dark);
  }
  .button-info{
    background-color: var(--color-info);
  }
  .button-info:hover{
    background-color: var(--color-info-light);
  }
  .button-info:active{
    background-color: var(--color-info-dark);
  }

  #modal-wrapper{
    position: fixed;
    top: 10px; bottom: 10px;
    left: 100px; right: 100px;
    border: 1px solid var(--color-primary);
    margin: var(--default-space-small);
    z-index: 99999;
    display: flex;
    box-shadow: 0 0 5px 0 #b1b0b0;
  }

  .image-cropper{
    flex-grow: 1;
    /* padding: var(--default-space-small); */
    display: flex;
    flex-direction: column;
    background-color: var(--color-tertiary);
    max-height: inherit;
    max-width: inherit;
  }

  .image-cropper > .editor{
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    background-color: var(--color-tertiary);
    padding: var(--default-space-small);
    max-height: inherit;
    max-width: inherit;
    overflow: hidden;
  }
  .image-cropper > .editor > .input{
    height: 50px;
    display: flex;
    flex-direction: row;
    align-items: flex-start;
  }
  .image-cropper > .editor > .input{
    height: 50px;
    display: flex;
    flex-direction: row;
    justify-content: center;
  }
  .image-cropper > .editor > div > .input > input, .image-cropper > .editor > .input > button{
    min-height: 20px;
    font-size: 16px;
    margin-left: var(--default-space-small);
  }


  input[type="file"]{
    position: relative !important;
    top: 1% !important;
    z-index: 1 !important;
    width: initial !important;
    height: initial !important;
    -webkit-appearance: initial !important;
    opacity: 1 !important;
    cursor: pointer !important;
  }

  .image-cropper > .editor > .img{
    position: relative;
    max-height: -webkit-fill-available;
    padding: var(--default-space-small);
    flex-grow: 1;
    background: var(--color-tertiary);
    min-height: 20px;
    margin-bottom: 20px;
  }

  .image-cropper > .options{
    display: flex;
    flex-direction: row;
    justify-items: center;
    justify-content: flex-end;
    height: 50px;
    background-color: var(--color-secondary);
    padding: var(--default-space-x-small);
    border-top: 1px solid var(--color-primary);
  }

  .image-cropper > .options > div{
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100px;
    padding: var(--default-space-small);
    margin-left: var(--default-space-x-small);
    cursor: pointer;
    font-weight: var(--default-font-weight-small);
  }


  @media screen and (orientation: portrait){
    #modal-wrapper{
      left: 20px; right: 20px;
    }

    .image-cropper > .options{
      height: 10vmin;
    }
  }

  img {
    max-width: 100%;
  }

/*model styes*/
  .show{
    display: flex !important
  }
  .hide{
    display: none !important
  }
```

### Getting The Uploaded Image URL ###
There are two ways to get the image url of the uploaded image from this component. The first is by listening to the **imageUrl** event on the component and the second is by passing a **inputName** prop which will populate the hidden input's name attribute within the component.
The later is the favorable way to use this component outside the Vue environment such as inside a blade form.

Now go out there and wreck the internet.