BLOG

Up and Running with Draft.js

Draft.js is a library for building a rich text / WYSIWYG editor, but unlike traditional libraries you may already be familiar with such as TinyMCE or CKEditor, Draft gives you much more granular control over the interface and usage of the editor. It’s a framework, not a fully baked editor for you to drop into your site.

The full working set of examples can be found on CodePen here: https://codepen.io/collection/nqqNak/

Creating a Plaintext Editor

Let’s start with the simplest thing we can do. We’ll mount the editor, let it accept some text, and output that text below when the user clicks the Save button.

Note that in order to run this I need the following dependencies:

  • React & React-DOM
  • Immutable.js (EditorState is stored as an Immutable Map)
  • Draft.js
// Here I'm treating Draft and React as globals,
// since I've added them with a script tag, not an import
const { Editor, EditorState } = Draft;

class MyEditor extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: EditorState.createEmpty(),
      output: ''
    };
  }
  render() {
    const { output, editorState } = this.state;
    const editorStyle = {
      border: '1px solid red',
      padding: 10
    };
    return (
      <div>
        <div>Type In the Red Box:</div>
        <div style={editorStyle}>
          <Editor editorState={editorState} onChange={(editorState) => this.editorChanged(editorState)} />
        </div>
        <button onClick={() => this.save()}>Save</button>
      
        <div>Output: {output}</div>
      </div>
    );
  }
  editorChanged(editorState) {
    this.setState({editorState});
  }
  save() {
    const { editorState } = this.state;
    const content = editorState.getCurrentContent().getPlainText();
    this.setState({
      output: content
    })
  }
}

ReactDOM.render(
  <MyEditor />,
  document.getElementById('app')
);

Run This on CodePen

The flow here is pretty straightforward:

  1. Create an empty EditorState with Draft.EditorState.createEmpty().
  2. Mount an instance of the Editor component in our render() method.
  3. When the onChange event of the editor fires, grab the content with editorState.getCurrentContent().getPlainText();. This will remove all the formatting and just give us the text typed into the box, separated by default by newline characters for each line.
  4. Set state in our component with this new editorState, which gets passed into the <Editor /> component in our render method.

Inline Styling - Adding a Bold button

Next we’ll improve on this with some basic inline styling by adding a Bold button. To do this we’ll need to bring in another library to help with the conversion to HTML. Draft doesn’t do this out of the box, but HubSpot has created a library draft-convert to help convert the Immutable object of EditorState into HTML.

We actually need to bring in two more libraries:

  • draft-convert
  • ReactDOMServer - a part of the React library that draft-convert relies on
const { Editor, RichUtils, EditorState } = Draft;
const { convertToHTML } = DraftConvert;

class MyEditor extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState:EditorState.createEmpty(),
      output: ''
    };
  }
  render() {
    const { output, editorState } = this.state;
    const editorStyle = {
      border: '1px solid red',
      padding: 10
    };
    const outputHtml = {
      __html: output
    }
    return (
      <div>
        <div>Type In the Red Box:</div>
        <div style={editorStyle}>
          <Editor editorState={editorState} onChange={(editorState) => this.editorChanged(editorState)} />
        </div>
        <button onClick={() => this.makeBold()}>Bold</button>
        <button onClick={() => this.save()}>Save</button>

        <div>Output: <span dangerouslySetInnerHTML={outputHtml} /></div>
      </div>
    );
  }
  editorChanged(editorState) {
    this.setState({editorState});
  }
  makeBold() {
    const editorState = RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD');
    this.setState({editorState});
  }
  save() {
    const { editorState } = this.state;
    const content = convertToHTML(editorState.getCurrentContent());
    this.setState({
      output: content
    })
  }
}

ReactDOM.render(
  <MyEditor />,
  document.getElementById('app')
);

Run This on CodePen

Here’s what we’ve added:

  • A new <button> [Bold], with a function makeBold()
  • In our makeBold() function, we set the editorState we’re saving in state to a new version created with RichUtils.toggleInlineStyle. Here we toggle the BOLD inline style, but we could also do the same thing for ITALIC and STRIKETHROUGH.
  • In our save() function, we leverage the convertToHTML function from draft-convert to convert editorState to HTML.
  • Finally, since our output is now HTML, we can’t just output it in our render method. Instead we’ll use dangerouslySetInnerHTML on a new <span> tag to output the HTML to our page.

