Defining a container node, that gets removed when empty?

Consider the following, example container node that accepts “child” nodes as its content:

Schema

export const myContainer: NodeSpec = {
  inline: false,
  group: 'block',
  content: 'child+',
  attrs: {},
  parseDOM: [{
    tag: 'p[data-node-type="my-container"]',
    getAttrs: (dom: Element) => ({})
  }],

  toDOM(node: any): [string, any, number] {
    return [
      'div',
      {
        'data-node-type': 'my_container'
      },
      0
    ];
  }
};

Use-case

  1. I have a myContainer node with exactly 1 child node.
  2. The pos of the child node is pos, size is 1
  3. I remove the node by calling view.dispatch(view.state.tr.delete(pos, pos + 1))

Expected

The myContainer node is removed, because I have deleted the last remaining child node from it (and it requires content to be child+)

Actual

The myContainer node is retained and a new “dummy” node of child type is inserted inside (with default attrs).

Rationale

I see two ways to configure content:

// Zero or more
content: 'child*'

// One or more
content: 'child+'

In the first case, the container node is allowed to remain empty.

In the second case, I want to be able to tell Prosemirror, that a node without content is an invalid one, hence it must be removed from the document.

1 Like

Here’s a temporary workaround I ended up using:

// pos => position of the child to be removed
const resolvedPos = view.state.doc.resolve(pos);
if (resolvedPos.parent.childCount > 1) {
  view.dispatch(view.state.tr.delete(pos, pos + 1));
} else {
  // This is the last item in myContainer, so remove the whole thing.
  view.dispatch(view.state.tr.delete(resolvedPos.before(), resolvedPos.after()));
}

That’s simply not how schema restrictions are enforced at that level, but if you use deleteRange (which uses a looser do-what-I-mean approach) instead of delete (which won’t touch anything outside of its from/to positions), I think it will do the thing you were expecting.

2 Likes

Nice, thanks! :+1:

Just tested. It seems that deleteRange() uses similar logic to my workaround, which means I can start using it right now.