How to modify Node Attribute without replacing it and causing it to re-render

So I know how to modify a nodes attribute with setNodeMarkup . But that will create a brand new node and causes the dom to re-render it’s content. This is fine if all you have is a paragraph.

But in my case I have put content in there which ProseMirror is not aware of and I would like to avoid destroying that content when I change the attribute.

Is there a way to do this ?

Why do I need this:

I have carousel node which inside of lots of dom modification happens and I don’t want to reinitialise the carousel every time I change its html attribute. It causes really annoying jumps in the editor. Everything else works great.

But maybe I am approaching this problem the wrong way ?

Schema:

    carousel: {
        group: "block",
        defining: true, // node is considered an important parent node during replace operations
        selectable: true,
        atom: true, // though this isn't a leaf node, it doesn't have directly editable content and should be treated as a single unit in the view.
        draggable: false,
        attrs: {
            class: {
                default: null
            },
            html: {
                default: ''
            }
        },
        parseDOM: [{
            tag: 'div.tg_subwidget_carousel',
            getAttrs: dom => {
                return {
                    'class': dom.getAttribute("class"),
                    html: dom.innerHTML
                };
            }
        }],
        toDOM(node) {
            let newDiv = document.createElement("div");
            newDiv.innerHTML = node.attrs.html;
            if(node.attrs){
                newDiv.setAttribute('class', node.attrs.class);
            }

            return newDiv;
        }
    },

Node View:


export default class CarouselView {

    constructor(node, view, getPos) {
        this.node = node
        this.view = view
        this.getPos = getPos

        this.dom = document.createElement('div');
        this.dom.innerHTML = node.attrs.html;

        if(node.attrs.class){
            this.dom.setAttribute("class", node.attrs.class);
        }

        // this where we need to reapply the carousel but not always
        if(view.$d_listeners && typeof view.$d_listeners.afterCarouselConstruct === 'function'){
            view.$d_listeners.afterCarouselConstruct(this.dom, node, view, getPos);
        }
    }

    update(node, decorations) {
        console.log('UPDATE --- CarouselHtmlView');
    }

    ignoreMutation() {
        return true;
    }

    destroy() {
        this.dom.remove();
        console.log('destroy --- CarouselHtmlView');
    }

}

2 Likes

The node view update method is what you should be looking at.

2 Likes

Thank you for that. Adding

update(node, decorations) {
        return true;
    }

to the node view indeed does the job and the dom does not get updated.

But now I have an other issue.

If the update was triggered by a history step I would like node view update to return false. So that the history back and forth steps are handled by ProseMirror. Anyway to know in the node view update what transaction caused the update to trigger ?

Or any other suggestion ?

Definitely don’t just return true from your update method. You’ll at the very least want to check whether the node you’re given is still of the right type, and often you’ll also need to check its attributes or content to make sure your DOM content accurately reflects the node—and then either update the DOM or return false if it doesn’t.

I think that should also cover your history issue—you can’t know what transaction caused the change, in the update method, since it’s not handling a transaction, it’s just getting a new node to display. If you make sure it does that properly, ProseMirror’s history transactions should also just work.

1 Like

" DOM content accurately reflects the node "

This was the key to this. Thank you! I ended up writing a function which gets the original html out of the carousel like if the carousel code has never been applied and compared that to the node.attrs.html in the update of the node view. If the values don’t match the view is out of sync that is when I have to return false and reinitialise the carousel on the dom.

It seem to work just fine.

This however I do not fully understand but don’t bother explaining it if you are busy … it is not crucial to me just now.

I have seen in the docs as well … https://prosemirror.net/docs/ref/#view.NodeView.update “It will be given a node (possibly of a different type)” or as per above “You’ll at the very least want to check whether the node you’re given is still of the right type”

When would this happen ?

I am kind of expecting it should be the same type all the time (in this case anyway). After all there is no editable content in the node.

Anyway thank you for putting me in the right direction! : )

You can write node views that are able to display several types of node, and switch to a new type without redrawing.

I see.

In my case this is not needed or wanted so I don’t have to check if type changed it will be same each and every time and only its attributes change.

Thank you.

Yes, you do have to check to make sure you actually got the same type.

I don’t get it. : ) I am sorry.

It makes no sense to me for this node to be any other type then ‘carousel’. This node view is for the node type ‘carousel’.

But perhaps there are some transactions which would attempt to change the node type to something else that I don’t know of ?

So update should just return true if the wrong type is given ? I guess so because it makes no sense to change the node type to anything else.

No, it should return false to indicate that it can not update itself to reflect the node.

Ok. I am just going to put the code in and not try to understand. Thank you for your help. Maybe later I try and wrap my head around this.

If anyone happens across this in the future (like I am now), here’s an example case I had to deal with for a node view.

My node view was meant to be for image nodes. So I added the { image: myImageNodeViewFactory } line to my plugin. I thought that meant I’d only have to deal with image nodes. But if I selected an image in the editor and typed a letter, the image would (rightly) be replaced with a paragraph node. My image node view’s update method would be called with a paragraph node. I have to return false in that case to let Prosemirror know I can’t handle (and don’t want to handle) paragraphs. Once I’ve done that, Prosemirror uses a different renderer for that paragraph node (presumably the built-in renderer).

So I guess the tl;dr is - your update method could be called with any node, not just the type of node you’re trying to handle. If you don’t want to handle it, return false.

This patch adds a special case to the DOM update algorithm to recognize and optimize wrapper-node-only changes.

1 Like