Delete Node from CustomView with line break inside


I have nodes A and B with the following schema:

foo - {
   content: 'bar'
bar - {

‘foo’ is rendered as a custom view with

this.dom = this.contentDOM = document.createElement('foo'); ....

inside its constructor.

Upon initial creation, there is no <br> inside the node. But when I try to delete it via ‘tr.delete(startPos, endPos)’, the node itself does not get deleted, only the content is replaced with a single <br>. What I need is to actually delete the node so it does not take space inside my document. The positions of the nodes are as follows

278|    foo    |287
 279|   bar   |286
 279|   text |285

No matter what positions I pass to ‘tr.delete()’ (tried some cases with -2, -1, +1, +2), it either places the content that follows ‘foo’ inside it or leaves <foo><bar><br></bar></foo> Any idea what could I do to get the node deleted? Please note that I do not have ‘inline: true’ on any of the two nodes

Does your schema allow the foo node to be deleted, or is something required in the place that it takes up in its parent node? I can’t quite follow your diagrams, but deleting from the position before a node to the one after it (maybe use doc.resolve(pos).toString() to debug your positions) should delete it, if it’s allowed to be deleted. Transaction methods act on the document level, so node views and DOM-related issues should not be relevant.


The diagram was regarding the positions of the nodes displayed in the dev tools image

My page schema is with content ‘(ElemA | foo | ElemB)*’ so that wasn’t the issue. Once I changed the content of ‘foo’ to be ‘bar*’ instead of just ‘bar’, I was able to delete the node. Does that mean that I need to make every single node in a hierarchy optional, in order to be able to delete a parent node?

No, definitely not (there’s several instances of non-optional children in the basic schema, and this problem doesn’t come up).

It was too early to open the champagne. My change only removed the inner “bar” node, but the “foo” wrapper was still in the DOM and PM State. Looks like the issue has something to do with my custom view, because after removing it from nodeViews: config of the EditorView, I am able to delete the nodes using the same code as before tr.delete(pos, node.nodeSize)

My custom views:

class BaseView {
    constructor(node, view, getPos) {
        // getting attributes to set to the dom element
        // let attrs = ...

        this.dom = this.contentDOM = document.createElement(this.nodeType);
        for (const attr in attrs) {
            this.dom.setAttribute(attr, attrs[attr]);
//.. some logic related to opening popup upon click

    destroy() {
        // detaching event handlers, no need?

    stopEvent() { return true; }

class FooView extends BaseView {
    constructor(node, view, getPos) {
        super(node, view, getPos);
        this.template = view.state.doc.attrs.template;

    update(node, decos) {
        this.dom.firstChild.innerHTML = `${this.template.fooTemplate} ${node.attrs.numbering}`;
        return true;

    get nodeType() {
        return 'foo';

Going deeper ;(

update(node, decos) {
        this.dom.firstChild.innerHTML = `${this.template.fooTemplate} ${node.attrs.numbering}`;
        return true;

these lines were causing the issue. After removing them, the nodes are being deleted. Any ideas why? Using textContent instead of innerHTML does not work as well.

The update method should at least check whether the given node is of the expected type, and return false otherwise. In this case, it’s claiming to have updated to whatever happened to come after it in the document.

(You’re not the first to trip over this and the use case of node views being able to update to other node types is rare, but that’s currently something we support.)

1 Like

Checking for the node type inside update method of the custom view, before setting textContent, DID WORK! Thank you!

Now I noticed that a node attribute is no longer updated correctly. I update the node attribute via

    $.extend(true, {}, node.attrs, { duration }),

inside a popup that I open upon view click.

My view update method now looks like:

update(node, decos) {
        if ( === {
            // foo>bar>text*
            this.dom.firstChild.textContent = `I like trains`;
            return true;
        } else {
            return false;

Upon opening the popup I get the attribute value from node.attrs.duration. Because of the update method, that attribute is always “null” (default value). But it starts working again when update method of the view is removed/commented. Any idea what else am I doing wrong (or not doing :?) inside that update method?

Edit: For now I have made a workaround, updating the text of the node with tr.insertText because of close deadlines, but I will eventually come back to the issue later and post any new findings.