Draft.js — rich text editor framework for React from Facebook

First five steps that you need to investigate

Mikhail Shabrikov
10 min readJul 11, 2018

See this article in Russian (Эта статья на русском 🇷🇺).

Final demo

Draft.js is part of the huge infrastructure that Facebook builds around the React.js. This technology, originally created by Facebook engineers within the company, was presented to the community in February 2016 at the React Conf, and by now, the repository has collected more than 13,000 stars on Github.

Of course, it should be noted that Draft.js is a tool for solving a very narrow range of tasks, namely, text input management tasks and text editing tasks. It is presented as “Rich text editor framework for React” on the official website.

The potential of this technology is extremely high. See some examples of text editors made with Draft.js. Below, I will try to demonstrate the key features of this framework.

To the finish, we will accumulate a considerable code base. Therefore, we will have separate brunch in the repository and demo page at the end of each section. I will not dwell on the folder structure, configuration files for Webpack and styles. If you had an experience with React.js and Webpack, it should not be tricky for you.

Step 1. Adding an Editor component.

We start with the starting point branch. Here, we will only be interested in the src/components/DraftEditor/DraftEditor.js file.

We added theEditor component from draft-js and specified theonChange event handler. This component cannot do something extraordinary now. Let us, however, take a closer look at the above code, and then we will start to add more interesting behaviour. Along with theplaceholder property, we passed these properties to the component:

  • editorState — current editor state object.
  • onChange — a function that is called each time when a user interacts with the editor text area. A new editor state object will be passed to the function as the first argument.

As you can see from the code, we will store the current editor state in the state of the parent component and update it in the onChange method via setState. In the constructor method, we set the initial value of the editorState property using EditorState.createEmpty(). For those cases when the editor should appear on the page with predefined content, use EditorState.createWithContent method. Put console.log in theonChange method to see the current editor state in a browser console every time it changes. Pay attention that editorState itself is the Record instance of Immutable.js library, so we convert it into a Javascript object using thetoJS() method. Look at how complex the structure is of this object:

Here you can find information about the current content of the editor, about the selected text (selection), the full history of changes (undoStack/redoStack) and other data. By using immutable structures, we can store the change history in a way that is optimal for the memory and performance of an application. Try the standard hotkeys (Ctrl/Cmd + Z and Ctrl/Cmd + Shift + Z) to undo and redo the changes. Of course, now, our editor does not differ much from the standard textarea in its functionality, therefore, let us move on to the next step.

Step 2. Inline Styling

In this part, we consider how to use Draft.js to apply style to the text. We will implement the same behaviour that Medium has. We will show a toolbar with options for styling above the selected text.

Switch to the inline-stylization branch. We have a new component, src/components/InlineToolbar.js, and a file for storing utility functions utils/index.js. Now there are two of them - getSelectionRange to get data about the current selection:

and getSelectionCoords to get the coordinates of the selected fragment relative to the editor container:

It should be noted that these functions are not even related to Draft.js and use a standard browser Selection API. We use them in the onChange method of the DraftEditor component, where we not only update the editorState property of the component’s state but also check if there is a selected text in the editor (as mentioned above, the editorState object contains information about selection as well). In the case there is selected text, we calculate the coordinates needed to make our toolbar appeared directly above the selected text and update state of the component.

A new method also appeared in the DraftEditor component:

Here, we first use a method from the RichUtils module. It is a set of utilities for Draft.js and that we will subsequently use repeatedly.

RichUtils.toggleInlineStyle method gets the current editorState as the first argument and a string with a style name that should be applied (for example BOLD) as the second argument and returns new editor state.

The toggleInlineStyle method is passed to theonToggle property of the InlineToolbar component.

In the component, we call this in theonMouseDown event handler.

The toolbar will have three items, by clicking on which the selected text will be stylized with one of the following styles: BOLD, ITALIC and HIGHLIGHT. And here it should be noted that BOLD and ITALIC are standard types of styles for Draft.js. That is, when one of them is applied to a fragment of the text, Draft.js knows which inline styles should be added to the corresponding DOM-node. Let us define a customStyleMap object in the src/components/DraftEditor.js file.

The structure of this object should be as follows: keys — the names of the custom style, values — the corresponding css properties. If we need other custom styles, we add new properties to this object. Now we can pass this object to the eponymous property of the Editor component so the Draft.js will know which styles should be applied for the custom style type.

The last thing we have to consider in this part is the handleKeyCommand method. As you can see, from the code snippet above, it is also passed to the eponymous property of the Editor component. This method is necessary so our editor can handle the standard keyboard shortcuts.

Try to select the text and press Ctrl/Cmd + B or Ctrl/Cmd + I, the text will be stylized with a bold or italic font respectively. In case we need to define non-standard hotkeys, we will need to do a little extra work. An illustrative example of this is on the official documentation site.

Step 3. Adding links. Entities and Decorators in Draft.js.

