loading...

Rich text editing on the web: Getting started with Draft.js

rose profile image Rose ・8 min read

Once upon a time, not too long ago, rich text editors on the web were a huge challenge to implement. There were options, the most popular being TinyMCE, which came out in 2004, but it was an area that had a lot of room to be improved.

Fast forward to 2016: Facebook drops a new open source rich text editor as a companion to their wildly popular React library called Draft.js

Fast forward to today: There are many great rich text editor options (Slate, Quill, tiptap, and ProseMirror to name a few) out there. You can pick your favourite!

As you can tell from the title, I’m going to be writing about how to use Draft.js. So if your favourite is one of the other options I linked, you can just stop reading now 😉 😛


Purpose of this post/series of posts

I am hoping that this is the first of a series of blog posts on working with Draft.js. It’ll start off with the basics, and as I continue, the posts will become a bit more advanced.

👩‍💻 I am not the #1 expert on Draft.js, but I do work with it pretty regularly and have come up against a variety of weird situations in which I need to write some custom code to handle various specific requirements, as well as search out 3rd party libraries to extend Draft.js’s base functionality.

I am hoping to share some of what I have learned in a way that someone googling for a specific answer can find what they are looking for, but also if you are just a developer starting out and want to play around with Draft.js, you can follow along with these posts and do so.


What will I be building today in this article?

This very simple Draft.js demo is what we will be building


Enough with the introduction text, let’s get started!

⚠️ Warning: Draft.js is a framework meant to be used with React, so if you already have a project you want to add a rich text editor to, but it’s written using a different library, like Vue, you may want to look at more suitable editor options. JefMari/awesome-wysiwyg is a great resource for looking at all your options.

💁‍♂️ If you don’t already have a project that you want to add Draft.js to, you’ll need to get one started. I don’t want to waste your time having to get something all set up, so if you need a quick-start for working with Draft and don’t already have your own workflow in place for quickly starting a new project, I suggest you go to facebook/create-react-app and follow the instructions there. Set up should be very speedy! 💨

😅 OK so much set up and intro text, I am sorry 🙂 Just want to make sure this is as accessible as possible for all levels of developers!

Installing the draft-js dependency

Before you can start using Draft.js you need to install it using your package manager. Here is how that looks with npm in the terminal -

npm install --save draft-js

Rendering the editor in your app

From there, in the component where you want to render the draft editor, you’ll need to import two modules: The editor component itself, and EditorState, which is what we’ll use to build out and modify the editor’s content. (⚡️ Hint: If you feel like this is not enough information for you, you can always check out the official documentation on EditorState)

import { Editor, EditorState } from 'draft-js';

What exactly is EditorState?
The Draft editor uses EditorState to know exactly what needs to be rendered. It contains all information on what text is written, how it is formatted, and whether any text is currently selected.

It is immutable! Immutable means that you cannot make changes to it. In other words, every time you make a change to the content in the editor, a new instance of EditorState is created. This is important to know and I think an easy place to get tripped up and confused if you are just starting out.

EditorState has a number of static methods defined that you can use to create the initial EditorState for your editor, as well as modify content later on.

The general pattern with EditorState is this:

  1. You have a wrapping component. On its state object, it has an EditorState instance.
  2. The wrapping component renders an Editor component and passes in editorState as a prop.
  3. The wrapping component also defines an onChange method that accepts an instance of editorState as an argument. Whenever this method gets called, it updates its own state to be the new instance of EditorState
  4. The wrapping component passes onChange to the Editor as a prop. The Editor will call it whenever the editorState needs to be updated.

If you are using create-react-app you can open src/App.js and replace its contents with this:

import React from 'react';
import './App.css';
import { Editor, EditorState } from 'draft-js';
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: EditorState.createEmpty()
    };

    this.onChange = this.onChange.bind(this);
  }

  onChange (editorState) {
    this.setState({editorState});
  }

  render() {
    return (
      <div className="my-little-app">
        <h1>Playing with Draft.js!</h1>
        <div className="draft-editor-wrapper">
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange} />
        </div>
      </div>
    );
  }
}

export default App;

This code is basically an exact code representation of the 4 bullet points I outlined above 🙂 I added a few wrapping divs and headers just ‘cause I think they may be useful later.

The one extra thing you may see is that in the constructor we set up an initial instance of editorState using EditorState.createEmpty(). This is one of those static methods I mentioned. As you can probably tell from the naming, it creates an empty instance of EditorState for us to start with, meaning no text, no formatting, no nothing 🙂 a blank slate!

If you’re using create-react-app and load up the app at this point, the editor is there, but it’s hard to tell because the page is completely unstyled and the editor is empty. I updated App.css to look like this just so that it’s a bit easier to see what’s going on:

.my-little-app {
  width: 500px;
  margin: 50px auto;
}

.draft-editor-wrapper {
  border: 1px solid #ccc;
}

Ok, the editor is rendered! But it’s boring!

Try typing something and it works! You are done! Sort of. 😬

You may notice that although you are now technically using a rich text editor, it… isn’t very rich. 🎨

Draft.js has the power to make text bold, italic, etc, but by default it won’t do any of that unless you tell it to.

There are a few ways to go about adding this functionality. We could add some buttons that you click to insert formatting! Or we could add some keyboard shortcuts, like cmd-b (ctrl-b for you windows and linux users) to make text bold. Draft.js offers a module called RichUtils that can handle a lot of this for you. (⚡️ Reminder: If you feel like this is not enough information for you, you can always check out the official documentation on RichUtils)

Since this is just a post about getting started with the basics, let’s start off simple and make a couple of buttons for some basic styles: Bold and Italic.

We need to update our import to also include the RichUtils module -

import { Editor, EditorState, RichUtils } from 'draft-js';

Then I’m going to add a couple of buttons above my draft editor, and add onMouseDown listeners to them.

My App.js now looks like this:

import React from 'react';
import './App.css';
import { Editor, EditorState, RichUtils } from 'draft-js';
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: EditorState.createEmpty()
    };

    this.onChange = this.onChange.bind(this);
    this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
  }

  onChange (editorState) {
    this.setState({editorState});
  }

  toggleInlineStyle (event) {
    event.preventDefault();
    let style = event.currentTarget.getAttribute('data-style');
    this.setState({
      editorState: RichUtils.toggleInlineStyle(this.state.editorState, style)
    });
  }

  render() {
    return (
      <div className="my-little-app">
        <h1>Playing with Draft!</h1>
        <input
          type="button"
          value="Bold"
          data-style="BOLD"
          onMouseDown={this.toggleInlineStyle}
        />

        <input
          type="button"
          value="Italic"
          data-style="ITALIC"
          onMouseDown={this.toggleInlineStyle}
        />

    <div className="draft-editor-wrapper">
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange} />
        </div>
      </div>
    );
  }
}

export default App;

Some quick questions and answers here:

Why onMouseDown and not onClick?
Because we don’t want the Draft editor to lose its focus, so by listening onMouseDown and calling event.preventDefault() we stop that event from bubbling up and causing the editor to lose focus. So we can toggle styles and continue typing without having to click back into the editor first.

Why set a data-style on the element instead of making separate methods for each style?
Personal preference 🙂 I just thought it was nicer to have a single toggleInlineStyle method and infer which style based on the attribute on the element clicked.

Tell me what is happening in the toggleInlineStyle method!

First, we call preventDefault on the event so that we don’t blur the editor, as I previously mentioned.

Then we grab which style is being toggled from the element clicked.

Then we use the magic RichUtils ’s toggleInlineStyle method.

It takes the first argument as editorState and the second argument as a string that tells it which style to apply. It then returns a brand new editorState .

We then need to use this new editorState and call setState to update our existing editorState.

Calling setState, as you may know if you are familiar with React, will trigger a re-render which will then cause that new editorState to be passed to the Editor component as a prop and voila! The style has been toggled.

I read this sentence several times and worried it was too wordy and confusing. I tried to tweak it a bit. Hopefully it made sense to you 🙃

What inline styles does RichUtils support?

Out-of-the-box the following styles are supported:

  • BOLD
  • CODE
  • ITALIC
  • STRIKETHROUGH
  • UNDERLINE

You can also define your own inline styles, but that’s a more advanced topic 🙂

I think that’s enough for one blog post

You are probably bored and/or tired by now and so am I 😀

If you’ve been following along using create-react-app boilerplate you should have something like this by now:

Simple Draft.js Demo

I know this was a pretty basic start and I promise I will get into more interesting things as I write more of these posts. Here’s some topics I’d like to cover, and I’m sure there will be more 🙂

  • Placeholder value when the editor is empty
  • Pre-populating an editor with something other than an empty value
  • Keyboard shortcuts
  • Block formatting (blockquote, unordered list, etc)
  • Custom styles and custom entities
  • Soft newlines vs hard newlines
  • Saving to the back end
  • Rendering saved content outside of the editor itself
  • Adding emoji autocomplete and other fun extras
  • Manipulating content in more weird and wonderful ways (for example, maybe you want cmd-b to wrap text in the markdown **text** instead of applying an actual bold style. This is a thing that can be done, if you so desire.)
  • Have ideas for stuff you'd like to see? Let me know in the comments!

Thanks very much, and I hope that this has been useful for some of you! 👋

Discussion

pic
Editor guide
Collapse
omercohenaviv profile image
Omer Cohen

Hey Rose,
a little question about draft-js
is there a way to control how many images can user insert?
Thanks

Collapse
rose profile image
Rose Author

Hey! A slightly complicated answer: Yes you can but....

By default out-of-the-box draft.js doesn't support images at all. It's fairly simple to add basic support:

there's an article here: medium.com/@siobhanpmahoney/buildi...
Or you can use something like this pre-built plugin: draft-js-plugins.com/plugin/image

From there you'd have to add some kind of control for maximum images on your end and enforce the rule. Since you are the one that controls editorState being updated at all times, you have the power to veto any change. With images, if they are being added by a user through a button, you could disable the button whenever they exceed a total number of images, and re-enable when they are under the maximum. Tracking total images should be fairly simple by using the onChange callback by searching for and counting total number of the custom image entity, or you could check on-the-fly whenever the button is clicked.

Collapse
omercohenaviv profile image
Omer Cohen

Wow Rose, thanks alot for such a deep answer wasn't expecting it.
Also a great article surely will keep following!

Collapse
ahmedhlaimi profile image
AhmedHlaimi

just bolt and italic in the demo

Collapse
rose profile image
Rose Author

This is the first post in a series of posts, as you move through you'll find the demo becomes more fleshed out.

Collapse
ahmedhlaimi profile image
AhmedHlaimi

Thanks

Collapse
marcellothearcane profile image
marcellothearcane

See tiptap for Vue!