Putting a React Draft WYSIWYG Editor in your MERN application

Posted on Posted in Blogs, Technology




Recently I’ve been working on an online news portal being rebuilt with the MERN stack. One of the critical requirements was a WYSIWYG editor (like one present in wordpress), that is fully compatible with react and the MERN stack. One of the more popular wysiwyg editor, the CKEditor is built to work on php applications, so I had to look for something that would be perfect for the MERN stack. Upon searching I came across a library called React Draft WYSIWYG.

I used this library to allow editors to create news articles in the admin dashboard, but along the way encountered some difficulties. The documentation is not very elaborate or easy to understand. I don’t blame the author, as she did a fantastic job in creating and maintaining this library. But as she is a single person, doing this open source (non-profit) work is quite difficult. Moreover it is built upon another library called draftjs which is created by Facebook itself, so the author assumes prior knowledge of that library.

There are some articles and forum discussions available where I mostly learnt how to use this library, but almost all of them did it in the old class component way. But as the react community is pushing towards the more modern function component based react apps, I want to document how to implement the same thing using functional components . So in this blog I am going to show you step by step how to use this editor in your MERN application, with functional components.

The source code for the sample project is available in Github, link here.

 

So what is draft.js:

Draft.js is a javascript library created by Facebook which allows for more functionality in their text form fields and creates what is called a RichText editor. For example in the comment box of any post in Facebook you can attach emojis or pictures or gifs. This is not possible in the traditional html input or textarea. Draft.js does this by creating all of the text as JSON objects instead of a string. It groups the text that is the same together and then once the text editor changes to bold or a list it will create a new key within the JSON object and the text written in that format will be saved there. For a deep dive into the technicalities see this blog.




Putting a React Draft WYSIWYG Editor in your MERN application:

Now lets get into the process of adding the editor in our MERN app. Lets start at the front-end i.e. the react app.

Step 1: Install the packages

npm install draft-js react-draft-wysiwyg draft-js-export-html

Step 2: Add the editor in the dashboard

We use the editor in the AddPost.js file to add a blog post (complete code in github repo). For simplicity we are using just two fields, title and description in every post (along with auto generated _id by mongoDB). As it is a functional component we use the hook useState to initialize these state variables.
const [title, setTitle] = useState('')
const [description, setDescription] = useState(EditorState.createEmpty())

We also import the following:

import { Editor } from 'react-draft-wysiwyg';
import { EditorState, convertToRaw } from 'draft-js';
import '../../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import {stateToHTML} from 'draft-js-export-html'

The editor is configured as follows:

<Editor editorState={description}
wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
wrapperStyle={{ border: "2px solid green", marginBottom: "20px" }}
editorStyle={{ height: "300px", padding: "10px"}}
toolbar={{ image: { uploadCallback }}}
onEditorStateChange={editorState => setDescription(editorState)}\>

 

As you can see we have an onChange function we need to set up to change the editorState when something is entered into the editor. With react hooks this is very simple with the setDescription function.

From here the WYSIWYG editor will be fully functional on the page. The WYSIWYG has other css properties at play here that we don’t need to worry about for now. This would be the wrapperClassName and editorClassName that are just taking in a CSS class and rendering the editor in a basic format. If you want to change the the look of the editor this is where you do it.

Step 3:  Handling images:

As we saw in the code for the editor, we have a toolbar attribute with a value which is an object containing the options for image upload. To upload images from your PC you need to explicitly specify a callback function which uploads the image into the server and returns the path to that image.

const uploadCallback = (file) => {
const formData = new FormData();
formData.append('file', file);
return new Promise((resolve, reject) => {
fetch('http://localhost:5000/uploadImage', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then( resData => {
console.log(resData)
resolve({ data: { link: resData } });
})
.catch(error => {
console.log(error)
reject(error.toString())
})
})
}

We will look into the API endpoint later. The endpoint, after saving the image, returns the path to that image. We resolve it to an object as seen in the snippet. For more details please refer to the official docs. Disclaimer: the image upload code works great when working with class components, but shows issue with functional component. I’ll update the blog and the repo when I can solve this issue.

Step 4: Submit the form in the onSubmit function:

const onSubmit = (e) => {
e.preventDefault()
const newPost = {
title: title,
description: convertToRaw(description.getCurrentContent())
}
console.log("POST: ",newPost)
fetch('http://localhost:5000/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newPost)
})
.then(res => res.json())
.then(data => {
console.log(data)
setTitle('')
setDescription(EditorState.createEmpty())
history.goBack()
})
.catch(err => console.log("ERROR:",err))
}

