/prosemirror-react-nodeviews

An example of how to use React components as NodeViews for ProseMirror

Primary LanguageTypeScript

ProseMirror React NodeViews

This is an example repo of how to use React FC components as NodeViews for ProseMirror

Screenshot

How to use:

Wrap your root component with ReactNodeViewPortalsProvider

Lets use React portals to preserve your app context (css-in-js, data, etc) when the NodeViews are rendered. ReactNodeViewPortalsProvider is a convenient way to help you with this.

import { createReactNodeView } from "./ReactNodeView";
import ReactNodeViewPortalsProvider from "./ReactNodeViewPortals";

const App: React.FC<Props> = props => {
  return (
    <ReactNodeViewPortalsProvider>
      <App {...props} />
    </ReactNodeViewPortalsProvider>
  );
};

export default App;

Loading ProseMirror with React components

This is how you initialize your ProseMirror editor

import React from "react";
import { useReactNodeViewPortals } from "./ReactNodeViewPortals";

const ProseMirror: React.FC<Props> = () => {
  const { createPortal } = useReactNodeViewPortals();
  const editorViewRef = useRef(null);

  const handleCreatePortal = useCallback(createPortal, []);
  const state = useMemo(() => {
    const doc = schema.nodeFromJSON(YOUR_PROSEMIRROR_SCHEMA);
    return EditorState.create({
      doc,
      plugins: [
        history(),
        keymap({ "Mod-z": undo, "Mod-y": redo }),
        keymap(baseKeymap)
      ]
    });
  }, []);

  const createEditorView = useCallback(
    editorViewDOM => {
      const view = new EditorView(editorViewDOM, {
        state,
        nodeViews: {
          heading(node, view, getPos, decorations) {
            return createReactNodeView({
              node,
              view,
              getPos,
              decorations,
              component: Heading,
              onCreatePortal: handleCreatePortal
            });
          },
          paragraph(node, view, getPos, decorations) {
            return createReactNodeView({
              node,
              view,
              getPos,
              decorations,
              component: Paragraph,
              onCreatePortal: handleCreatePortal
            });
          }
        },
        dispatchTransaction(transaction) {
          const newState = view.state.apply(transaction);
          handleChange(newState.doc.toJSON());
          view.updateState(newState);
        }
      });
    },
    [state, handleChange, handleCreatePortal]
  );

  useEffect(() => {
    const editorViewDOM = editorViewRef.current;
    if (editorViewDOM) {
      createEditorView(editorViewDOM);
    }
  }, [createEditorView]);

  return <div ref={editorViewRef}></div>;
};

export default ProseMirror;

Getting node props within your React components

Each of the React components have been wrapped with a context provider before sending it through the portal, so its easy to access the nodeview's props:

import { Heading } from "@chakra-ui/core";
import React from "react";
import { useReactNodeView } from "./ReactNodeView";

const HeadingBlock: React.FC = ({ children }) => {
  const context = useReactNodeView();
  const level = context.node?.attrs.level;
  return <Heading fontSize={`${7 - level}xl`}>{children}</Heading>;
};

export default HeadingBlock;
import { Box, Image, Text } from "@chakra-ui/core";
import React from "react";
import { useReactNodeView } from "./ReactNodeView";

const ImageBlock: React.FC = () => {
  const context = useReactNodeView();
  const attrs = context.node?.attrs;
  return (
    <Box>
      <Image alt={attrs?.alt} src={attrs?.src} />
      {attrs?.title && (
        <Text mt={2} color="gray.500" textAlign="center" fontSize="xs">
          {attrs.title}
        </Text>
      )}
    </Box>
  );
};

export default ImageBlock;