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

  • 335