diff --git a/prosemirror/use-a-svelte-component-as-a-nodeview.md b/prosemirror/use-a-svelte-component-as-a-nodeview.md new file mode 100644 index 0000000..f0931d6 --- /dev/null +++ b/prosemirror/use-a-svelte-component-as-a-nodeview.md @@ -0,0 +1,67 @@ +# Use a Svelte component as a NodeView + +The ProseMirror rich text editor library has a concept called `NodeView` for [rendering custom widgets with a document](https://prosemirror.net/docs/guide/#view.node_views). + +To use a NodeView, you write a "constructor" function that takes in the ProseMirror `Node`, `EditorView` and a function to get the node's position. That function is expected to set up the DOM for the custom widget and return an object implementing the [`NodeView` interface](https://prosemirror.net/docs/ref/#view.NodeView). + +The examples set up DOM using vanilla JS, but it's pretty simple to instead create a DOM element and render a framework component inside of it. This general approach should work for any framework — Svelte, React, Solid, etc — as well as with web components. + +TL;DR, this `SvelteNodeView` class creates an element with the given tag (by default `
`) inside of which it mounts a Svelte component. It spreads the node's attributes into the component's props, adding an additional `onchange` prop that can be called from within the component to set that attribute. + +Right now, it doesn't update the component if the node's attributes change outside. + +The static `create` method creates a constructor function given a Svelte component class and an optional tag name. + +```ts +import { type Node } from "prosemirror-model"; +import { type EditorView, type NodeViewConstructor } from "prosemirror-view"; +import { type SvelteComponent, type ComponentType, mount } from "svelte"; + +type Component = ComponentType< + SvelteComponent< + Record & { + onchange(name: string, value: any): void; + } + > +>; + +class SvelteNodeView { + node: Node; + view: EditorView; + getPos: () => number | undefined; + dom: Element; + + constructor( + comp: Component, + tag: string, + node: Node, + view: EditorView, + getPos: () => number | undefined + ) { + this.node = node; + this.view = view; + this.getPos = getPos; + + const target = (this.dom = window.document.createElement(tag)); + target.style.display = "contents"; + + mount(comp, { + target, + props: { + ...node.attrs, + onchange(name, value) { + const pos = getPos(); + if (pos === undefined) return; + + const tr = view.state.tr.setNodeAttribute(pos, name, value); + view.dispatch(tr); + } + } + }); + } + + static create(comp: Component, tag: string = "div"): NodeViewConstructor { + return (node, view, dom) => new SvelteNodeView(comp, tag, node, view, dom); + } +} +```