Intro To Rich Text Editing In React With Draft.js
Tuomo Kankaanpää
What is Draft.js?
- Framework for building rich text editors in React
- Introduced by Facebook in 2016
How does it work
- State and the representation of the editor are separated
- State updates trigger re-render
- Editor state is always in sync with the representation
EditorState
- Top level state object for the editor
- Immutable record that represents the entire state of the Draft editor
- Full snapshot of the state of the editor
- The current text content state
- The current selection state
- The fully decorated representation of the contents
- Undo/redo stack (ContentState objects)
ContentState
- Immutable Record
- Represents the entire contents of an editor: text, block and inline styles, and entity ranges.
- Represents two selection states of an editor: before and after the rendering of these contents.
- EditorState.getCurrentContent()
EditorState
ContentState
Undo/Redo stack
SelectionState before
SelectionState after
Editor contents
Exporting the state
- convertToRaw()
- converts ContentState to plain JSON
- convertFromRaw()
- converts plain JSON to ContentState
Basic Editor Example
import React from 'react';
import ReactDOM from 'react-dom';
import {Editor, EditorState} from 'draft-js';
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = (editorState) => this.setState({editorState});
}
render() {
return (
<Editor editorState={this.state.editorState} onChange={this.onChange} />
);
}
}
ReactDOM.render(
<MyEditor />,
document.getElementById('container')
);
{
"blocks": [
{
"key": "e0745",
"text": "Trick",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "212ar",
"text": "or",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "8m87r",
"text": "treat",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
}
],
"entityMap": {}
}RichUtils
import React from 'react';
import ReactDOM from 'react-dom';
import {Editor, EditorState, RichUtils} from 'draft-js';
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = (editorState) => this.setState({editorState});
}
handleKeyCommand(command) {
const { editorState } = this.state;
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
this.onChange(newState);
return true;
}
return false;
}
render() {
return (
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
handleKeyCommand={this.handleKeyCommand.bind(this)}
/>
);
}
}
ReactDOM.render(
<MyEditor />,
document.getElementById('container')
);
{
"blocks": [
{
"key": "616lk",
"text": "Trick or treat",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [
{
"offset": 6,
"length": 2,
"style": "BOLD"
},
{
"offset": 9,
"length": 5,
"style": "UNDERLINE"
}
],
"entityRanges": [],
"data": {}
}
],
"entityMap": {}
}Customisation properties
- blockStyleFn - apply styles for blocks
- blockRenderMap - map block types to html elements
- blockRendererFn - render custom components for specified block types
- customStyleMap - add custom inline styles for specific character ranges
draft-js-plugins

Static toolbar plugin

import React from 'react';
import ReactDOM from 'react-dom';
import {EditorState} from 'draft-js';
import Editor from "draft-js-plugins-editor";
import createToolbarPlugin from "draft-js-static-toolbar-plugin";
import editorStyles from "./editorStyles.css";
const staticToolbarPlugin = createToolbarPlugin();
const { Toolbar } = staticToolbarPlugin;
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = (editorState) => this.setState({editorState});
}
render() {
return (
<div className={editorStyles.editor}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[staticToolbarPlugin]}
/>
<Toolbar />
</div>
);
}
}
ReactDOM.render(
<MyEditor />,
document.getElementById('container')
);{
"blocks": [
{
"key": "9nm1k",
"text": "Trick or treat",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [
{
"offset": 0,
"length": 5,
"style": "ITALIC"
},
{
"offset": 9,
"length": 5,
"style": "UNDERLINE"
}
],
"entityRanges": [],
"data": {}
}
],
"entityMap": {}
}Emoji plugin

import React from 'react';
import ReactDOM from 'react-dom';
import {EditorState} from 'draft-js';
import Editor from "draft-js-plugins-editor";
import createEmojiPlugin from "draft-js-emoji-plugin";
import editorStyles from "./editorStyles.css";
const emojiPlugin = createEmojiPlugin();
const { EmojiSuggestions, EmojiSelect } = emojiPlugin;
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = (editorState) => this.setState({editorState});
}
render() {
return (
<div className={editorStyles.editor}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[emojiPlugin]}
/>
<EmojiSuggestions />
<EmojiSelect />
</div>
);
}
}
ReactDOM.render(
<MyEditor />,
document.getElementById('container')
);Mention plugin

import React from 'react';
import ReactDOM from 'react-dom';
import {EditorState} from 'draft-js';
import Editor from "draft-js-plugins-editor";
import createMentionPlugin, {
defaultSuggestionsFilter
} from "draft-js-mention-plugin";
import mentions from "./mentions";
import editorStyles from "./editorStyles.css";
class MyEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = (editorState) => this.setState({editorState});
this.mentionPlugin = createMentionPlugin();
this.onSearchChange = ({ value }) => {
this.setState({ suggestions: defaultSuggestionsFilter(value, mentions) });
};
}
render() {
const { MentionSuggestions } = this.mentionPlugin;
return (
<div className={editorStyles.editor}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[this.mentionPlugin]}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
/>
</div>
);
}
}Thank you!
bit.ly/wds_draftjs
twitter.com/tumee
github.com/tumetus
WD&S 11/2018
By tume
WD&S 11/2018
- 373