Draft.js — rich text editor framework for React from Facebook
First five steps that you need to investigate
See this article in Russian (Эта статья на русском 🇷🇺).
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.