Skip to content

Commit afd0833

Browse files
committed
feat!: automatically use node view components
Remove the need to call `useNodeView` and render the portals manually. Make it happen automatically it `<ProseMirror>`. Remove `useNodeView` from the public API.
1 parent 1bb79ad commit afd0833

File tree

10 files changed

+85
-151
lines changed

10 files changed

+85
-151
lines changed

.yarn/versions/1266e9a0.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
releases:
2+
"@nytimes/react-prosemirror": major

README.md

Lines changed: 27 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,12 @@ yarn add @nytimes/react-prosemirror
3434
- [Building NodeViews with React](#building-nodeviews-with-react)
3535
- [API](#api)
3636
- [`ProseMirror`](#prosemirror)
37+
- [`react`](#react)
3738
- [`useEditorState`](#useeditorstate)
3839
- [`useEditorEventCallback`](#useeditoreventcallback-1)
3940
- [`useEditorEventListener`](#useeditoreventlistener-1)
4041
- [`useEditorEffect`](#useeditoreffect-1)
4142
- [`useNodePos`](#usenodepos)
42-
- [`useNodeViews`](#usenodeviews)
43-
- [`react`](#react)
4443

4544
<!-- tocstop -->
4645

@@ -280,7 +279,7 @@ semantics for ProseMirror's `handleDOMEvents` prop:
280279
want to prevent the default contenteditable behavior, you must call
281280
`event.preventDefault`.
282281

283-
You can use this hook to implement custom behavior in your NodeViews:
282+
You can use this hook to implement custom behavior in your node views:
284283

285284
```tsx
286285
import { useEditorEventListener } from "@nytimes/react-prosemirror";
@@ -306,22 +305,20 @@ function Paragraph({ node, children }) {
306305
}
307306
```
308307

309-
### Building NodeViews with React
308+
### Building node views with React
310309

311310
The other way to integrate React and ProseMirror is to have ProseMirror render
312-
NodeViews using React components. This is somewhat more complex than the
313-
previous section. This library provides a `useNodeViews` hook, a factory for
314-
augmenting NodeView constructors with React components, and `react`, a
315-
ProseMirror Plugin for maintaining the React component hierarchy.
311+
node views using React components. The `<ProseMirror>` component recognizes when
312+
a node view constructor returns a node view with a `component` property and it
313+
renders the React component into the ProseMirror DOM element using a portal. The
314+
node view constructor must return at least `dom` and `component` property, but
315+
can also return any other node view properties. To support React node views, the
316+
editor state must include the React plugin (see below).
316317

317-
`useNodeViews` takes a map from node name to an extended NodeView constructor.
318-
The NodeView constructor must return at least a `dom` attribute and a
319-
`component` attribute, but can also return any other NodeView attributes. Here's
320-
an example of its usage:
318+
Example usage:
321319

322320
```tsx
323321
import {
324-
useNodeViews,
325322
useEditorEventCallback,
326323
NodeViewComponentProps,
327324
react,
@@ -338,12 +335,11 @@ function Paragraph({ children }: NodeViewComponentProps) {
338335
return <p onClick={onClick}>{children}</p>;
339336
}
340337

341-
// Make sure that your ReactNodeViews are defined outside of
342-
// your component, or are properly memoized. ProseMirror will
343-
// teardown and rebuild all NodeViews if the nodeView prop is
344-
// updated, leading to unbounded recursion if this object doesn't
345-
// have a stable reference.
346-
const reactNodeViews = {
338+
// Make sure that your node views are defined outside of your copmonent, or are
339+
// properly memoized. ProseMirror will teardown and rebuild all node views if
340+
// the `nodeView` prop changes, leading to unbounded recursion if the reference
341+
// is not stable.
342+
const nodeViews = {
347343
paragraph: () => ({
348344
component: Paragraph,
349345
// We render the Paragraph component itself into a div element
@@ -355,21 +351,18 @@ const reactNodeViews = {
355351
}),
356352
};
357353

354+
// You must add the `react` plugin to use React node views.
358355
const state = EditorState.create({
359356
schema,
360-
// You must add the react plugin if you use
361-
// the useNodeViews or useNodePos hook.
362357
plugins: [react()],
363358
});
364359

365360
function ProseMirrorEditor() {
366-
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews);
367361
const [mount, setMount] = useState<HTMLElement | null>(null);
368362

369363
return (
370364
<ProseMirror mount={mount} nodeViews={nodeViews} defaultState={state}>
371365
<div ref={setMount} />
372-
{renderNodeViews()}
373366
</ProseMirror>
374367
);
375368
}
@@ -417,6 +410,17 @@ function MyProseMirrorField() {
417410
}
418411
```
419412

413+
### `react`
414+
415+
```tsx
416+
type react = Plugin<Map<number, string>>;
417+
```
418+
419+
A ProseMirror Plugin that assists in maintaining the correct hierarchy for React
420+
node views.
421+
422+
If you use React node views, then your `EditorState` _must_ include this plugin.
423+
420424
### `useEditorState`
421425

422426
```tsx
@@ -509,75 +513,3 @@ type useNodePos = () => number;
509513
Returns the node's current position in the document. Takes the place of
510514
ProseMirror's `getPos` function that gets passed to NodeView's, which is unsafe
511515
to use in React render functions.
512-
513-
This hook can only be used in React components rendered with
514-
[`useNodeViews`](#usenodeviews).
515-
516-
### `useNodeViews`
517-
518-
```tsx
519-
/**
520-
* Extension of ProseMirror's NodeViewConstructor type to include
521-
* `component`, the React component to used render the NodeView.
522-
* All properties other than `component` and `dom` are optional.
523-
*/
524-
type ReactNodeViewConstructor = (
525-
node: Node,
526-
view: EditorView,
527-
getPos: () => number,
528-
decorations: readonly Decoration[],
529-
innerDecorations: DecorationSource
530-
) => {
531-
dom: HTMLElement | null;
532-
component: React.ComponentType<NodeViewComponentProps>;
533-
contentDOM?: HTMLElement | null;
534-
selectNode?: () => void;
535-
deselectNode?: () => void;
536-
setSelection?: (
537-
anchor: number,
538-
head: number,
539-
root: Document | ShadowRoot
540-
) => void;
541-
stopEvent?: (event: Event) => boolean;
542-
ignoreMutation?: (mutation: MutationRecord) => boolean;
543-
destroy?: () => void;
544-
update?: (
545-
node: Node,
546-
decorations: readonly Decoration[],
547-
innerDecoration: DecorationSource
548-
) => boolean;
549-
};
550-
551-
type useNodeViews = (nodeViews: Record<string, ReactNodeViewConstructor>) => {
552-
nodeViews: Record<string, NodeViewConstructor>;
553-
renderNodeViews: () => ReactElement[];
554-
};
555-
```
556-
557-
Hook for creating and rendering NodeViewConstructors that are powered by React
558-
components. To use this hook, you must also include
559-
[`react`](#reactnodeviewplugin) in your `EditorState`.
560-
561-
`component` can be any React component that takes `NodeViewComponentProps`. It
562-
will be passed as props all of the arguments to the `nodeViewConstructor` except
563-
for `editorView`. NodeView components that need access directly to the
564-
EditorView should use the `useEditorEventCallback`, `useEditorEventListener` and
565-
`useEditorEffect` hooks to ensure safe access.
566-
567-
For contentful Nodes, the NodeView component will also be passed a `children`
568-
prop containing an empty element. ProseMirror will render content nodes into
569-
this element. Like in ProseMirror, the existence of a `contentDOM` attribute
570-
determines whether a NodeView is contentful (i.e. the NodeView has editable
571-
content that should be managed by ProseMirror).
572-
573-
### `react`
574-
575-
```tsx
576-
type react = Plugin<Map<number, string>>;
577-
```
578-
579-
A ProseMirror Plugin that assists in maintaining the correct hierarchy for React
580-
node views.
581-
582-
If you use `useNodeViews` or `useNodePos`, you _must_ include this plugin in
583-
your `EditorState`.

demo/main.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import "prosemirror-view/style/prosemirror.css";
1616
import React, { useCallback, useState } from "react";
1717
import { createRoot } from "react-dom/client";
1818

19-
import { ProseMirror, useNodeViews } from "../src/index.js";
19+
import { ProseMirror } from "../src/index.js";
2020
import type { NodeViewComponentProps } from "../src/index.js";
2121
import type { ReactNodeViewConstructor } from "../src/nodeViews/createReactNodeViewConstructor.js";
2222
import { react } from "../src/plugins/react.js";
@@ -87,7 +87,7 @@ function ListItem({ children }: NodeViewComponentProps) {
8787
return <li>{children}</li>;
8888
}
8989

90-
const reactNodeViews: Record<string, ReactNodeViewConstructor> = {
90+
const nodeViews: Record<string, ReactNodeViewConstructor> = {
9191
paragraph: () => ({
9292
component: Paragraph,
9393
dom: document.createElement("div"),
@@ -106,7 +106,6 @@ const reactNodeViews: Record<string, ReactNodeViewConstructor> = {
106106
};
107107

108108
function DemoEditor() {
109-
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews);
110109
const [mount, setMount] = useState<HTMLDivElement | null>(null);
111110
const [state, setState] = useState(defaultState);
112111

@@ -119,13 +118,12 @@ function DemoEditor() {
119118
<main>
120119
<ProseMirror
121120
mount={mount}
122-
state={state}
123121
nodeViews={nodeViews}
122+
state={state}
124123
dispatchTransaction={dispatchTransaction}
125124
>
126125
<Menu />
127126
<div ref={setMount} />
128-
{renderNodeViews()}
129127
</ProseMirror>
130128
</main>
131129
);

src/components/Editor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ import type { ReactNode } from "react";
44
import { EditorContext } from "../contexts/EditorContext.js";
55
import { useEditorView } from "../hooks/useEditorView.js";
66
import type { UseEditorViewOptions } from "../hooks/useEditorView.js";
7+
import { useNodeViews } from "../hooks/useNodeViews.js";
78

89
export interface EditorProps extends UseEditorViewOptions {
910
mount: HTMLElement | null;
1011
children?: ReactNode | null;
1112
}
1213

1314
export function Editor({ mount, children, ...options }: EditorProps) {
14-
const value = useEditorView(mount, options);
15+
const { nodeViews, renderNodeViews } = useNodeViews(options.nodeViews)
16+
const value = useEditorView(mount, { ...options, nodeViews });
1517
return (
16-
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
18+
<EditorContext.Provider value={value}>
19+
{children}
20+
{renderNodeViews()}
21+
</EditorContext.Provider>
1722
);
1823
}

src/components/__tests__/ProseMirror.test.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { EditorState } from "prosemirror-state";
55
import type { Transaction } from "prosemirror-state";
66
import React, { useEffect, useState } from "react";
77

8-
import { useNodeViews } from "../../hooks/useNodeViews.js";
98
import type { NodeViewComponentProps } from "../../nodeViews/createReactNodeViewConstructor.js";
109
import { react } from "../../plugins/react.js";
1110
import {
@@ -189,7 +188,7 @@ describe("ProseMirror", () => {
189188
return <p data-testid="paragraph">{children}</p>;
190189
}
191190

192-
const reactNodeViews = {
191+
const nodeViews = {
193192
paragraph: () => ({
194193
component: Paragraph,
195194
dom: document.createElement("div"),
@@ -199,7 +198,6 @@ describe("ProseMirror", () => {
199198

200199
function TestEditor() {
201200
const [mount, setMount] = useState<HTMLDivElement | null>(null);
202-
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews);
203201

204202
return (
205203
<ProseMirror
@@ -208,7 +206,6 @@ describe("ProseMirror", () => {
208206
nodeViews={nodeViews}
209207
>
210208
<div data-testid="editor" ref={setMount} />
211-
{renderNodeViews()}
212209
</ProseMirror>
213210
);
214211
}

src/hooks/__tests__/useNodeViews.test.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import React, { createContext, useContext, useState } from "react";
66
import { ProseMirror } from "../../components/ProseMirror.js";
77
import type { NodeViewComponentProps } from "../../nodeViews/createReactNodeViewConstructor.js";
88
import { react } from "../../plugins/react.js";
9-
import { useNodeViews } from "../useNodeViews.js";
109

1110
// Mock `ReactDOM.flushSync` to call `act` to flush updates from DOM mutations.
1211
jest.mock("react-dom", () => ({
@@ -49,7 +48,7 @@ describe("useNodeViews", () => {
4948
);
5049
}
5150

52-
const reactNodeViews = {
51+
const nodeViews = {
5352
list: () => ({
5453
component: List,
5554
dom: document.createElement("div"),
@@ -63,13 +62,11 @@ describe("useNodeViews", () => {
6362
};
6463

6564
function TestEditor() {
66-
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews);
6765
const [mount, setMount] = useState<HTMLDivElement | null>(null);
6866

6967
return (
7068
<ProseMirror mount={mount} nodeViews={nodeViews} defaultState={state}>
7169
<div ref={setMount} />
72-
{renderNodeViews()}
7370
</ProseMirror>
7471
);
7572
}
@@ -102,7 +99,7 @@ describe("useNodeViews", () => {
10299
);
103100
}
104101

105-
const reactNodeViews = {
102+
const nodeViews = {
106103
list: () => ({
107104
component: List,
108105
dom: document.createElement("div"),
@@ -116,13 +113,11 @@ describe("useNodeViews", () => {
116113
};
117114

118115
function TestEditor() {
119-
const { nodeViews, renderNodeViews } = useNodeViews(reactNodeViews);
120116
const [mount, setMount] = useState<HTMLDivElement | null>(null);
121117

122118
return (
123119
<ProseMirror mount={mount} nodeViews={nodeViews} defaultState={state}>
124120
<div ref={setMount} />
125-
{renderNodeViews()}
126121
</ProseMirror>
127122
);
128123
}

src/hooks/useEditorView.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useLayoutEffect, useMemo, useState } from "react";
77
import { flushSync } from "react-dom";
88

99
import type { EditorContextValue } from "../contexts/EditorContext.js";
10+
import type { ReactNodeViewConstructor } from "../nodeViews/createReactNodeViewConstructor.js";
1011

1112
import { useComponentEventListeners } from "./useComponentEventListeners.js";
1213

@@ -24,6 +25,7 @@ const EMPTY_STATE = EditorState.create({
2425
let didWarnValueDefaultValue = false;
2526

2627
export interface UseEditorViewOptions extends EditorProps {
28+
nodeViews?: Record<string, ReactNodeViewConstructor>;
2729
defaultState?: EditorState;
2830
state?: EditorState;
2931
plugins?: Plugin[];

src/hooks/useNodeViews.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
} from "../nodeViews/createReactNodeViewConstructor.js";
1515

1616
export function useNodeViews(
17-
nodeViews: Record<string, ReactNodeViewConstructor>
17+
nodeViews?: Record<string, ReactNodeViewConstructor>
1818
) {
1919
const [portals, setPortals] = useState({} as NodeViewsContextValue);
2020

@@ -47,17 +47,17 @@ export function useNodeViews(
4747
[]
4848
);
4949

50-
const reactNodeViews = useMemo(() => {
51-
const nodeViewEntries = Object.entries(nodeViews);
52-
const reactNodeViewEntries = nodeViewEntries.map(([name, constructor]) => [
50+
const wrappedNodeViews = useMemo(() => {
51+
const nodeViewEntries = Object.entries(nodeViews ?? {});
52+
const wrappedNodeViewEntries = nodeViewEntries.map(([name, constructor]) => [
5353
name,
5454
createReactNodeViewConstructor(constructor, registerPortal),
5555
]);
56-
return Object.fromEntries(reactNodeViewEntries);
56+
return Object.fromEntries(wrappedNodeViewEntries);
5757
}, [nodeViews, registerPortal]);
5858

5959
return {
60-
nodeViews: reactNodeViews,
60+
nodeViews: wrappedNodeViews,
6161
renderNodeViews: () => <NodeViews portals={portals} />,
6262
};
6363
}

0 commit comments

Comments
 (0)