-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add cookbook entry for richtext * Add a cookbook entry for prosemirror+vanilla JS
- Loading branch information
Showing
4 changed files
with
343 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
--- | ||
sidebar_position: 2 | ||
--- | ||
|
||
# Prosemirror + React + Automerge | ||
|
||
Automerge supports rich text editing on top of [ProseMirror](https://prosemirror.net/). This guide will show you how to set up a simple collaborative rich text editor in React using Automerge and ProseMirror. | ||
|
||
All the code here can be found at https://github.com/automerge/automerge-prosemirror/examples/react | ||
|
||
First, create a an example vite app using the `@automerge/vite-app` template. This will give you a basic React app with the Automerge dependencies already installed. | ||
|
||
```bash | ||
yarn create @automerge/vite-app | ||
``` | ||
|
||
Then install our prosemirror dependencies | ||
|
||
|
||
```bash | ||
yarn add @automerge/prosemirror prosemirror-example-setup prosemirror-model prosemirror-state prosemirror-view | ||
``` | ||
|
||
Now, the app created by `@automerge/vite-app` creates a document which contains a `Counter`, but we want a `string` which will contain the text. Modify `main.tsx` so that the handle initialization logic looks like this: | ||
|
||
```jsx title="src/main.tsx" | ||
... | ||
let handle | ||
if (isValidAutomergeUrl(rootDocUrl)) { | ||
handle = repo.find(rootDocUrl) | ||
} else { | ||
handle = repo.create({text: ""}) | ||
} | ||
... | ||
``` | ||
|
||
First, let's create a basic skeleton component which just loads the document handle. The prosemirror bindings require that the document handle be loaded before we begin, so we'll add a bit of boilerplate to achieve this: | ||
|
||
```jsx title="src/App.tsx" | ||
import { AutomergeUrl } from "@automerge/automerge-repo" | ||
import { useHandle } from "@automerge/automerge-repo-react-hooks" | ||
import { useEffect, useState } from "react" | ||
|
||
function App({ docUrl }: { docUrl: AutomergeUrl }) { | ||
const handle = useHandle<{text: string}>(docUrl) | ||
const [loaded, setLoaded] = useState(handle && handle.docSync() != null) | ||
useEffect(() => { | ||
if (handle != null) { | ||
handle.whenReady().then(() => { | ||
if (handle.docSync() != null) { | ||
setLoaded(true) | ||
} | ||
}) | ||
} | ||
}, [handle]) | ||
|
||
return <div id="editor"></div> | ||
} | ||
|
||
export default App | ||
``` | ||
|
||
Now, we're going to create a ProseMirror editor. Prosemirror manages its own UI and state, it just needs to be attached to the DOM somehow. To achieve this we'll use the `useRef` hook to get hold of a reference to a dom element inside a React component which we can pass to prosemirror. | ||
|
||
```jsx title="src/App.tsx" | ||
import { AutomergeUrl } from "@automerge/automerge-repo" | ||
import { useHandle } from "@automerge/automerge-repo-react-hooks" | ||
// highlight-start | ||
import { useEffect, useRef, useState } from "react" | ||
import {EditorState} from "prosemirror-state" | ||
import {EditorView} from "prosemirror-view" | ||
import {exampleSetup} from "prosemirror-example-setup" | ||
import { AutoMirror } from "@automerge/prosemirror" | ||
import "prosemirror-example-setup/style/style.css" | ||
import "prosemirror-menu/style/menu.css" | ||
import "prosemirror-view/style/prosemirror.css" | ||
import "./App.css" | ||
// highlight-end | ||
|
||
function App({ docUrl }: { docUrl: AutomergeUrl }) { | ||
const editorRoot = useRef<HTMLDivElement>(null) | ||
const handle = useHandle<{text: string}>(docUrl) | ||
const [loaded, setLoaded] = useState(handle && handle.docSync() != null) | ||
useEffect(() => { | ||
if (handle != null) { | ||
handle.whenReady().then(() => { | ||
if (handle.docSync() != null) { | ||
setLoaded(true) | ||
} | ||
}) | ||
} | ||
}, [handle]) | ||
|
||
// highlight-start | ||
const [view, setView] = useState<EditorView | null>(null) | ||
useEffect(() => { | ||
// We're not using this for anything yet, but this `AutoMirror` object is | ||
// where we will integrate prosemirror with automerge | ||
const mirror = new AutoMirror(["text"]) | ||
if (editorRoot.current != null && loaded) { | ||
const view = new EditorView(editorRoot.current, { | ||
state: EditorState.create({ | ||
schema: mirror.schema, // It's important that we use the schema from the mirror | ||
plugins: exampleSetup({schema: mirror.schema}), | ||
doc: mirror.initialize(handle!, ["text"]) | ||
}), | ||
}) | ||
setView(view) | ||
} | ||
return () => { | ||
if (view) { | ||
view.destroy() | ||
setView(null) | ||
} | ||
} | ||
}, [editorRoot, loaded]) | ||
// highlight-end | ||
|
||
return <div id="editor" ref={editorRoot}></div> | ||
} | ||
|
||
export default App | ||
``` | ||
|
||
At this point if you run the application you'll find that there's a working prosemirror editor but it looks rubbish. Add the following to `src/App.css` and things will look a lot better: | ||
|
||
```css title="src/App.css" | ||
#root { | ||
max-width: 1280px; | ||
margin: 0 auto; | ||
padding: 2rem; | ||
display:flex; | ||
flex-direction: column; | ||
width: 100%; | ||
height: 100vh; | ||
} | ||
|
||
/* center the editor inside the #root */ | ||
#editor { | ||
margin: 0 auto; | ||
width: 100%; | ||
max-width: 800px; | ||
flex: 1; | ||
background-color: #f8f9fa; | ||
color: #333; | ||
} | ||
``` | ||
|
||
Alright, now we're ready to collaborate. | ||
|
||
Update `src/App.tsx` with the following changes: | ||
|
||
```jsx title="src/App.tsx" | ||
import { AutomergeUrl, DocHandleChangePayload } from "@automerge/automerge-repo" | ||
import { useHandle } from "@automerge/automerge-repo-react-hooks" | ||
import { useEffect, useRef, useState } from "react" | ||
import {EditorState, Transaction} from "prosemirror-state" | ||
import {EditorView} from "prosemirror-view" | ||
import {exampleSetup} from "prosemirror-example-setup" | ||
import { AutoMirror } from "@automerge/prosemirror" | ||
import "prosemirror-example-setup/style/style.css" | ||
import "prosemirror-menu/style/menu.css" | ||
import "prosemirror-view/style/prosemirror.css" | ||
import "./App.css" | ||
|
||
function App({ docUrl }: { docUrl: AutomergeUrl }) { | ||
const editorRoot = useRef<HTMLDivElement>(null) | ||
const handle = useHandle<{text: string}>(docUrl) | ||
const [loaded, setLoaded] = useState(handle && handle.docSync() != null) | ||
useEffect(() => { | ||
if (handle != null) { | ||
handle.whenReady().then(() => { | ||
if (handle.docSync() != null) { | ||
setLoaded(true) | ||
} | ||
}) | ||
} | ||
}, [handle]) | ||
|
||
const [view, setView] = useState<EditorView | null>(null) | ||
useEffect(() => { | ||
// We're not using this for anything yet, but this `AutoMirror` object is | ||
// where we will integrate prosemirror with automerge | ||
const mirror = new AutoMirror(["text"]) | ||
// highlight-start | ||
let view: EditorView // We need a forward reference to use next | ||
// This is a callback which will update the prosemirror view whenever the document changes | ||
const onPatch: (args: DocHandleChangePayload<unknown>) => void = ({ | ||
doc, | ||
patches, | ||
patchInfo, | ||
}) => { | ||
const newState = mirror.reconcilePatch( | ||
patchInfo.before, | ||
doc, | ||
patches, | ||
view!.state, | ||
) | ||
view!.updateState(newState) | ||
} | ||
// highlight-end | ||
if (editorRoot.current != null && loaded) { | ||
view = new EditorView(editorRoot.current, { | ||
state: EditorState.create({ | ||
schema: mirror.schema, // It's important that we use the schema from the mirror | ||
plugins: exampleSetup({schema: mirror.schema}), | ||
doc: mirror.initialize(handle!, ["text"]), | ||
}), | ||
// highlight-start | ||
// Here we're intercepting the prosemirror transaction and feeding it through the AutoMirror | ||
dispatchTransaction: (tx: Transaction) => { | ||
const newState = mirror.intercept(handle!, tx, view!.state) | ||
view!.updateState(newState) | ||
}, | ||
// highlight-end | ||
}) | ||
setView(view) | ||
// highlight-next-line | ||
handle!.on("change", onPatch) | ||
} | ||
return () => { | ||
// highlight-start | ||
// we have to remove the listener when tearing down | ||
if (handle != null) { | ||
handle.off("change", onPatch) | ||
} | ||
// highlight-end | ||
setView(null) | ||
if (view != null) { | ||
view.destroy() | ||
} | ||
} | ||
}, [editorRoot, loaded]) | ||
|
||
return <div id="editor" ref={editorRoot}></div> | ||
} | ||
|
||
export default App | ||
``` | ||
|
||
Now, you can load up the app in a different tab, or a different browser (the URL will contain a document URL after the `#`), and you can see changes being merged from one side to the other. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
--- | ||
sidebar_position: 3 | ||
--- | ||
|
||
# Prosemirror + VanillaJS + Automerge | ||
|
||
Automerge supports rich text using [ProseMirror](https://prosemirror.net/). This guide will show you how to set up a simple collaborative rich text editor in a vanilla JS app; where "vanilla" means plain JavaScript without any frameworks or libraries. | ||
|
||
We _do_ need a bundler in order to use Automerge, so we'll assume you have set up something like Vite and that you have two files, `index.html` and `main.js`. | ||
|
||
First, put the following in `index.html` | ||
|
||
```html title="index.html" | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<title>Prosemirror + Automerge</title> | ||
</head> | ||
<body> | ||
<div id="app"></div> | ||
<script type="module" src="/main.js"></script> | ||
</body> | ||
</html> | ||
``` | ||
|
||
First, we need to get `automerge-repo` set up: | ||
|
||
```js title="main.js" | ||
import { DocHandle, Repo, isValidAutomergeUrl } from "@automerge/automerge-repo" | ||
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" | ||
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket" | ||
|
||
const repo = new Repo({ | ||
storage: new IndexedDBStorageAdapter("automerge"), | ||
network: [new BrowserWebSocketClientAdapter("wss://sync.automerge.org")], | ||
}) | ||
``` | ||
|
||
Now, we'll store the automerge URL for the document we are editing in the browsers URL hash. This way, we can share the URL with others to collaborate on the document. | ||
|
||
```js title="main.js" | ||
// Get the document ID from the URL fragment if it's there. Otherwise, create | ||
// a new document and update the URL fragment to match. | ||
const docUrl = window.location.hash.slice(1) | ||
if (docUrl && isValidAutomergeUrl(docUrl)) { | ||
handle = repo.find(docUrl) | ||
} else { | ||
handle = repo.create({ text: "" }) | ||
window.location.hash = handle.url | ||
} | ||
// Wait for the handle to be available | ||
await handle.whenReady() | ||
``` | ||
|
||
At this point we have a document handle with a fully loaded automerge document, now we need to wire up a prosemirror editor. | ||
|
||
```js title="main.js" | ||
// This is the integration with automerge. | ||
const mirror = new AutoMirror(["text"]) | ||
|
||
// This is the prosemirror editor. | ||
const view = new EditorView(document.querySelector("#editor"), { | ||
state: EditorState.create({ | ||
doc: mirror.initialize(handle), // Note that we initialize using the mirror | ||
plugins: exampleSetup({ schema: mirror.schema }), // We _must_ use the schema from the mirror | ||
}), | ||
// Here we intercept the transaction and apply it to the automerge document | ||
dispatchTransaction: (tx) => { | ||
const newState = mirror.intercept(handle, tx, view.state) | ||
view.updateState(newState) | ||
}, | ||
}) | ||
|
||
// If changes arrive from elsewhere, update the prosemirror state and view | ||
handle.on("change", d => { | ||
const newState = mirror.reconcilePatch( | ||
d.patchInfo.before, | ||
d.doc, | ||
d.patches, | ||
view.state, | ||
) | ||
view.updateState(newState) | ||
}) | ||
``` | ||
|
||
Now, you can open `index.html` in your browser and start editing the document. If you open the same URL in another browser window, you should see the changes you make in one window reflected in the other. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters