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 ablob
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.
- The unique
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.
- The cloud storage link (
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 thepublicID
.
- Swaps blob
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 thepublicID
, into an array of strings to send to the backend. - Using the
publicID
, the backend deletes the image from cloud storage.
- The Rich Text Editor pushes the
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 includeenableImageOperations
,executeImageOperations
,imageOperationsData
, andfetchImageOperationsData
.
- 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
- Prop
- 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 ofimageOperationsData.outputUpdatedWithImageLink
. ThisimageOperationsData.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 theuseEffect
, 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
- 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.
- Configure the Request
- The endpoint should be configured to handle POST requests.
- Understand Request Payload
- The backend will receive a
formData
containing images under theimagesToUpload
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.
- The backend will receive a
- 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..
- Upload image files received in the
- Image Deletion
- If
imagesToDelete
exists in the request body, it lists the publicID for each image you should delete from storage.
- If
- 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 asanything/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 theimagesToUpload
array.
Note: If you're using Cloudinary as your cloud storage solution, you don't need to manually structure the
publicId
. Just provide thepublicID
you receive directly from Cloudinary. -
-
- The frontend expects a response containing an array named
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`)
})