Using Draft.js for Custom Rich Text Editing in React
This article explains how Draft.js, a React‑based rich‑text editor framework, can be applied to build a customizable editor with support for structured content, immutable state management, custom block rendering, data conversion, and hook handling, providing code examples throughout.
Draft.js is a Facebook‑released rich‑text editor framework built on React, designed to manage editor state through immutable EditorState objects.
Typical use cases include articles with paragraphs and simple formatting, the need to embed images or product information, and projects that already use the React technology stack.
We chose Draft.js because its implementation is purely functional, encapsulating the editor state in immutable objects, offering a rich API for state manipulation, and allowing fine‑grained control over shortcuts, paste, and drag‑drop operations to keep the state clean.
The core concepts involve EditorState , which wraps a ContentState instance. ContentState holds an ordered set of ContentBlock objects and an entity map; inline style ranges are ignored for our simplified formatting needs.
Example of a minimal React component using Draft.js:
class QEditor extends React.Component {
constructor(props) {
super(props);
this.state = {editorState: EditorState.createEmpty()};
this.onChange = editorState => this.setState({editorState});
}
render() {
return
;
}
}Custom block rendering is achieved via the blockRendererFn method, which returns a component for blocks of type atomic :
function entityBlockRenderer(block) {
if (block.getType() === 'atomic') {
return {
component: Entity,
editable: false,
props: { key: block.key }
};
}
}To convert a ContentState to a plain JavaScript object, we iterate over its blocks and handle each type accordingly:
function convertToArticle(contentState) {
const blocks = contentState.getBlocksAsArray();
return blocks.map(block => {
let text;
let type = 'TEXT';
switch (block.type) {
case 'header-two':
type = 'STRONG_TEXT';
case 'unstyled':
text = block.text.trim();
if (text) {
return { type, content: text };
} else {
return null; // remove empty lines
}
case 'atomic':
let entityKey = block.getEntityAt(0);
if (!entityKey) return null;
let entity = contentState.getEntity(entityKey);
return { type: entity.type, content: entity.data };
default:
return null;
}
}).filter(item => !!item);
}Creating an EditorState from an article involves building blocks, generating entities, and constructing a characterList for each block. For custom atomic blocks we create immutable entities and insert a trailing empty line to aid cursor handling.
function convertFromArticle(article) {
const emptyEditor = EditorState.createEmpty();
let contentWithEntity = emptyEditor.getCurrentContent();
return {
contentBlocks: article.reduce(function(last, paragraph) {
let type = 'unstyled';
let characterList;
switch (paragraph.type) {
case 'IMG':
case 'PRODUCT':
contentWithEntity = contentWithEntity.createEntity(paragraph.type, 'IMMUTABLE', paragraph.data);
characterList = List([CharacterMetadata.create({
style: OrderedSet(),
entity: contentWithEntity.getLastCreatedEntityKey()
})]);
return last.concat([
new ContentBlock({ key: genKey(), type: 'atomic', depth: 0, text: '', characterList }),
new ContentBlock({ key: genKey(), type: 'unstyled', text: '\r', depth: 0, characterList: List([CharacterMetadata.create([{ style: OrderedSet(), entity: null }])]) })
]);
case 'STRONG_TEXT':
type = 'header-two';
case 'TEXT':
characterList = List(new Array(paragraph.data.length).fill('').map(() => CharacterMetadata.create({ style: OrderedSet(), entity: null })));
last.push(new ContentBlock({ key: genKey(), type, depth: 0, text: paragraph.data, characterList }));
return last;
}
}, []),
entityMap: contentWithEntity.getEntityMap()
};
}Hooks such as handleKeyCommand allow us to modify editor behavior; for example, clearing bold formatting when the user presses Enter after a bold block:
handleKeyCommand(command, editorState) {
switch (command) {
case 'split-block':
setTimeout(() => {
const { editorState } = this.state;
this.onChange(RichUtils.toggleBlockType(editorState, 'unstyled'));
}, 0);
break;
}
return false;
}In summary, we have built a React‑based rich‑text editor using Draft.js that supports custom content types, data import/export, and fine‑grained control through hooks, laying the groundwork for more sophisticated editing scenarios.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.