Using with React


#1

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!


#2

Thanks for writing that up – and it’s really great to hear that the library design made things easy. This use case was one of the things I had in mind when moving towards the current library structure.


#3

@andreypopp Are you using React components for some of the nodes? If yes, how do they look like?


#4

@Keats using React components for editor’s chrome — menus and the such. I’m planning to implement some nodes using React components too but I’m not yet there.


#5

Hi,

Are your React components shared on GitHub?