Hello fellow ProseMirror users!
(this is my first post here so I apologize if I messed up)
Recently I’ve been tasked with integrating ProseMirror into a React application.
I found two solutions on GitHub exist, both are called react-prosemirror: tgecho/react-prosemirror and episodeyang/react-prosemirror. They are both quiet outdated though and don’t adhere to the requirements I got for my use case.
The requirements I have are such that I need to keep all editor chrome and
controls UI inside the React so that I can reuse React UI components I already
have elsewhere in my app. That means keeping ProseMirror only for the “editor
surface” (equals to EditorView
without any UI plugins) and editor state
transformations.
It turns out the code I need to achieve my goals is quiet minimal (and even elegant, I think). This is all thanks to well thought out ProseMirror design: state management, editor UI and editor controls/chrome are kept orthogonal.
The gist is (I think code is pretty self describing):
/**
* This wraps ProseMirror's EditorView into React component.
*/
class ProseMirrorEditorView extends React.Component {
props: {
/**
* EditorState instance to use.
*/
editorState: EditorState,
/**
* Called when EditorView produces new EditorState.
*/
onEditorState: EditorState => *,
};
_editorView: ?EditorView;
_createEditorView = (element: ?HTMLElement) => {
if (element != null) {
this._editorView = new EditorView(element, {
state: this.props.editorState,
dispatchTransaction: this.dispatchTransaction,
});
}
};
dispatchTransaction = (tx: any) => {
// In case EditorView makes any modification to a state we funnel those
// modifications up to the parent and apply to the EditorView itself.
const editorState = this.props.editorState.apply(tx);
if (this._editorView != null) {
this._editorView.updateState(editorState);
}
this.props.onEditorState(editorState);
};
focus() {
if (this._editorView) {
this._editorView.focus();
}
}
componentWillReceiveProps(nextProps) {
// In case we receive new EditorState through props — we apply it to the
// EditorView instance.
if (this._editorView) {
if (nextProps.editorState !== this.props.editorState) {
this._editorView.updateState(nextProps.editorState);
}
}
}
componentWillUnmount() {
if (this._editorView) {
this._editorView.destroy();
}
}
shouldComponentUpdate() {
// Note that EditorView manages its DOM itself so we'd ratrher don't mess
// with it.
return false;
}
render() {
// Render just an empty div which is then used as a container for an
// EditorView instance.
return <div ref={this._createEditorView} />;
}
}
Then using such component is quiet simple:
export default class RichTextEditor extends React.Component {
state: {editorState: EditorState};
constructor(props: RichTextEditorProps) {
super(props);
this.state = {
editorState: EditorState.create(...)
};
}
dispatchTransaction = (tx: any) => {
const editorState = this.state.editorState.apply(tx);
this.setState({editorState});
};
onEditorState = (editorState: EditorState) => {
this.setState({editorState});
};
render() {
const {editorState} = this.state;
return (
<div>
<div class="menu">
<UndoMenuButton
editorState={editorState}
dispatchTransaction={this.dispatchTransaction}
/>
</div>
<div class="editorview-wrapper">
<ProseMirrorEditorView
ref={this.onEditorView}
editorState={editorState}
onEditorState={this.onEditorState}
/>
</div>
</div>
);
}
}
Few things to note:
-
EditorState
is not managed by<ProseMirrorEditorView />
component but rather passed in as a prop by a container component which manages all the editor UI (along with<ProseMirrorEditorView />
). -
I implemented menu entirely in React (looking at how
prosemirror-menu
works).
This is how a generic <MenuButton />
is implemented with React. Note that it
receives editorState
and dispatchTransaction
props as essentials to allow to
render UI based on editor state and transform editor state with commands:
function MenuButton({
editorState,
dispatchTransaction,
children,
command,
isActive,
isAllowed,
}) {
const onMouseDown = (e: MouseEvent) => {
e.stopPropagation();
// so we don't steal focus from EditorView
e.preventDefault();
// this is like an `run` field in prosemirror-menu item spec
command(editorState, dispatchTransaction);
};
// this is like an `select` field in prosemirror-menu item spec
const disabled = isAllowed && !isAllowed(editorState);
// this is like an `active` field in prosemirror-menu item spec
const active = isActive && isActive(editorState);
return (
<button disabled={disabled} active={active} onMouseDown={onMouseDown}>
{children}
</button>
);
}
And the <UndoMenuButton />
looks like this:
import {undo} from 'prosemirror-commands'
function UndoMenuButton(props) {
return (
<MenuButton {...props} command={undo} isAllowed={undo}>
Undo
</MenuButton>
);
}
I really like how useful and general this component is — I can render it
anywhere I can access the editorState
and dispatchTransaction
and it would
work well.
If you have any feedback, I’d be glad to hear it! Thanks!