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');
    }

}

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

1 Like

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.