update: i implemented this, checkout https://bangle.dev
Hey Folks I have been working on a higher level tool kit for Prosemirror and wanted to get some feedback on some ideas.
I have broken down this post into 3 ideas.
Separation of schema and plugins
const emojiSuggestKey = new PluginKey('emojiSuggestKey');
const emojisArray = [
['japanese_ogre', '👹'],
['shallow_pan_of_food', '🥘'],
];
const schema = new Schema([
...coreSpec(),
emojiSuggest.spec({ markName: 'emojiSuggest', trigger: ':' }),
])
const plugins = () => [
...corePlugins(),
emojiSuggest.plugins({
key: emojiSuggestKey,
emojis: emojisArray,
markName: 'emojiSuggest',
}),
]
export default function Example() {
const editorState = useEditorState({
schema,
plugins: plugins(),
initialValue: 'Hello there!',
});
return (
< EditorView state={editorState}>
<EmojiSuggest emojiSuggestKey={emojiSuggestKey} emojis={emojisArray} />
</EditorView>
);
}
I have seen some libraries (tiptap) mixing node/mark spec and PM plugins in a single class which sort of works as a plug-n-play component. I initially took the same approach, but then hit a roadblock where I wanted to share the schema between multiple editor views and a collaboration server. Sharing the schema is doable but I found the coupling of everything in one giant class is an uphill battle against the modular architecture of PM. I am curious to hear what folks think about this, do the benefits of fusing schema, plugins, keybindings, etc in one class help in larger and more complex applications ?
Using React hooks
export function EmojiSuggest({ emojiSuggestKey, emojis }) {
const view = useEditorViewContext();
const emojiPluginState = usePluginState(emojiSuggestKey);
// Extract the DOM Node from the plugin state to demonstrate a one way data flow from
// PM to react component.
return reactDOM.createPortal(<SomeUIComponent />, emojiPluginState.tooltipContentDOM)
}
In the example above I am using hooks to persist (between renders) creation of EditorState and bunch of other things. The benefit of custom hooks like these is that it makes things modular – similar to PM’s architecture.
I struggled with a bunch of ways to solve the ‘sharing of PM state with XYZ React component’ and settled with saving related things in a Plugin state and then passing it down as a prop as shown in the example above. Every time the plugin’s state changes, usePluginState
will re-render the EmojiSuggest component. In my opinion the biggest benefit is the practice of uplifting the state to parent component and passing it down to children. A parent component in this case happens to be a PM plugin.
A problematic aspect of linking React and Prosemirror is the rerendering on every transaction. Prosemirror expects you to have numerous transactions, each having a fast turn around time, but if each transaction triggered a React update our application would come to a grinding halt. In the example above I am leveraging the plugin’s state to shield the React component from only updating when the plugin’s state changes. Some easy wins with this approach is the managing of DOM node which is more appropriately handled by PM and then passed to React to render.
Extensibility
This is not really related to React but would love to bounce some ideas on this. The plugin architecture of PM works great, but PM leaves ambiguity on how to extend and share specs and plugins.
To give a hypothetical example:
- We have a PM tooltip module which contains 2 plugins.
- We have an Emoji Suggest module which builds on top of tooltip module and also introduces its own plugins.
- The consumer of Emoji Suggest library might already be using the tooltip library.
What would be the best way to connect these components, without causing dependency issues? Currently I am thinking that Emoji Suggest module should internally create its own tooltip plugin instances and expose the entire thing as flat array to the user of the library.
// The emoji suggest library
export const emojiPlugins = ({ opts }) => {
return [
tooltip.plugins({ ... }),
someEmojiPlugin,
]
}
// User using the library
const plugins = () => [
...corePlugins(),
emojiPlugins({ opts }),
]
With this approach each component ends up creating its own plugins instead of expecting the user of the library to setup tooltip
plugin. However with this approach, I am not sure if I should be worrying about the total number plugins in an application. If having more plugins linearly impacts the latency of application, this approach will not work great. There can be an alternative way where we sort expect the user to setup tooltip plugin and somehow use it to display our tooltip, but I worry the developer experience of this approach would be bad.
I would love to hear what you all think about the above ideas, any feedback, thoughts, improvements?
PS: The above ideas have mostly been inspired after combing through a bunch of already existing awesome topics in this website, thanks you all <3.