Jeremy Kithome Software Developer #MUFC To infinity and beyond! Fortune favours the bold. From tomato farmer to API farmer.

Building rich text editors in React using Draft.js and react-draft-wysiwyg

7 min read 2019

Building rich text editors in React using Draft.js and react-draft-wysiwyg

Introduction

Rich text editors have become integral in the way we interact with web applications especially when it comes to content generation. Rich text editors enable you to control the appearance of the text and provide a powerful way for content creators to create and publish HTML anywhere.

In this article, we will be using Draft.js and react-draft-wysiwyg to build a rich text editor and display the text we created using the editor. Draft.js is a rich text framework for React that provides the pieces that enable a developer to create a wide range or rich text composition possibilities varying from basic text styles to more powerful features such as embedded media. React-draft-wysiswg is a wysiwyg(what you see is what you get) editor built using React and Draft.js libraries. It provides a quick way to set up an editor with a lot of features out of the box so you don’t have to create them yourself.

Prerequisites

  • This tutorial assumes that you have a working knowledge of React
  • Ensure that you have Node, Yarn, or npm installed on your machine. You can follow the links for instructions on how to install them if you have not installed them already
  • We will be using create-react-app to bootstrap our project

Getting started

Let’s create our project in a directory of your choice. Any of the commands highlighted below can be typed in the command line to create your project.

npx:

$ npx create-react-app draft-js-example

npm (npm init <initializer> is available in npm 6+):

$ npm init react-app draft-js-example

yarn (yarn create is available in Yarn 0.25+):

These will generate a project folder with the following folder structure:

myapp-|
      |__node_mudles/
      |__public/
      |__src/
      |__.gitignore
      |__pacakge.json
      |__README.md
      |__yarn.lock

Navigate to the root of your project folder and run it:

cd draft-js-example
npm start //or
yarn start

This will run the app in development mode and you can view it in the browser using the link http://localhost:3000/.

react appjs in browser

We made a custom demo for .
No really. Click here to check it out.

Install additional dependencies

$ yarn add draft-js react-draft-wysiwyg

Setup editor

To get underway, we will need to make some edits to the src/App.js file. We will require the editor component and styles from react-draft-wysiwyg as well as EditorState from Draft.js. The editor component uses the default Draft.js editor without any styling. The Draft.js editor is built as a controlled ContentEditable component that is based on React’s controlled input API. EditorState provides a snapshot of the editor state. This includes the undo/redo history, contents, and cursor.

Let’s add some initial changes to display the editor:

import React, { useState } from 'react';
import { EditorState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import './App.css';
const App = () => {
  const [editorState, setEditorState] = useState(
    () => EditorState.createEmpty(),
  );
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor editorState={editorState} />
    </div>
  )
}
export default App;

We will start with an empty state created using the createEmpty method of EditorState. The editor component takes EditorState as a prop. You will notice after saving the changes and updates are displayed, the view doesn’t look great:

text editor example in browser

A few changes will be required for the App.css file. Replace its contents with the following:

.App-header {
   background-color: #282c34;
   min-height: 5vh;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   font-size: calc(10px + 2vmin);
   color: white;
   margin-bottom: 5vh;
   text-align: center;
 }

After updates, our App component will look something like this:

rendered editor

Styling the editor

If you look at the editor we rendered above, it is quite difficult to tell where one should enter text. The different sections of the editor can be made more apparent using style props. The props can either be the class to be applied for a particular section or an object containing the styles.

  • wrapperClassName=”wrapper-class”
  • editorClassName=”editor-class”
  • toolbarClassName=”toolbar-class”
  • wrapperStyle={<wrapperStyleObject>}
  • editorStyle={<editorStyleObject>}
  • toolbarStyle={<toolbarStyleObject>}

Add the className props to the Editor component and the relevant styles to App.css as follows to style the editor.

App.js:

<Editor
  editorState={editorState}
  onChange={setEditorState}
  wrapperClassName="wrapper-class"
  editorClassName="editor-class"
  toolbarClassName="toolbar-class"