Block Styling - Adding a H1 Button

Similar to inline styling, we can add block styling for things like lists and headers. Only a couple simple changes needed for that:

First, let’s remove makeBold() and replace it with makeHeader()

makeHeader() {
  const editorState = RichUtils.toggleBlockType(this.state.editorState, 'header-one');
  this.setState({editorState});
}

Here we’re toggling one of the built-in block types, header-one. You can see the full list here: Custom Block Rendering.

And then we just need to wire it up to our button:

<button onClick={() => this.makeHeader()}>H1</button>

Run This on CodePen

Importing Existing HTML

Of course, you’re not just going to want to save your HTML to the screen. You’ll probably persist it somewhere, so it can be rendered later. To do that we’ll just need to change our constructor to import HTML rather than create EditorState from EditorState.createEmpty(). Once again we’ll leverage draft-convert to help in the conversion process from HTML to EditorState.

constructor(props) {
  super(props);
  const htmlContent = props.existingHtml || '<p>Edit This Content</p>';
  this.state = {
    editorState: EditorState.createWithContent(convertFromHTML(htmlContent)),
    output: ''
  };
}

Run This on CodePen

Advanced Conversion of HTML

Finally, what if we need some type of customization of the HTML either imported or exported? For this, we’ll leverage the fact that draft-convert allows us to specify custom converters. To try that, we’ll create our own support for custom font colors. Since the user could potentially select any hex color, there are 3 parts to make this work.

First, a customStyleFn. This function will be called for each inline style during the rendering of the <Editor /> component. Here we’re looking for any styles that are in the format COLOR_<hexcolor>. This allows us to parse styles that will be in the format COLOR_#0000FF, and add a styles.color attribute on the node that matches the color we’ve selected. In this case #0000FF (blue).

const CUSTOM_STYLE_PREFIX_COLOR = 'COLOR_';
function customStyleFn(style) {
  const styleNames = style.toJS();
  return styleNames.reduce((styles, styleName) => {
    if(styleName.startsWith(CUSTOM_STYLE_PREFIX_COLOR)) {
      styles.color = styleName.split(CUSTOM_STYLE_PREFIX_COLOR)[1];
    }
    return styles;
  }, {});
}

That function is passed into the <Editor /> component:

<Editor
  editorState={editorState}
  customStyleFn={customStyleFn}
  onChange={(editorState) => this.editorChanged(editorState)}
/>

Next we need to make sure our conversion functions to/from HTML support this new style type. Our constructor function therefore will change to this:

constructor(props) {
  super(props);
  const htmlContent = props.existingHtml || '<span style="color:#0000FF;">Edit This Content</span>';
  this.state = {
    editorState: EditorState.createWithContent(convertFromHTML({
      htmlToStyle: (nodeName, node, currentStyle) => {
        if (nodeName === 'span' && node.style.color) {
          return currentStyle.add(`${CUSTOM_STYLE_PREFIX_COLOR}${node.style.color}`);
        } else {
          return currentStyle;
        }
      }
    })(htmlContent)),
    output: ''
  };
}

Finally, when we want to save to HTML, we need a corresponding function there too:

save() {
  const { editorState } = this.state;
  const content = convertToHTML({
    styleToHTML: (style) => {
      const spanStyle = {
        color: style.substr(CUSTOM_STYLE_PREFIX_COLOR.length)
      };
      if (style.indexOf(CUSTOM_STYLE_PREFIX_COLOR) === 0) {
        return <span style={spanStyle} />;
      }
    }
  })(editorState.getCurrentContent());
  this.setState({
    output: content
  })
}

Run This on CodePen

Interested in More?

Check out the WYSIWYG editor I built on top of Draft.js, using React and Redux. You can see more examples of how to build each individual action type such as bulleted lists or hyperlinks.

https://github.com/appcues/wysiwyg

calendartwitterfeedenvelopelinkedinangellistgithub-altbitbucketheart