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:
- You have a wrapping component. On its state object, it has an
EditorState
instance. - The wrapping component renders an Editor component and passes in
editorState
as a prop. - The wrapping component also defines an
onChange
method that accepts an instance ofeditorState
as an argument. Whenever this method gets called, it updates its own state to be the new instance ofEditorState
- 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:
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! 👋
Top comments (12)
Hey Rose,
a little question about draft-js
is there a way to control how many images can user insert?
Thanks
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.Wow Rose, thanks alot for such a deep answer wasn't expecting it.
Also a great article surely will keep following!
You can use
contenidojs
. It's a library on top ofdraft-js
with a lot of ready-to-use utilities.I have an introduction post for that if someone is interested.
is it possible to create an app like
docs.google.com
with only draftjs and its blugins?just bolt and italic in the demo
This is the first post in a series of posts, as you move through you'll find the demo becomes more fleshed out.
Thanks
See tiptap for Vue!
This is a life saver, I have look thought the Internet for a detailed explanation but thank God I came across one 😊
Hello from Japan !
This all articles are very educational for me!
May I introduce Draft.js along with your article on my blog?