Skip to content

Commit

Permalink
Add support for uncontrolled ProseMirror component
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Jun 1, 2023
1 parent ae94a1a commit 12d07ee
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"node": false
},
"rules": {
"no-console": ["off", { "allow": "error" }],
"no-console": ["error", { "allow": ["error"] }],
"react-hooks/exhaustive-deps": [
"warn",
{
Expand Down
2 changes: 2 additions & 0 deletions .yarn/versions/27cac6b1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
"@nytimes/react-prosemirror": patch
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function ProseMirrorEditor() {
const [mount, setMount] = useState();

return (
<ProseMirror mount={mount} state={EditorState.create({ schema })}>
<ProseMirror mount={mount} defaultState={EditorState.create({ schema })}>
<div ref={setMount} />
</ProseMirror>
);
Expand Down Expand Up @@ -368,7 +368,7 @@ function ProseMirrorEditor() {
return (
<ProseMirror
mount={mount}
state={EditorState.create({ schema })}
defaultState={EditorState.create({ schema })}
nodeViews={nodeViews}
>
<div ref={setMount} />
Expand All @@ -383,13 +383,13 @@ function ProseMirrorEditor() {
### `ProseMirror`

```tsx
type ProseMirror = (props: {
dispatchTransaction: (tr: Transaction) => void;
editorProps: EditorProps;
editorState: EditorState;
mount: HTMLElement | null;
children?: ReactNode | null;
}) => JSX.Element;
type ProseMirror = (
props: {
mount: HTMLElement;
children: ReactNode;
} & DirectEditorProps &
({ defaultState: EditorState } | { state: EditorState })
) => JSX.Element;
```

Renders the ProseMirror View onto a DOM mount.
Expand Down
8 changes: 7 additions & 1 deletion demo/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { createRoot } from "react-dom/client";
import {
NodeViewComponentProps,
ProseMirror,
useEditorEffect,
useEditorState,
useNodeViews,
} from "../src/index.js";
import { ReactNodeViewConstructor } from "../src/nodeViews/createReactNodeViewConstructor.js";
Expand Down Expand Up @@ -47,7 +49,11 @@ function DemoEditor() {
return (
<main>
<h1>React ProseMirror Demo</h1>
<ProseMirror mount={mount} state={editorState} nodeViews={nodeViews}>
<ProseMirror
mount={mount}
defaultState={editorState}
nodeViews={nodeViews}
>
<div ref={setMount} />
{renderNodeViews()}
</ProseMirror>
Expand Down
14 changes: 7 additions & 7 deletions src/components/ProseMirrorInner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { DirectEditorProps } from "prosemirror-view";
import React, { useMemo } from "react";
import type { ReactNode } from "react";

import { EditorProvider } from "../contexts/EditorContext.js";
import { useComponentEventListeners } from "../hooks/useComponentEventListeners.js";
import { useEditorView } from "../hooks/useEditorView.js";
import { EditorProps, useEditorView } from "../hooks/useEditorView.js";

export type ProseMirrorProps = DirectEditorProps & {
export type ProseMirrorProps = EditorProps & {
mount: HTMLElement | null;
children?: ReactNode | null;
};
Expand Down Expand Up @@ -35,7 +34,6 @@ export type ProseMirrorProps = DirectEditorProps & {
export function ProseMirrorInner({
children,
dispatchTransaction,
state,
mount,
...editorProps
}: ProseMirrorProps) {
Expand All @@ -53,18 +51,20 @@ export function ProseMirrorInner({
const editorView = useEditorView(mount, {
...editorProps,
plugins,
state,
dispatchTransaction,
});

const editorState =
"state" in editorProps ? editorProps.state : editorView?.state ?? null;

const editorContextValue = useMemo(
() => ({
editorView,
editorState: state,
editorState,
registerEventListener,
unregisterEventListener,
}),
[editorView, state, registerEventListener, unregisterEventListener]
[editorState, editorView, registerEventListener, unregisterEventListener]
);

return (
Expand Down
79 changes: 61 additions & 18 deletions src/hooks/useEditorView.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Transaction } from "prosemirror-state";
import type { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import type { DirectEditorProps } from "prosemirror-view";
import { useLayoutEffect, useState } from "react";
import { unstable_batchedUpdates as batch } from "react-dom";

import { useForceUpdate } from "./useForceUpdate.js";

function withBatchedUpdates<This, T extends unknown[]>(
fn: (this: This, ...args: T) => void
): (...args: T) => void {
Expand All @@ -14,12 +16,19 @@ function withBatchedUpdates<This, T extends unknown[]>(
};
}

const defaultDispatchTransaction = function (
this: EditorView,
tr: Transaction
) {
batch(() => this.updateState(this.state.apply(tr)));
};
function defaultDispatchTransaction(this: EditorView, tr: Transaction) {
this.updateState(this.state.apply(tr));
}

type EditorStateProps =
| {
state: EditorState;
}
| {
defaultState: EditorState;
};

export type EditorProps = Omit<DirectEditorProps, "state"> & EditorStateProps;

/**
* Enhances editor props so transactions dispatch in a batched update.
Expand All @@ -33,13 +42,25 @@ const defaultDispatchTransaction = function (
* the Editor View unmodified after we upgrade to React 18, which batches every
* update by default.
*/
function withBatchedDispatch(props: DirectEditorProps): DirectEditorProps {
function withBatchedDispatch(
props: EditorProps,
forceUpdate: () => void
): EditorProps & {
dispatchTransaction: EditorView["dispatch"];
} {
return {
...props,
...{
dispatchTransaction: props.dispatchTransaction
? withBatchedUpdates(props.dispatchTransaction)
: defaultDispatchTransaction,
dispatchTransaction: function dispatchTransaction(
this: EditorView,
tr: Transaction
) {
const batchedDispatchTransaction = withBatchedUpdates(
props.dispatchTransaction ?? defaultDispatchTransaction
);
batchedDispatchTransaction.call(this, tr);
forceUpdate();
},
},
};
}
Expand All @@ -55,12 +76,26 @@ function withBatchedDispatch(props: DirectEditorProps): DirectEditorProps {
*/
export function useEditorView<T extends HTMLElement = HTMLElement>(
mount: T | null,
props: DirectEditorProps
props: EditorProps
): EditorView | null {
const [view, setView] = useState<EditorView | null>(null);

props = withBatchedDispatch(props);
const { state, ...nonStateProps } = props;
const forceUpdate = useForceUpdate();

const editorProps = withBatchedDispatch(props, forceUpdate);

const stateProp = "state" in editorProps ? editorProps.state : undefined;

const state =
"defaultState" in editorProps
? editorProps.defaultState
: editorProps.state;

const nonStateProps = Object.fromEntries(
Object.entries(editorProps).filter(
([propName]) => propName !== "state" && propName !== "defaultState"
)
);

useLayoutEffect(() => {
return () => {
Expand All @@ -81,18 +116,26 @@ export function useEditorView<T extends HTMLElement = HTMLElement>(
}

if (!view) {
setView(new EditorView({ mount }, props));
setView(
new EditorView(
{ mount },
{
...editorProps,
state,
}
)
);
return;
}
}, [mount, props, view]);
}, [editorProps, mount, state, view]);

useLayoutEffect(() => {
view?.setProps(nonStateProps);
}, [view, nonStateProps]);

useLayoutEffect(() => {
view?.setProps({ state });
}, [view, state]);
if (stateProp) view?.setProps({ state: stateProp });
}, [view, stateProp]);

return view;
}

0 comments on commit 12d07ee

Please sign in to comment.