Defining a container node, that gets removed when empty?

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


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 [
        'data-node-type': 'my_container'


  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(, pos + 1))


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


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


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(, pos + 1));
} else {
  // This is the last item in myContainer, so remove the whole thing.
  view.dispatch(, 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.


Nice, thanks! :+1:

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