Custom Rendering

Up untill this point you might be wondering what is the best way if you want to change the way a particular node is rendered in the browser. Bangle provides you with few options

  • The simplest option is to look at the style.css of a component and override it with your own.
  • If you only care about the HTML semantics you can modify the toDOM and parseDOM in your spec.schema. Checkout this cute Prosemirror example for details.
  • If you want all the bells and whistles read on ๐Ÿ˜Ž!

In this guide we will create a component that speaks the words written inside of it with the help of the NodeView API.

Below is the finished product:

Spec#

We create a spec named speech having the following fields:

{
type: 'node',
name: 'speech',
schema: {
content: 'inline*',
group: 'block',
draggable: false,
toDOM,
parseDOM,
},
}

The schema defines that this component can have 0 or more content of group inline.

Next we define the toDOM and parseDOM fields of the schema. They control how editor serializes and parses your node. For node views, these fields dictate the exporting of HTML, copy pasting and dragging.

const { toDOM, parseDOM } = domSerializationHelpers(name, {
tag: 'div',
content: 0,
});

๐Ÿ’ก '0' in Prosemirror world is what props.children is in React i.e. dom content can be put in it. See Proseemirror.DOMOutputSpec for more details

Since we donot really care about the semantics of the output, we use the helper function domSerializationHelpers which generates a good enough pair of toDOM and parseDOM.

๐Ÿ“– Read the Prosemirror.Schema guide if you want to dig deeper on what each of the fields in the schema mean.

Plugins#

plugins() {
return [
keymap({
'Ctrl-d': createSpeechNode(),
}),
...
];
}

In the plugins, we define a keyboard shortcut which allows us to create a new speech node whenever user presses Ctrl-d. Here, createSpeechNode() is a command which will insert a new speech node for us.

<DOC>
|| EDITOR LAND ||
<containerDOM>
|| YOUR LAND ||
<contentDOM> || EDITOR LAND || </contentDOM>
</containerDOM>
|| EDITOR LAND ||
</DOC>

Rendering#

Fully aborbing the visualization above, we create a React component:

<div>
<div className="your-land" contentEditable={false}>
<select
onChange={(e) => {
setLang(e.target.value);
}}
>
<option>some option</option>
</select>
<button onClick={onPlay}>Play</button>
</div>
{children}
</div>

We put all our customization under the div .your-land. Bangle will automatically insert contentDOM in the children to allow for typing in our speech component.

As you notice we have marked just a part of the UI as contentEditable=false, this tells the browser that unlike the rest, this content is not meant to be editable.

๐Ÿ’ก When creating NodeView UI make sure that all the dom ancestors of {children} are free of contentEditable=true or contentEditable=false. This helps prevent contentEditable islands, which hamper the free flow of the cursor in your document. Like the example above, tuck all the custom UI in a single div or span.

// ๐Ÿšซ donot do this as it creates `contentEditable` islands
<div contentEditable={true}>
<div className="your-land" contentEditable={false}>
<button onClick={onPlay}>Play</button>
</div>
{children}
</div>
// ๐Ÿšซ donot do this as it creates `contentEditable` islands
<div contentEditable={true}>
<div className="your-land" contentEditable={false}>
<button onClick={onPlay}>Play</button>
<div contentEditable={true}>
{children}
</div>
</div>
</div>
// ๐Ÿ‘ this is good
<div>
<div className="your-land" contentEditable={false}>
<button onClick={onPlay}>Play</button>
</div>
<div>
{children}
</div>
</div>

Wiring it all up#

In your React component we expect a render prop which will be called whenever there is an update for any of your node view and based on the type of your node you can decide what component to render.

<BangleEditor
state={editorState}
renderNodeViews={({ node, updateAttrs, children }) => {
if (node.type.name === 'speech') {
return (
<Speech node={node} updateAttrs={updateAttrs}>
{children}
</Speech>
);
}
}}
/>

In the second plugin, we tell Bangle to use a NodeView plugin for rendering the speech node. With this we are on our own on deciding the rendering of this node.

The contentDOM and containerDOM are names you will hear often in the docs:

  • contentDOM: Is the dom node which will be the parent of the content. As you know, in our spec we defined our node to have any content which is of the group inline. So anything that you type in the speech node, will be directly housed by this dom node.

  • containerDOM: This is the outermost node of speech node.

The bangle editor will directly interface with these two nodes and leave the things in between for you. You can visualize this relationship something like this, where you only control the part demarcated as YOUR LAND.

NodeView.createPlugin({
name: 'speech',
// The speech component will be mounted on this
containerDOM: ['div', { class: 'speech-container' }],
// The dom.Node which Bangle will control to allow for typing of things to speak.
contentDOM: ['span', { class: 'speech-text' }],
});