Update parent node's attrs when child node's attrs change

In my schema I have a node of type line, and a node of type chunk which is a group of lines. Inside the chunk’s toDOM I have some code that uses all the child lines’ attrs, but what I’d really love to do is to be able to calculate and set some attrs on the chunk itself (in particular when a joinUp operation joins two chunks, say). Is there a way to do this? For example, in a plugin, can I detect a joinUp operation (or anything that may be changing the set of lines in chunks) and do some work to set attrs on each affected chunk? I’m hoping this will cause a re-render of the parent chunk node.

This may be similar to:

but I’m trying to see if there’s a lightweight solution possible.

For what it’s worth, my schema looks something like:

const schema = new Schema({
    nodes: {
        text: {
            group: "inline"
        },
        line: {
            content: 'inline*',
            attrs: {
                pageNum: {},
                y1: {},
                y2: {},
            },
            isolating: true,
            toDOM: () => ["div", 0],
        },
        chunk: {
            content: 'line*',
            attrs: {
                label: { default: null },
            },
            toDOM(node) {
                // (something that uses children's attrs to render a div
            },
        },

This seems to work, though I’m not sure whether it has any problems:

  • Define an attr on the parent node (chunk) (here I’m just using numChildren which will hold childCount, which is something that changes on join operations I care about)
  • In a plugin, recompute and update this attr for every parent node affected.

I understand from Identifying the changed nodes for a given transaction - #2 by marijn that the latter may require more care to get the positions right, but for typical cases / light editing, this seems to work:

function updateChunkAttrPlugin() {
    return new Plugin({
        appendTransaction(transactions, oldState, newState) {
            let transactionToAppend: Transaction | null = null;
            transactions.forEach(tx => {
                tx.steps.forEach((step) => {
                    step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
                        newState.doc.nodesBetween(newStart, newEnd, (node, pos) => {
                            if (node.type.name === "chunk") {
                                const newAttrValue = node.childCount;
                                if (node.attrs.numChildren !== newAttrValue) {
                                    if (!transactionToAppend) transactionToAppend = newState.tr;
                                    transactionToAppend.setNodeAttribute(pos, 'numChildren', newAttrValue);
                                }
                            }
                        });
                    });
                });
            });
            return transactionToAppend;
        }
    });
}