Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images

Siobhan Mahoney
Jul 1, 2018 · 7 min read

This post is part of the second installment of a multi-part series about building a Rich Text Editor with React and Draft.js. For an introduction to the Draft.js framework with setup instructions, checkout Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up. Links to additional posts in this series can be found below. The full codebase for my ongoing Draft.js project is available on GitHub. Feel free to checkout the deployed demo here.


In this post, I will review how to build the functionality to allow image embedding in your React text editor using tools provided by the Draft.js API.

Draft.js can be installed with:

yarn add draft-js

For reference, I’ll be integrating these features into the React basic text editor I built for the demo discussed in the first post in this series, Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up, the code for which is available on CodeSandbox.


Overview of Relevant Draft.js API Building Blocks

Custom Block Components

As touched upon in my post covering how to build a key command and button for embedding links, a ContentBlock is an immutable record that represents the full state of a single block of editor content, and together, are structured in an OrderedMap, which makes up the editor’sContentState. The rendering of ContentBlocks may be customized to render rich media content instead of text.

Demo: Building the Image Embedding Functionality

The following demonstration is based on the Media Editor example provided in the official Draft.js documentation.

Defining a Custom Renderer Object

Custom Block rendering relies on the blockRendererFn prop, which, when passed to the Editor component as a prop, enables a higher-level component to define a rendering protocol based on the specified block type, text, or other criteria.

To begin, let’s build a function to define the blockRendererFn prop in our Editor component.

Create an ./entities directory within ./components, and within it create a file entitled mediaBlockRenderer.js. Start off mediaBlockRenderer.js by importing the following:

import React from "react";
import { EditorState, RichUtils, AtomicBlockUtils } from "draft-js";

Then write create mediaBlockRenderer:

export const mediaBlockRenderer = block => {
if (block.getType() === "atomic") {
return {
component: Media,
editable: false
};
}
return null;
};
  • Check if the block passed in as an argument has type atomic
  • Indicate the respective component to render (in this example, we’ll be rendering Media, which we will build in a sec)
  • Pass the optional props object includes props that will be passed through to the rendered custom component via the props.blockProps sub property object
  • Define the editable property asfalse since the component is will not include text content

We’ll then import mediaBlockRenderer into our PageContainer component and pass it to our Editor component as a prop. **Note: in doing so, you’ll also need to pass ref as a prop as well¹:

{...}
import { mediaBlockRenderer } from "./entities/mediaBlockRenderer";
class PageContainer extends React.Component {{...}render() {
return (
<Editor

{...}
ref="editor"
blockRendererFn={mediaBlockRenderer} />
);
}
}
export default PageContainer;

If there isn’t a custom renderer object for the blockRendererFn function to return, the Editor will render the default DraftEditorBlock text block component.

The benefit of defining the function within the context of a higher-level component is that it enables props to be bound to the custom component, allowing instance methods for custom component props.

Building the Media Custom Block Component and Image Entity

Next, we’ll create the Image entity and Media custom block component referenced in the object returned by the mediaBlockRenderer function:

const Image = props => {
if (!!props.src) {
return <img src={props.src} />;
}
return null;
};
const Media = props => {
const entity = props.contentState.getEntity(props.block.getEntityAt(0));
const { src } = entity.getData();
const type = entity.getType();

let media;

if (type === "image") {
media = <Image src={src} />;
}

return media;
};

Here, we first define Image entity type. We then create the Media component, we access the ContentBlock object and the ContentState record, which are made available within the custom component, along with the props (foo).

By grabbing the entity information from the ContentBlock and the Entity map, we can access the entity metadata needed to render our custom component.

Building addImage()

Now that we have a custom block component to render an image, let’s build the functionality to insert the image into our editor.

To do so, we’ll update our PageContainer component with the below steps:

  1. First, add AtomicBlockUtils to our list of imports in the PageContainer component:
import { EditorState, RichUtils, AtomicBlockUtils } from “draft-js”;

2. Define a focus() function:

focus = () => this.refs.editor.focus();

We’ll use focus() as a callback in the event listener responsible for adding the image (described in greater detail in the below step).

3. Finally, we’ll build our onAddImage() function:

onAddImage = (e) => {
e.preventDefault();
const editorState = this.state.editorState;
const urlValue = window.prompt("Paste Image Link");
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
"image",
"IMMUTABLE",
{ src: urlValue }
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const newEditorState = EditorState.set(
editorState,
{ currentContent: contentStateWithEntity },
"create-entity"
);
this.setState(
{
editorState: AtomicBlockUtils.insertAtomicBlock(
newEditorState,
entityKey,
" "
)
},
() => {
setTimeout(() => this.focus(), 0);
}
);
};

Here’s what’s happening:

  • The user is prompted to input the url source of an image
  • An entity is created with type (“image”), mutability (“IMMUTABLE”), and data ({src: url})
  • Using the key of the newly created entity, we can then update our the EditorState
  • We’ll call this.focus() once our EditorState has finished updating. This way, users will be able to immediately resume entering (or deleting) text upon the addition of the image¹.

Building ‘Add Image’ UI Button

Last but not least, we will create a UI button to which onAddImage will be passed as a callback function, allowing users to access the new feature we just built:

<button className="inline styleButton" onClick={this.onAddImage}>
<i class="material-icons">image</i>
</button>

Note: The button in my example is labeled with an icon from Google’s Material Icon catalogue.

And, there you go!

Demo Code is available here: https://codesandbox.io/embed/n0ozyqr9z4

¹ Note on Managing Focus:

But Siobhan, aren’t you breaking the declarative paradigm by using a ref to call focus() directly on the Editor component?

To the curious and incredulous readers who might be wondering why we are managing focus state here and whether this is advisable, this is indeed the recommended approach.

As highlighted in the DraftJS docs:

Managing text input focus can be a tricky task within React components. The browser focus/blur API is imperative, so setting or removing focus via declarative means purely through render()tends to feel awkward and incorrect, and it requires challenging attempts at controlling focus state.

With that in mind, at Facebook we often choose to expose focus()methods on components that wrap text inputs. This breaks the declarative paradigm, but it also simplifies the work needed for engineers to successfully manage focus behavior within their apps.

The Editor component follows this pattern, so there is a public focus() method available on the component. This allows you to use a ref within your higher-level component to call focus() directly on the component when needed.

The event listeners within the component will observe focus changes and propagate them through onChange as expected, so state and DOM will remain correctly in sync.


Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade