Using with React

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!

11 Likes

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 Likes

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

1 Like

@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.

1 Like

Hi,

Are your React components shared on GitHub?

1 Like

@andreypopp did you ever get around to adding react components in the schema? Working on that today and I would love any advice.

Hey @thelionthatiam - I’m also about to dive into this, did you make any meaningful progress?

I’ve got some React components wrapping a ProseMirror implementation right now (mostly derived from example repos in this forum), but I haven’t done any work making React components for the blocks themselves.

Might be worth looking at how the tiptap folks wrapped up Vue components. From my perspective it’s a pretty slick integration and allows leveraging Vue which has a lot of similarities to React.

Oh hey, that’s a good point. Thanks!

@thelionthatiam / @andreypopp

I just wanted to follow-up here and leave a trail of breadcrumbs for you. I did get ProseMirror and React blocks integrated this week, with some help from @saranrapjs

Here’s a gist demonstration of what I’m doing. It’s a workaround but it seems to hold up.

One thing I want to mention here, which isn’t shown in the above sample, is that cleanup needs to happen whenever these NodeView + React integrations are created. I haven’t had time to do it myself (this is a side project for me), but I believe it would usually look like a method on the NodeView which looks like this:

public destroy = () => {
  ReactDOM.unmountComponentAtNode(this.dom);
}

That should make sure you don’t spawn a bunch of React subtrees and leave them orphaned.

5 Likes

Is this standard JavaScript? I’m familiar with standard JavaScript and TypeScript. Alas, I don’t know what ?someVal means, nor () => *…

I’ve recently been working on a general project called remirror combining react and prosemirror. It’s available here in case you want to follow the progress.

It was initially intended to be a simple port from tiptap but has taken a life of its own and is now quickly becoming quite different.

Like tiptap it’s built on the concept of Extensions, MarkExtensions, and NodeExtensions which add functionality to the underlying prosemirror editor.

It heavily borrows NodeViews from Atlassian for injecting custom react components into the prosemirror editor as part of the plugin state.

Unlike both these editors it’s also built for injection into native mobile context via Expo, React Native and in the future flutter.

It’s still early days but I’ve been able to create some simple examples like:

Twitter UI

Epic Mode

4 Likes

@ifi, remirror looks really great! I really appreciate the fastidious and professional organization of the codebase. Providing a react-friendly layer on top of prosemirror that aims to be just as powerful is an ambitious project and it looks like you are delivering on the potential.

Previously I experimented with atlaskit - their extensive set of prosemirror plugins and integration with react was very interesting - but they don’t make it easy to decouple those components from their cloud provider apis (by design, I’m sure), and the ambiguity around their licenses and the massive scope of their codebase made me feel too anxious to continue exploring atlaskit. Looking forward to using remirror instead, and perhaps contributing to its success along the way.

2 Likes

@100ideas thanks so much for the feedback. There are a number of useful changes coming at some point this week, time permitting. The main change is what I believe to be a significantly simpler API for creating editors with React. You can see the proposal here.

I believe the main thing going forward will be getting the documentation to a level where anybody can jump right in.

I was hesitant to do that at the beginning since the code structure wasn’t quite settled and my understanding of the best way to build the project was constantly changing. Hopefully, now that the API is settling down, I’ll be able to dedicate more time to documentation and onboarding.

Sounds good!

I took a small step into the codebase last night by trying to get the simplest example in the docs working in my app’s react-styleguidist component library.

Copy-pastying the example code basically worked. Here’s a gist of the little editor component I was playing around with (it should be possible to put package.json in root and everything else in src then hit yarn; yarn run styles to get it to run locally).

One area I had to puzzle through was how to properly initialize the <RenderTree /> json viewer. I initially copied-and-pasted the output of the RenderTree component as json back into my source code so I could initialize the editor with more content. However, RenderTree didn’t know how to interpret bold or italic marks that the stock remirror editor generated. I eventually figured out how to pass in a modifed markMap. I didn’t try to hard to read all the documentation since I assumed it was in flux, so I may have missed the details about that. otherwise that info would be good to add to the docs.

I’d be happy to help with that.

Then I got stuck - I want to add a way for the user to to toggle the <EditorLayout> and/or <RenderTree> between menubar edit mode & direct markdown entry mode, a la https://prosemirror.net/examples/markdown/.

As a first step I just wanted to show/hide various components by triple-clicking the RenderTree or executing a special keystroke.

I saw that there are type definitions for prosemirror config props to do that sort of thing in remirror/support/types/prosemirror-view/index.d.ts#L406, but I couldn’t figure out where or how to pass in props to <RenderTree> - could you give me a hint or two?

// remirror/blob/master/support/types/prosemirror-view/index.d.ts#L406
/**
 * Props are configuration values that can be passed to an editor view
 * or included in a plugin. This interface lists the supported props.
 *
 * The various event-handling functions may all return `true` to
 * indicate that they handled the given event. The view will then take
 * care to call `preventDefault` on the event, except with
 * `handleDOMEvents`, where the handler itself is responsible for that.
 *
 * How a prop is resolved depends on the prop. Handler functions are
 * called one at a time, starting with the base props and then
 * searching through the plugins (in order of appearance) until one of
 * them returns true. For some props, the first plugin that yields a
 * value gets precedence.
 */
export interface EditorProps<S extends Schema = any> {
  /**
   * Can be an object mapping DOM event type names to functions that
   * handle them. Such functions will be called before any handling
   * ProseMirror does of events fired on the editable DOM element.
   * Contrary to the other event handling props, when returning true
   * from such a function, you are responsible for calling
   * `preventDefault` yourself (or not, if you want to allow the
   * default behavior).
   */

// skipping down a few lines...

   /**
   * Called when the editor is triple-clicked, after `handleTripleClickOn`.
   */
handleTripleClick?: ((view: EditorView<S>, pos: number, event: MouseEvent) => boolean) | null;

p.s. happy to move this convo over to your repo moving forward, just let me know.

@andreypopp Any updates on that?

@Storyteller So I was just randomly looking up what’s new here and I came past this post and your question. A while ago I had this same issue/goal of trying to implement my nodeviews using React. I looked up the current implementations and made up my own way here https://github.com/TeemuKoivisto/prosemirror-react-typescript-example

It’s not a complete setup that would be easily extendable with nice interfaces but it’s a start and I think you can at least use as a basis. There were some improvements I have on my other repo that I haven’t added yet since it was kinda muddy how I would add the React components with the plugins and state using Mobx in a nice way. I had a plan to make a general toolbox with all the required plumbing included but I got stuck with some other ProseMirror related problems (custom inline nodes are a pain in the ass).

Oh yeah and I should refactor it to use hooks. Hmm. Oh well. Quite busy right now with other stuff so will see when I have time to work on it.

2 Likes

Hey guys - I have been trying to render React components as NodeView too. I have created this repo to show what I am currently doing:

  1. Use a NodeView to render the component
  2. Instead of using ReactDOM.render, I am using ReactDOM.createPortal. I return the portal component to my editor component which then renders it on the page for it to access my app’s context
  3. I’ve wrapped the NodeView in a React context so that its props are available within the component using hooks

Is this the correct way to do it?

5 Likes

I like how you’ve done this a lot.

Remirror is going through an extensive rewrite and I’d love to borrow this. Maybe we could work together on it.

Some of the things I’m thinking through right now

  • React native support
  • Improved API
  • Completely extensible
  • Chainable commands
  • Better tests

I like how elegant your react node view implementation is. You’re definitely on the right track.

4 Likes

Thanks for this example, I did a proof of concept implementation based on this — https://gist.github.com/88ec11a788110a27f6a5ee0068c07f0e