DEV Community

Cover image for CKEditor Image upload with Firebase and React
Suraj975
Suraj975

Posted on

CKEditor Image upload with Firebase and React

CKEditor is an awesome opensource project that offers tons of tools similar to Microsoft word. I have been using this editor on the website that I recently created (https://www.codingnotesonline.com/). However, for the image upload option in CKEditor, it requires a server to store the images. The examples mentioned in the documents are specifically based on PHP for the backend side and you can find more examples of similar implementation in Nodejs. The problem was that I was using firebase for all the backend worked and didn't find a proper example to implement it with firebase.

How to Implement CKEditor Image upload with Firebase in React ?

I have used firebase upload functions and added in custom adapter function of CKEditor.
Firebase(upload function):(https://firebase.google.com/docs/storage/web/upload-files)
CKEditor(custom adapter):(https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html)

In the below example, MyUploadAdapter is the custom adapter in which we use a firebase upload function that returns a URL, which is a link to an image stored in the firebase server. This link is then used by the CKEditor to display it on the page.

Firebase also offers several upload stages that you can use to show the progress of image upload to the user.

Below is a complete working example that you can implement in your project.

import React from "react";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
//import firbase from your directory


class MyUploadAdapter {
  constructor(loader) {
    this.loader = loader;
  }
  // Starts the upload process.
  upload() {
    return this.loader.file.then(
      file =>
        new Promise((resolve, reject) => {
          let storage = firebase.storage().ref();
          let uploadTask = storage
            .child(file.name)
            .put(file, metadata);
          uploadTask.on(
            firebase.storage.TaskEvent.STATE_CHANGED, // or 'state_changed'
            function(snapshot) {
              // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
              var progress =
                (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
              console.log("Upload is " + progress + "% done");
              switch (snapshot.state) {
                case firebase.storage.TaskState.PAUSED: // or 'paused'
                  console.log("Upload is paused");
                  break;
                case firebase.storage.TaskState.RUNNING: // or 'running'
                  console.log("Upload is running");
                  break;
              }
            },
            function(error) {
              // A full list of error codes is available at
              // https://firebase.google.com/docs/storage/web/handle-errors
              // eslint-disable-next-line default-case
              switch (error.code) {
                case "storage/unauthorized":
                  reject(" User doesn't have permission to access the object");
                  break;

                case "storage/canceled":
                  reject("User canceled the upload");
                  break;

                case "storage/unknown":
                  reject(
                    "Unknown error occurred, inspect error.serverResponse"
                  );
                  break;
              }
            },
            function() {
              // Upload completed successfully, now we can get the download URL
              uploadTask.snapshot.ref
                .getDownloadURL()
                .then(function(downloadURL) {
                  // console.log("File available at", downloadURL);
                  resolve({
                    default: downloadURL
                  });
                });
            }
          );
        })
    );
  }
}

class App extends Component {
    render() {
        return (
            <div className="App">
                <h2>Using CKEditor 5 build in React</h2>
                <CKEditor
                    editor={ ClassicEditor }
                    data="<p>Hello from CKEditor 5!</p>"
                    onInit={editor => {
                      editor.plugins.get("FileRepository").createUploadAdapter = loader => {
                        return new MyUploadAdapter(loader);
                      };
                    }}
                    onChange={ ( event, editor ) => {
                        const data = editor.getData();
                        console.log( { event, editor, data } );
                    } }
                    onBlur={ ( event, editor ) => {
                        console.log( 'Blur.', editor );
                    } }
                    onFocus={ ( event, editor ) => {
                        console.log( 'Focus.', editor );
                    } }
                />
            </div>
        );
    }
}

This is a boilerplate code that you can use and make changes as per your needs. I hope this would help you and saved a lot of your time finding the solution to it.

Discussion (10)

Collapse
devalo profile image
Stephan Bakkelund Valois

Hi, very nice article.
I just have a question.

Say the user adds an image, but regret the action and remove it from the editor.
The image will be uploaded to storage either way. How can we revert this action / delete the unused image from storage? or else, we might run into the problem of which we fill up our storage with unused images :p

Collapse
devalo profile image
Stephan Bakkelund Valois

I made a working solution to this. I'm pushing the file.name from your module into a root array, and taking substrings from the editor.getData() which only consists of image names. On create, I check both these arrays up against each other, and delete the images from storage that haven't been used.

This way, all the images will be temporarily until the user clicks save.
One edge case if if the user leaves the editor before the save is pressed. This also needs to be handled by deleting all images that has been uploaded temporarily.

Collapse
anildharni profile image
Anil Dharni

Hi Stephan, I am stuck here. Could you please tell me how you achieved this using a code snippet?
Thank You.

Collapse
dhirajverma_in profile image
Dhiraj Verma

Hi Stephan. could you please tell me file repo. or any git hub link for this. please.

Collapse
sands45 profile image
Sands

Here is the code with Firebase 9
class MyUploadAdapter {
constructor(loader) {
this.loader = loader;
}
// Starts the upload process.
upload() {
return this.loader.file.then(
(file) =>
new Promise((resolve, reject) => {
let storage = getStorage();
uploadBytes(
ref(storage, /${"Dial & Dine"}/${new Date().getTime()}),
file
)
.then((snapshot) => {
return getDownloadURL(snapshot.ref);
})
.then((downloadURL) => {
resolve({
default: downloadURL,
});
}).catch((error)=>{
reject(error.message);
})
})
);
}
}

Collapse
andrewroar profile image
andrewroar • Edited on

does anyone know what firebase to import from your directory??

My code:

import { initializeApp } from "firebase/app";

import { getAnalytics } from "firebase/analytics";

import { getStorage } from "firebase/storage";

export const firebaseConfig = {
apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXX",

authDomain: ""XXXXXXXXXXXXXXXX",",

projectId: "XXXXXXXXXXXXXXXX",",

storageBucket: "XXXXXXXXXXXXXXXX",,

messagingSenderId: "XXXXXXXXXXXXXXXX",,

appId: "XXXXXXXXXXXXXXXX",,

measurementId: "XXXXXXXXXXXXXXXX",
};

// Initialize Firebase

export const firebase = initializeApp(firebaseConfig);

export const storage = getStorage(firebase);

Collapse
wasurocks profile image
Wasu

Thank you so much.

Collapse
fastcodesoluti1 profile image
fastcodesolutions

I apply this but my page is load when image is upload

Collapse
udayupix profile image
udayupix

It was nice..
It will be helpful if you provide git repos..
thanks in advance..

Collapse
abhishekjaiswal55236 profile image
Abhishek jaiswal

Thank you so much.

if the code still does not work try using onReady instead of onInit.