In this part, we will add to our editor the ability to add links and in this example, we will consider two widely used concepts in Draft.js: Entities (specially annotated ranges of text that can have some metadata) and Decorators. Switch to the link-entity branch. Please note that the toolbar has a new element to add a link.

For simplicity, we will use the standard prompt dialog. Consider the setLink method that is called when clicking on the item in the toolbar.

Let us take a closer look at creating an Entity. To create an Entity, we should call the createEntity method, which takes two required arguments and one optional. Let’s look at each of them in our example:

  • LINK (string) — the type of the created Entity. Below, when we start to consider decorators, we will see an example of using this property.
  • SEGMENTED (string) — this property denotes the behavior of a range of text annotated with this entity object when editing the text range within the editor. Possible values are listed and explained in the documentation.
  • { url: urlValue } (object) — metadata that will be associated with this Entity instance (optional).

To render the link in the desired way, we use the concept of decorators in Draft.js. The decorator concept is based on scanning the contents of a given ContentBlock for ranges of text that match a defined strategy, then rendering them with a specified React component.

Let us create an instance of the CompositeDecorator class passing an array of objects with following properties: strategy — matching strategy function, component — the component by which this fragment will be rendered. Pass the instance to the createEmpty method. The code of the component and the matching function is shown below.

Step 4. Slider custom block

The ability to define custom blocks is probably the most powerful feature in the Draft.js arsenal. In this part, we implement the ability to add an images-slider to the editor. And this can be done simply by dragging images into the editor area.

In its final form, this functionality is stored in the custom-component branch of the demo repository (try how it works).

Note that there is a set of default blocks in Draft.js (a complete list of them can be found here). As with default styles, we don’t need a lot of work to add them to the editor. But the situation with a custom block is different.

In the case of a custom block, we need to create a separate react-component and inform the editor that it needs to render the block of a specific type [1], (in our case a SLIDER) using the specified component [2]. Define the customBlockRenderer function. It will get a function to update the current state of the editor as the first argument, and a function to get the current state of the editor as the second argument [3]. These functions we will pass to the props of our component. We also define RenderMap[4].We need it to inform the editor which element should be used as a wrapper [5] for the custom block.

The RenderMap and this.blockRenderFn method we pass to the eponymous properties of the Editor component [1]. The component has two more properties not previously considered: handleReturn and handleDroppedFiles. The first of them [2] is necessary to correctly handle when the user pressing the “Enter” button (a newline) for the case when the cursor is within our custom block. To the handleDroppedFiles property, we pass the event handler function which is triggered when a user drags and drop some files into the editor area [3].

The files array filters the file array selecting only images and if we have at least one image [1] and update the editor state using addNewBlockAt method. The implementation of this method you can see in the /src/utils/index.js file. The addNewBlockAt method gets as arguments: current state of the editor, the key of the text fragment that is the anchor of the current selection, type of a custom block and meta-data for the custom block (slides URLs in our case) [2]. To simplify the example we will create slides URLs using URL.createObjectURL — i.e. they will be blob-objects [3]. In the real world, we would have a method that sends the files to the server and gets URLs in the response. Let us consider only the code of updateData — method which is called after exit edit mode and updates the block metadata (in the edit mode, we can remove, add or reorder slides).

Step 5. Export editor state in html markup

Of course, a full-fledged text editor should be able to generate HTML markup which will be displayed on the user pages. We will consider how to export the current state of the editor to HTML markup in the last part of this publication.

Demo page, branch in the repository — markup-export.

We will use the draft-convert module. Let us define a set of rules according to which the content of our editor will be converted to an HTML string (see src/components/DraftEditor/converter.js file).

We need to define three functions: styleToHTML[1], blockToHTML[2] and entityToHTML [3] — their names speak for themselves — each of them returns a markup fragment based on the inline stylization, block, or entity types. Note that in the blockToHTML function, we saved the data associated with the block into slides variable, converted it to a JSON string, and set it to the data-slides data attribute [4].

These functions are used in a configuration object [1] that is passed to the convertToHTML method [2]. Calling this method will return a function that gets the contentState of the editor as the first argument and returns what we need — an HTML string.

We save the HTML string into the markup variable [1]. For clarity, we append a markup to the page and show in the console [2]. Since we expect to see a working slider on the page, we will need some more code. As you remember, we have saved an array of slides URLs in the data-slides attribute and set js-slider class to the element. Now, we can find all DOM-nodes with this class [3] and use the standard render method from React.js to render the ContentSlider component on the place of these nodes [4]. We pass the data necessary for the component via props. The ContentSlider component itself is a simplified EditorSlider component. We cleared it from the functionality that we need only in edit mode.

As a result, we got a text editor with basic functionality, which can be used as a basis for more complex applications.

I used many code recipes from this project. You must to investigate this if you want to figure out how really full-fledged text editor architecture should be organized.

--

--