/>

App.css:

.wrapper-class {
  padding: 1rem;
  border: 1px solid #ccc;
}
.editor-class {
  background-color:lightgray;
  padding: 1rem;
  border: 1px solid #ccc;
}
.toolbar-class {
  border: 1px solid #ccc;
}

When we reload the application, the editor should look something like this:
styled text editor

Editor state

The editor can either be a controlled or uncontrolled component. A controlled editor is achieved using EditorState (the top-level state object for the editor.) While an uncontrolled editor can be created using EditorState or RawDraftContentState ( a flow type that denotes the expected structure of the raw format of the contents).

If we want to create a controlled editor, we will pass it the following props:

  • editorState — Prop to update editor state in a controlled way
  • onEditorStateChange — A function called when there is a change in editor state that takes an object argument of type EditorState

After adding these, to our editor component, it will look like this:

const App = () => {
  const [editorState, setEditorState] = useState(
    () => EditorState.createEmpty(),
  );
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor
        editorState={editorState}
        onEditorStateChange={setEditorState}
        wrapperClassName="wrapper-class"
        editorClassName="editor-class"
        toolbarClassName="toolbar-class"
      />
    </div>
  )
}

EditorState can also be used for creating an uncontrolled editor by passing:

  • defaultEditorState — Object of type EditorState to initialize editor state with once it has been created:
const App = () => {
  const [editorState, setEditorState] = useState(
    () => EditorState.createEmpty(),
  );
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor
        defaultEditorState={editorState}
        onEditorStateChange={setEditorState}
        wrapperClassName="wrapper-class"
        editorClassName="editor-class"
        toolbarClassName="toolbar-class"
      />
    </div>
  )
}

The other way for achieving an uncontrolled editor is to use RawDraftContentState. The uncontrolled editor takes the following props:

  • defaultContentState — Object of type RawDraftContentState to initialize editor state with once it has been created
  • onContentStateChange — A function called when there is a change in editor state that takes an object argument of type RawDraftContentState

An uncontrolled editor implemented using RawDraftContentState would look like this:

import React, { useState } from 'react';
import { ContentState, convertToRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import './App.css';
const App = () => {
  let _contentState = ContentState.createFromText('Sample content state');
  const raw = convertToRaw(_contentState)
  const [contentState, setContentState] = useState(raw) // ContentState JSON
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor
        defaultContentState={contentState}
        onContentStateChange={setContentState}
        wrapperClassName="wrapper-class"
        editorClassName="editor-class"
        toolbarClassName="toolbar-class"
      />
    </div>
  )
}
export default App;

When rendered this will display the editor like this:
Uncontrolled editor using RawDraftContentState

Data conversion

As a user interacting with a rich text editor you will undoubtedly need to save the text or edit the saved text. This means that you should be able to convert the ContentState to raw JavaScript and vice versa. Draft.js provides three functions that enable you to do just this:

ContentFromRaw — Converts raw state(RawDraftContentState) to ContentState

ContentToRaw — Converts ContentState to raw state

ContentFromHTML — Converts an HTML fragment to an object with two keys one of which holds an array of ContentBlock objects and another holding a reference to the entityMap. This object can then be used to construct content state

For our particular use case, we want to convert the editor state to HTML for storage and display. We can get this done using libraries such as draftjs-to-html or draft-convert. We will use draft-convert since it has more features than draftjs-to-html. Run the following command in your terminal to install it:

yarn add draft-convert

Display rich text

Our editor is ready to create some rich text but it’s not ready to save or display the content that we have entered. You can play around with the editor and the different toolbar options to see all the different ways in which you can create rich text. The following is an example of different things I was able to do with the editor at this stage.

sample formatted text

The next step is to convert this rich text into HTML. We will use convertToHTML function from draft-convert to convert it but first, we need to get the current content entered in the editor.

EditorState provides a method getCurrentContent which returns the current content of the editor which we can then convert to HTML. The way we handle changes in editor state will have to change a bit. A handler will be called that will update the editor state and store the converted HTML. Let’s add a component method that will take the contents and convert them to HTML:

import { convertToHTML } from 'draft-convert';
....

const App = () => {
  ...
  const handleEditorChange = (state) => {
    setEditorState(state);
    convertContentToHTML();
  }
  const convertContentToHTML = () => {
    let currentContentAsHTML = convertToHTML(editorState.getCurrentContent());
    setConvertedContent(currentContentAsHTML);
  }
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor
        editorState={editorState}
        onEditorStateChange={handleEditorChange}
        wrapperClassName="wrapper-class"
        editorClassName="editor-class"
        toolbarClassName="toolbar-class"
      />
    </div>
  )
}

Please note that some of the editor formats may not be supported by draft-convert and may cause errors.

Now that the text entered in the editor is converted and saved to state, we can display it. We will create a div that will show the entered text. So as to display the text, we will use dangerouslySetInnerHTML. You may ask yourself whydangerously. By setting HTML from code, you expose yourself to cross-site scripting (XSS) attacks. The name is a subtle reminder of the dangers involved when you do this. You have to ensure that your HTML is properly structured and sanitized before adding it to your page. An easy way to do this is by using libraries such as dompurify. Install this package using the following command:

yarn add dompurify

dangerouslySetInnerHTML receives an object with a __html key(two underscores). This key will hold the sanitized HTML. We can now add the methods to sanitize the HTML and element to hold it:

import DOMPurify from 'dompurify';
...
const App = () => {
  ...
  const createMarkup = (html) => {
    return  {
      __html: DOMPurify.sanitize(html)
    }
  }
  return (
    <div className="App">
      ...
      <div className="preview" dangerouslySetInnerHTML={createMarkup(convertedContent)}></div>
    </div>
  )
}

The createMarkup function receives an HTML string as an argument and returns an object with the sanitized HTML. This method is called by dangerouslySetInnerHTML with the content that was converted to HTML.

Some styles for the HTML container:

.preview {
  padding: 1rem;
  margin-top: 1rem;
}

After saving and reloading, we can input text in the editor and see it displayed below the editor the same way we formatted it.
Displaying formatted text
That’s it! We are done. We have built a rich editor that enables us to format text in various ways. The final code for the App.js file should be:

import React, { useState } from 'react';
import { EditorState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import { convertToHTML } from 'draft-convert';
import DOMPurify from 'dompurify';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import './App.css';
const App = () => {
  const [editorState, setEditorState] = useState(
    () => EditorState.createEmpty(),
  );
  const  [convertedContent, setConvertedContent] = useState(null);
  const handleEditorChange = (state) => {
    setEditorState(state);
    convertContentToHTML();
  }
  const convertContentToHTML = () => {
    let currentContentAsHTML = convertToHTML(editorState.getCurrentContent());
    setConvertedContent(currentContentAsHTML);
  }
  const createMarkup = (html) => {
    return  {
      __html: DOMPurify.sanitize(html)
    }
  }
  return (
    <div className="App">
      <header className="App-header">
        Rich Text Editor Example
      </header>
      <Editor
        editorState={editorState}
        onEditorStateChange={handleEditorChange}
        wrapperClassName="wrapper-class"
        editorClassName="editor-class"
        toolbarClassName="toolbar-class"
      />
      <div className="preview" dangerouslySetInnerHTML={createMarkup(convertedContent)}></div>
    </div>
  )
}
export default App;

Conclusion

In this article, we looked at creating an editor that already has a toolbar with several options, entering text, and displaying the text. We barely scratched the surface of the experiences that can be created with react-draft-wysiwyg. You can find additional options and features in the react-draft-wysiwyg documentation and explore all the ways you can build a more featured rich editor. If you wish to challenge yourself more, you can add functionality to save the text to a database, retrieve it, and update it. The code for this article can be found on GitHub. I can’t wait to see what you build.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Jeremy Kithome Software Developer #MUFC To infinity and beyond! Fortune favours the bold. From tomato farmer to API farmer.

Leave a Reply