In this post I want to discuss one way of persisting data to a back end (an API) using a Draft JS Wysiwyg editor.
To follow along with this post, you'll need to be familiar with Draft JS, Draft JS Wysiwyg, and React JS. I provide a decent introduction to Draft JS and Draft JS Wysiwyg along with external links in the first two sections of my post Stabilizing Your Draft JS Wysiwyg Editor .
In order to save the contents of the editor to a database there are a couple of things that need to be set up first, besides setting up the editor itself. We need a routing system for our app so that when we click on"Edit" for a specific blog post, the editor loads the clicked blog post, and when we click on "New Post", a blank editor is loaded. We also need to connect our app to an API to which we send the editor's data upon creating or updating a post, and from where we receive the data for blog posts to display them on our app. To demonstrate such a setup, I created a codesandbox that uses React for the frontend, React Router v6 for the routes, and a Json server for the back end API (the database). You can find the codesandbox below. [Note: if you see a 502 error message in the codesandbox, refresh this page, and the codesandbox should load properly; if it still does not work, open the codesandbox, give it a minute to run the scripts, and then click on the refresh page button in the browser inside the codesandbox]
I invite you to click on the "Open preview in new window" button in the left section of the codesandbox (this will open the app alone in a new tab), and then click around to see how the routes change as you click on the different links. There are two entry points to the Draft JS Wysiwyg editor: either, clicking on "New Post" or clicking on "Edit" in the show page of a post. Let us see how we can write code that utilizes the app's routing system to load a blank editor or an editor with contents depending on the route we are in. Consider the below code snippet, which defines our Wysiwyg editor component:
// WysiwygDataPersistence.js
import React, { useState } from "react";
import { EditorState, convertToRaw } from "draft-js";
import { Editor } from "react-draft-wysiwyg";
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
import draftToHtml from "draftjs-to-html";
import { convertFromHTML } from "draft-convert";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import { addPost, editPost } from "../../actions/postActions";
import { validPost } from "./validator";
function WysiwygDataPersistence({ posts }) {
const routeParams = useParams();
const location = useLocation();
const navigate = useNavigate();
const [title, setTitle] = useState("");
const [editorState, setEditorState] = useState(() => {
if (location.pathname === "/posts/new") {
return EditorState.createEmpty();
} else if (routeParams.postId) {
const currentPost =
posts && posts.find(({ id }) => `${id}` === routeParams.postId);
setTitle(currentPost.title);
return
EditorState.createWithContent(convertFromHTML(currentPost.body));
}
});
const onEditorStateChange = (editorState) => {
setEditorState(editorState);
};
function publish() {
const body =
draftToHtml(convertToRaw(editorState.getCurrentContent()));
if (validPost(title, body)) {
const postData = { title, body };
addPost("posts", postData, navigate);
} else {
console.log("Posts need to include a title and a body");
}
}
function update() {
const body =
draftToHtml(convertToRaw(editorState.getCurrentContent()))
const id = routeParams.postId;
if (validPost(title, body)) {
const postData = { title, body };
editPost(id, postData, navigate);
} else {
console.log("Posts need to include a title and a body");
}
}
const handleTitle = (event) => {
setTitle(event.target.value);
};
let buttons;
if (location.pathname === "/posts/new") {
buttons = <button onClick={publish}>Publish</button>;
} else {
buttons = <button onClick={update}>Update</button>;
}
return (
<div className="textEditor">
<header className="posteditor-header">
<strong>Post Editor</strong>
</header>
<input
type="text"
placeholder="Title"
value={title}
onChange={handleTitle}
/>
<Editor
editorState={editorState}
wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
onEditorStateChange={onEditorStateChange}
/>
{buttons}
</div>
);
}
export default WysiwygDataPersistence;
Excluding the import lines, our WysiwygDataPersistence editor contains 80 lines of code. In many occasions, I have found myself reading articles trying to debug a problem, and the article misses to explain an important part of the code, which then leaves you in a gray area and asking yourself why the author did not explain the relevant code. Considering this, I'll do my best in explaining the full code snippet.
Let's start with the imports at the top of the code snippet. EditorState is an immutable object that holds the contents (the data) of the editor. We can call methods on this object to instantiate a blank editor or an editor with contents (more on this later).
convertToRaw is a function that converts the editor's immutable data into a JavaScript object. This JavaScript object can then later be transformed into other forms of data, such as HTML. Let's take a look at an example of a convertToRaw JavaScript object below:
As shown in the picture, each paragraph in the editor is saved in a blocks array in the convertToRaw JavaScript object. The JS object has two keys: "blocks", and "entityMap".
Editor is a component, and it renders the Wysiwyg editor. Its most important props are editorState and onEditorStateChange (refer to the return section of the code to see the props) (more on this later).
The react-draft-wysiwyg.css import contains CSS styles for the editor.
draftToHTML is a function that converts the JavaScript object returned by the converToRaw function into an HTML string. In the previous picture, you can see the HTML content returned by the draftToHTML function: the three <p> tags corresponding to the three paragraphs in the post.
convertFromHTML transforms an HTML string into data readable by the Draft JS editor.
We will skip the remainder import lines as they will be explained in the WysiwygDataPersistence component.
Let's now get into the WysiwygDataPersistence functional component.
In the component, the first three constants come from react router v6. Here the useParams hook provides us with relative paths (that is, whether we are in posts/1 or in posts/2; the 1 and the 2 are the relative paths). The useLocation hook provides us with absolute paths (whether we are in posts/new, in posts/1/edit, in authors/3 or in any other location). Finally, the useNavigate hook allows us to push the history of the browser (for example, when we create a new post, we will want to move from /posts to the show page of the newly created post, say, /posts/7, if the new post has an id of 7).
The title useState variable is used in a text input field for writing the title of the post (refer to the return section of the component).
Next, we define an editorState constant that will hold an instance of the EditorState object, which contains the data for the editor. We use the useState hook to define this constant, and initialize it via a callback function. If we are loading the editor for a new post, the callback function returns an instance of a blank EditorState object; if we are loading the editor for editing a post, we first find the incoming post using the useParams hook and the posts array coming from the back end API, and then we convert the HTML contents of the post into an object readable by the editor, and initialize an EditorState object with this content.
Since the WysiwygDataPersistence component is a controlled component, we define an onEditorStateChange arrow function that updates the editorState constant defined above and, consequently, updates the editor itself. Refer to the return section of the WysiwygDataPersistence component for usage of the onEditorStateChange function.
The publish function converts the editor's data into a JavaScript object through the converToRaw function, and the JS object into an HTML string via the draftToHTML method. If the post is valid (it has a title and a body), we pass the HTML string to the addPost function, which connects with the app's API to persist the post to the database, and pushes the browser's history to the newly created post's show page. The definition of the addPost function is not included in the editor component, but can be found in the source code for the codesandbox in this repository. If the post is invalid, we console log an error message.
The workflow for the update function is identical to that of the publish function, except that we need to include the id of the post being edited, and call an editPost function with the posts' HTML data
The title variable is a controlled variable, and it is updated using the handleTitle arrow function.
Next we define the "Publish" and "Update" buttons that can be found below the editor in the codesandbox. If we are loading a blank editor, the Publish button will show, otherwise, the Update button will show.
The return block renders the above defined button and the Draft JS Wysiwyg editor. With this code we can create, edit, and delete posts, and the changes will be saved. You can give it a try yourself.
You may refer to the source code for the codesandbox if you are interested in knowing how the app transfers the posts' data to the API in the addPost and editPost functions.
In summary, the code for the WysiwygDataPersistence component accomplishes two main tasks:
And there you have a fully working editor with data persistence.
Setting up the above codesandbox was a real challenge. I did not want to deploy a repository to production with the purpose of simply showing this demonstration. So I decided to set up the demo in a codesandbox instead. If you use a React JS codesandbox, you cannot save the contents of blog posts as the codesandbox does not count with a server nor a terminal to start a server (that is, you won't have a connection to a back end). If you use a Node JS codesandbox (this is the one I used), then you need to figure out how to configure the server and the client (the React JS app) to work properly in the codesandbox. One way of achieving this is by writing code to start the React app, and then manually opening a new terminal and run a command to start the server.
Since I did not want the visitors of the codesandbox to run any commands for the app to start working, I had to find a way to start both, the React client, and the Json server automatically when opening the codesandbox. One way to achieve this is by using the concurrently npm package which allows you to run multiple commands concurrently, including at app start up. So I configured the scripts of the package.json file to start the client and the server concurrently. But there was a problem: under this configuration, the server always starts first, and then the client.
The process that starts first will be shown in the sandbox's main browser window and stays fixed, and a new browser window opens for the second process. Since the page for the Json server is a blank page, when opening the codesandbox the user would simply see a blank page. Therefore, to solve the problem, I needed to find a way of delaying the execution of the code for the Json server until the React client started so that the main browser window is loaded with the React app. After a lot of research and many failed attempts, I got the solution with the wait-on npm package. This package allows you to specify that a given command should not execute until another command has finished executing first. Let's see the scripts section of the package.json for the codesandbox:
"scripts": {
"client": "react-scripts start",
"server": "wait-on http://localhost:3000 && nodemon server",
"start": "concurrently \"npm run server\" \"npm run client\""
},
We use concurrently to run the "npm run server" command, immediately followed by the "npm run client" command. Now, the "npm run server" command will not start to execute (will wait) until the connection to localhost:3000, executed by the "react-scripts start" command, has started. This way, when the codesandbox starts, the main browser window starts with the React app, which the user can see and interact with immediately.
Another thing to note is that the codesandbox does not use an external API for persisting the changes made in the app. All changes are saved in place in the db.json file located in the src folder in the main directory. In fact, you can open this file, and then make a change in the browser (like creating a post), and you will see how the file is overwritten with your changes in real time.
I hope you have found this post useful. For the moment, DevBlog does not include comments for blog posts, but I'll be adding this functionality soon.