Pass custom id to nodes?

I have a hierarchical document tree that I want to concatenate into a single editor. In order to track changes to the original document, is there a way I can pass custom Id values into the editor state? so, for instance:

const docTree = [
  {id: 0, body: "chapter one", children:[
    {id: 1, body: "section one of chapter one"},
    {id: 2, body: "section two of chapter one"}]},
  {id: 3, body: "chapter 2..."}
]

Right now, I’m just reducing the body strings themselves and doing…

options.doc = newDoc;
const newState = EditorState.create(options);
this.view.updateState(newState);

So that works fine. But when I make changes, I need to somehow know what original doc id I was typing in. So what would I have to pass into EditorState.create in order to retain those ids? Would I extend the nodes schema to do this?

Thank you!!

If you’re creating the document, you can set its attributes. So I’m not sure what the question is here.

My question is how? Is there an example of custom attributes? Do I need to create a custom node schema?

In our implementation, we add the attribute id to the element definition itself when we create the schema. By storing the id in an attribute it gets retained even when copying, serializing or exporting to HTML.

const paragraph = {
  content: 'inline*',
  marks: '_',
  group: 'block',
  attrs: { id: idAttr },
  toDOM(node) { return ['p', { id: node.attrs.id }` }, 0]; },
  parseDOM: [{ tag: 'p', getAttrs: readID }]
};

const idAttr = { default: null }; // ids are generated by the id plugin
const readID = (dom) => ({ id: dom.getAttribute('id') });

We then use a plugin to make sure all IDs are unique (which is why we do not initially create them). The gist is:

In appendTransaction use newState.doc.descendants to loop through all nodes. If an attribute id exists, check whether it is unique in the document (e.g. by keeping a hashset of existing ids) and generate a new one if needed. You may want to register a collision somewhere so you can make sure it can be tracked back to the original document.

Does this answer your question?

Yes, this is great! I think the only thing I’m missing is the right way to set the attributes when setting the editor state? I would also need a way to, for instance, bind a key command to create a new element with a brand new ID…

When you’re setting the editor state you provide a document, if that already has the attribute id in the nodes, then ProseMirror will have that as well.

So if I do something like

const dom = document.createElement('div');
dom.innerHTML = `<p id="one">my text</p>`;
const doc = DOMParser.fromSchema(mySchema).parse(dom);
const state = EditorState.create({ doc, .. });

The id will be in there (since the schema knows how to parse the DOM. Of course you could also create the document from serialized JSON.

The JSON that ProseMirror uses internally will look sth. like this

{
        "type": "paragraph",
        "attrs": {
          "id": "one"
        },
        "content": [
          {
            "type": "text",
            "text": "my text"
          }
        ]
      }

If you add a new element (e.g. by pressing Enter in the editor) your ID plugin (as mentioned above) will detect a new element without an ID and will add one.

Awesome—I’m inching closer but for some reason 1) the id isn’t being parsed even though I use that identical parseDOM function, and also now the editor UI won’t actually reflect any typing. Is there a gist of a complete example somewhere? There are just so many elements to this API I know I’m still missing something.

Here’s a codepen that I hope will illustrate my problem: https://codepen.io/t3db0t/pen/xxKQMpm

In the (browser) console output, you can see where parseDOM correctly extracts the supplied custom node and ID attribute. However, toDOM then shows that the ID is missing (undefined), and any subsequent look at the ProseMirror document shows that the IDs are missing.

It seems like there’s some step or element I’m not doing to make this work…?

Hi,

getAttrs expects the return value to be an object, not a string.

So changing

return id;

into

return { id };

solves the issue with the attribute not being displayed on docnode.