Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images
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 renderingMedia
, 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 theprops.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:
- First, add
AtomicBlockUtils
to our list of imports in thePageContainer
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 ourEditorState
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!
¹ Note on Managing Focus:
“But Siobhan, aren’t you breaking the declarative paradigm by using a ref to call
focus()
directly on theEditor
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 publicfocus()
method available on the component. This allows you to use a ref within your higher-level component to callfocus()
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.