Guide
Upload Image in Cloud

Upload Image in Cloud

If you have read about toolbarOptions, you may already know you have two choices for handling images. The first is inserting images as base64 strings directly in the editor. The second is uploading images to cloud storage and using the image URL in the editor.

If you've passed for the image_base64 in the toolbarOptions prop, you're all set—no further configuration is needed. However, if you've passed image_cloud because you prefer to store images in the cloud and use URLs in the editor, then this guide is for you.

Prerequisite

You'll need a backend server to manage cloud uploads. You can use any programming language or framework of your choice for the backend server.

Below, we'll first understand the image lifeclycle and then set up both the frontend and backend.

Understanding the Image Lifecycle: From Editor Insertion to Cloud Management

A Quick Overview
  • As you have chosen image_cloud, so now everytime user inserts an image on the editor, it will get inserted as a blob image.

  • Now, let's assume there's a "Submit" button that the user clicks after they've finished writing. At this point, all blob images in the output will be uploaded to your cloud storage. After a successful upload, each blob URL will be replaced with its corresponding cloud URL.

A Detailed Breakdown

1. Frontend - Image Insertion in the Editor:

  • Images are initially inserted in the editor as blobs.
  • A unique data-image-id attribute is attached to each blob image.

2. Frontend - Content Submission

  • Upon content submission:
    • The unique data-image-id assigned to every blob image is set as the name of its associated file.
    • Blob-associated files are sent to the backend as formData.

3. Backend - Operations and Response

  • The backend uploads the images in the cloud storage.
  • After successful uploads, the backend responses with:
    • The cloud storage link (src) for each image.
    • A unique identifier (publicID) for each image.

4. Frontend - Response Handling

  • The Rich Text Editor:
    • Swaps blob src attributes with the corresponding cloud URLs.
    • Updates the blob's data-image-id to match the publicID.

5. Image Deletion Mechanism

  • While editing, the user may delete images. After deleting when they submit the content:
    • The Rich Text Editor pushes the data-image-id attribute, holding the publicID, into an array of strings to send to the backend.
    • Using the publicID, the backend deletes the image from cloud storage.

Why Have We Decided to Upload Images All at Once Instead of One by One?

  • When users are writing, they often insert multiple images just to try them out. If we upload images one by one as they're added, we risk uploading images that the user might eventually remove. Even if we track these removed images and delete them from cloud storage, this approach still consumes extra time and bandwidth unnecessarily.
  • Moreover,If a user uploads images then closes or uninstalls the browser, we can't track those images.
  • Conversely, by waiting for a "Submit" action and only uploading the images that the user has finalized, we can optimize both time and bandwidth usage.

Setting Up the Frontend

Initially, we'll go through the code, followed by an in-depth explanation

import { useEffect } from 'react'
 
// import RichTextEditor component & useRichTextEditor hook
// ...
 
export default function Demo() {
 
    // Destructuring properties from useRichTextEditor
    const {
        output,
        fetchOutput, 
        enableImageOperations,
        executeImageOperations,
        imageOperationsData,
        fetchImageOperationsData  
    } = useRichTextEditor()
 
    // handleSubmit function
    const handleSubmit = () => {
        executeImageOperations() 
    }
 
    // useEffect hook
    useEffect(() => {
 
        if (imageOperationsData.outputUpdatedWithImageLink === '') return
 
        // Custom code placeholder
        // Put your code here. The code will only run when the dependency changes, not on the initial render of the component
 
    }, [imageOperationsData.outputUpdatedWithImageLink])  
 
    
    // JSX
    return (
 
        <div> 
 
            <RichTextEditor
            
                toolbarOptions={[
                    'image_cloud',
                    //...
                ]}
 
                customizeUI={{
                    // ...
                }}
 
                fetchOutput={fetchOutput}
 
                imageValidation={{
                    maximumFileSize: 1024,
                    acceptableFileFormats: ['png', 'jpg', 'jpeg']
                }}
 
                cloudImageApiEndpoint="https://your-server-domain.com/api/v1/rte/manage-image"  
 
                enableImageOperations={enableImageOperations}  
 
                fetchImageOperationsData={fetchImageOperationsData}  
            />
 
            {/* Submit REDIRECT_BUTTON */}
            <button onClick={handleSubmit}>
                Submit
            </button>
 
        </div>
    )
}
 

Code Explanation:

  • Lines 12-15: Here, we're destructuring several properties from the useRichTextEditor hook. These include enableImageOperations, executeImageOperations, imageOperationsData, and fetchImageOperationsData.
  • Line 39: Here, in our 'RichTextEditor' component, we are passing 3 new props that are specific to cloud image upload:
    • Prop cloudImageApiEndpoint: We are passing the backend server's API endpoint which manages image operations.
    • Prop enableImageOperations: We are passing "enableImageOperations" which we have destructured from useRichTextEditor hook
    • Prop fetchImageOperationsData: We are passing the "fetchImageOperationsData" which we have destructured from useRichTextEditor hook
  • Line 65: Notice the "Submit" button. When clicked, it triggers the handleSubmit function defined on line 19.
  • handleSubmit Function: This function calls executeImageOperations, which sends a request to the backend to initiate image-related operations.
  • Line 24: We employ useEffect with a dependency of imageOperationsData.outputUpdatedWithImageLink. This imageOperationsData.outputUpdatedWithImageLink changes only when the image operations are complete, and blob images in the output are replaced with their cloud URLs.
  • Bypassing Initial useEffect Execution: Inside our useEffect, there's a check against the dependency, imageOperationsData.outputUpdatedWithImageLink. If found to be an empty string, which is its initial value, we return from the effect. Inside the useEffect, we are checking the value of the dependency. This ensures that the logic inside the useEffect only runs when the dependency changes. not when the component initially renders.
  • Inside useEffect: You'll notice a comment indicating where to place your custom code. This is where you'd typically insert API calls to save the user's rich text editor content after image operations are complete.

Setting Up the Backend

While various programming languages, frameworks, or cloud storage solutions can be employed for backend image management, the example code that we will provide in this section will be exclusively utilizing Express.js and Cloudinary.

However, the detailed guide below can help you to use your desired framework or cloud storage solution

Guide of Backend Image Management
  1. Define the Endpoint
    • Create a specific endpoint for image management on the backend.
    • Ensure this endpoint matches the one provided to the RichTextEditor component as a prop.
  1. Configure the Request
    • The endpoint should be configured to handle POST requests.
  1. Understand Request Payload
    • The backend will receive a formData containing images under the imagesToUpload field.
    • Additionally, A field named imagesToDelete might be present in the request body. This lists the publicIDs of images intended for removal from cloud storage.
  1. Handle Image Upload
    • Upload image files received in the imagesToUpload to your cloud storage.
    • Post-upload, retrieve the necessary details from each image, specifically:
      • src: URL where the image is hosted.
      • publicID: A unique identifier for the image..
  1. Image Deletion
    • If imagesToDelete exists in the request body, it lists the publicID for each image you should delete from storage.
  1. Response Formation
    • The frontend expects a response containing an array named result. Each element within this array should be an object with the following structure:
      • src: URL to retrieve the uploaded image.

      • publicID: Unique ID for the image, formatted as anything/fileName.

        • anything: Represents any data or string that helps identify the image within your storage solution. This part is crucial as it should allow the backend to locate the image in the cloud storage for any subsequent operations, like deletions.

        • fileName: This must be the exact file name provided by the frontend in the imagesToUpload array.

        Note: If you're using Cloudinary as your cloud storage solution, you don't need to manually structure the publicId. Just provide the publicID you receive directly from Cloudinary.

Example Code

Building upon our understanding of backend image management, let's dive deeper into its practical implementation. As highlighted earlier, our demonstration will center on Express.js in combination with Cloudinary. To facilitate this, we'll be making use of the express-cloudinary-image-handler (opens in a new tab) package.

To begin, ensure you have the required packages:

npm install express express-cloudinary-image-handler
// importing express & dotenv
const express = require('express')
 
// importing express-cloudinary-image-handler
const { 
    cloudinaryConfig,
    imageUploadMiddleware,
    uploadImagesToCloudinary,
    deleteImagesFromCloudinary 
} = require('express-cloudinary-image-handler')
 
// app
const app = express()
 
// parse incoming JSON data in the request body
app.use(express.json())
 
// Configure Cloudinary settings (Use .env file to keep the following cloudinary credentials secret)
cloudinaryConfig({
    cloudName: '...',
    apiKey: '...',
    apiSecret: '...'
})
 
// Initialize the imageUploadMiddleware with the express app instance 
imageUploadMiddleware(app)
 
 
// Managing Image Operations
app.post('/api/v1/rte/manage-image', async (req, res, next) => {
 
    try {
 
        // Now, we will update the images
        const upload_report = await uploadImagesToCloudinary({
            req: req,
            configuration: {
                formDataFieldName: 'imagesToUpload',
                cloudinaryFolderName: 'images',
                maxFileSizeInKB: 1024,
                maxNumberOfUploads: 30,
                deleteAllTempFiles: true,
                useSourceFileName: true
            }
        })
 
        
        if (upload_report.isError) {
            return res.json({
                status_code: upload_report.errorInfo.statusCode,
                message: upload_report.errorInfo.message
            })
        }
 
 
 
        let result = upload_report.imagesInfo.map((image)=>{
            return {
                src: image.imageSrc,
                publicID: image.imagePublicId,
            }
        })
 
 
        // Getting the ids of the images which we need to delete 
        let idsOfImagesToDelete:any = req.body.imagesToDelete
 
 
        // Now, we will delete the images
        if (idsOfImagesToDelete && idsOfImagesToDelete.length > 0) {
 
            const delete_report = await deleteImagesFromCloudinary({
                publicIds: idsOfImagesToDelete
            })
 
            if (delete_report.isError) {
                return res.json({
                    status_code: upload_report.errorInfo.statusCode,
                    message: upload_report.errorInfo.message
                })
            }
 
        }
 
       
        // Sending JSON response
        return res.json({
            status: 'success',
            message: "All the images are successfully uploaded on the cloudinary",
            result: result
        })
    }
    
 
    catch (error) {
        return res.status(500).json({ error: 'An error occurred while processing your request.' })
    }
 
}
 
 
app.listen(3000, () => {
    console.log(`Server is running`)
})