We use the function convertToRaw to convert the editor state into a JSON object that we can save in the database. Now I have encountered a problem with mongoDB while using mongoose. If any object is empty or null, it removes that object. But even if we don’t have any image file in our editor’s content, we need one field entityMap to be an empty object, rather than getting deleted. We can fix it in the database Schema, which I’ll get back to later.




Step 5: Display the data in the front-end

We display the data in the Post component file, Post.js using the functions stateToHTML and convertFromRaw. For this we’ll need these imports.

import { stateToHTML } from 'draft-js-export-html';
import { convertFromRaw } from 'draft-js'

Then we need to translate the data being fetched from the back-end into HTML. We create a function convertFromJSONToHTML where we pass the data, and return an object with the converted data. We use a try-catch block in case it encounters any error. We have to put the data into a div which Facebook’s React tells us to title “dangerouslySetInnerHTML”. This is a security measure, but as long as our data is returned from draft.js text editor, we don’t have any thing to be worried about.

The convert function:

const convertFromJSONToHTML = (text) => {
try{
return { __html: stateToHTML(convertFromRaw(text))}
} catch(exp) {
console.log(exp)
return { __html: 'Error' }
}
}

The HTML:

<div dangerouslySetInnerHTML={convertFromJSONToHTML(props.post.description)} > </div >
Now lets take a look at the backend code.

Step 6: Create the schema for the post content:

Using mongoose in the back-end, for a mongoDb database (I’m using a cloud service called Atlas), we create the Schema in the model folder in a file called Post.js

const PostSchema = new Schema({
title:{
type: String,
required: true
},
description: {
type: Object,
required: true
}
}, { minimize: false })

Here the second argument to the Schema constructor is a options object which must have the minimize property set to false, so that empty entityMap object is not removed from the editor’s json data.

 

Step 7: Create the API for uploading images in the WYSIWYG editor:

For file uploads we use the standard npm package multer and its standard implementation in an express app. The process is out of scope of this blog but I am sharing the code snippet.

const multer = require('multer')
app.use('/static', express.static(__dirname + '/uploads'))
const storage = multer.diskStorage({
destination: function(req, file, cb){
cb(null, './uploads')
},
filename: function(req, file, cb){
cb(null, new Date().toISOString().split(':')[0] + file.originalname)
}
})
const upload = multer({storage:storage})
app.post('/uploadImage', upload.single('file'), (req, res) => {
console.log("starting upload...", req.file)
res.json('http://localhost:5000/static/' + req.file.filename)
});

On successful upload, the path to the file is returned to the uploadCallback function of the editor. Now we can prepare to receive data from the editor to store in the database.

Step 8: Create the API for receiving and storing the data from the WYSIWYG editor:

Storing the data is very simple as we already have the model ready and it receives a simple JSON object. The implementation is very basic using express.Router() function. We are separating out this code from the index file because we have multiple APIs for Posts.

router.post('/', (req, res) => {
console.log(req.body)
const newPost = new Post({
title: req.body.title,
description: req.body.description
})
newPost.save()
.then(post => res.json(post))
.catch(err => res.status(500).json({"Error": err}))
})


Similarly for retrieving posts:

router.get('/:id', (req, res) => {
Post.findById(req.params.id)
.then(post => res.json(post))
.catch(err => res.json("Error: ", err))
})

Step 9: Add functionality to edit a Post in the react app:

The process is similar to what we saw in creating and displaying the post, but it is a mixture of both. We need to retrieve the data from the database and then convert it not to HTML but to a draft.js editor object which is compatible with our editor’s state. So first we initial the editor’s state with EditorState.createEmpty() like we did while creating a new post.

const [description, setDescription] = useState(EditorState.createEmpty())

But upon loading the data from the server convert the data to be used in the editor.

fetch(`http://localhost:5000/api/posts/${props.match.params.id}`)
.then(res => res.json())
.then(data => {
setTitle(data.title)
const contentState = convertFromRaw(data.description)
const editorState = EditorState.createWithContent(contentState)
setDescription(editorState)
})

The rest is similar to adding a new post.

 

Step 10: Add the API to update the post:

This is a simple PUT route to update the content in the database.
router.put('/:id', (req, res) => {
Post.findByIdAndUpdate(
req.params.id,
req.body,
{new: true},
(err, post) => {
if (err) return res.status(500).send(err);
return res.send(post);
}
)
})

For the complete code refer to my github repository. Reach out to me in case you find any trouble in doing this.




 





If you liked this article please comment and show your support and interest so that I’ll be motivated to continue this effort. Like our facebook page if you haven’t already. And if you have any questions please comment. I’ll try to reply all.

Comments

comments