Force nodes of specific type to re-render

I have a simple leaf node that represents a variable. Its only attribute is the name of the variable. In toDOM() the actual value of the variable is used to as text content of the node. Roughly like this:

[ 'span', { class: 'variable' }, variableValue ]

The problem is that variableValue is stored and modified apart from the document. Hence changes to a variable’s value obviously won’t be reflected in the editor.

One approach I’m trying out is to force the editor to re-render all “variable nodes” whenever any variable values have changed. So far I haven’t found a way to do that except for updating the entire document/editor, but that seems overkill.

How can I force a re-render/update of all nodes of a specific type?

There are also decorations and node views but I have trouble finding good examples on how to use them. I have no idea whether they’ll actually be useful for my use-case.

You’ll want to use a node view here, since those are the only way to get direct control over a node’s rendering and DOM lifecycle, but those alone still don’t help much here, since they are only given a chance to update themselves when the node they represent changes in the document, or if any of the node decorations on that node changes. So you could write an extension that maintains decorations on all nodes of this type, and when the external data changes, makes sure to update them all, which will cause the node views’ update method to be called, allowing them to read the changed data and update their DOM content.

2 Likes

Wow, more difficult than I’ve expected. Thank you for the insight. I’ll give it a try and report back.

I’ve got it working with NodeView and Decoration.

Updating my variable values through a Transaction updates related Decorations which in turn updates related NodeViews. All encapsulated nicely in a Plugin and the variable values are now part of the state of my Plugin. The respective NodeSpec is also defined by the Plugin and accessed using plugin.nodeSpecs.

But I still have to access a variable’s value in NodeSpec.toDOM() for copy & paste to work properly. Unfortunately I’ve only got access to the Node and not the EditorState, so I cannot access the variable values from my Plugin’s state.

There are multiple potential paths forward now:

  1. I find a way to access a Plugin’s state from within NodeSpec.toDOM().
    I don’t think there is a proper one.
  2. I maintain a reference the EditorView from within my NodeSpec in order to access the current EditorState.
    That doesn’t sound clean.
  3. I allow my Plugin to provide a DOMSerializer that can serialize my NodeSpec. The problem here is the same as with toDOM() though: I don’t have access to the EditorState.
    So also not clean.
  4. I put all state of a variable (label, value, etc.) directly into all Nodes that reference it. I replace these Nodes every that state changes.
    That doesn’t sound right either. The node is only a simple variable reference with a name. Adding the value and other information to it shouldn’t be necessary.

It’s basically similar to the issue that I already have with state-sensitive text serialization, just with HTML this time:

Desired HTML output:

<span data-variable="varname">varvalue</span>

(varname is a Node attribute, varvalue is Plugin state)

Desired text output:

varvalue

Am I missing something here or is there no good solution to that problem?

No, this kind of thing is definitely tricky, and usually solved by going through some global state (which, depending on how your variables work, might not be practical). Creating a DOMSerializer that closes over a reference to (or getter function that can retrieve) the view might be the least problematic approach.

@marijn is there any new solution to force node to be rerendered? I have an annotation node whose label (i.e. [1], [2] etc.) is rendered based on the number of these nodes in the document and its order. So if I remove first [1] annotation, the [2] should change to [1]. Currently I’m using plugin with appendTransaction which first search for all the annotations within the doc and then update its label attribute to force them to rerender. Is there any better solution?

I recommend to use node decorations that assign such nodes their number, maintained by some plugin. When their decorations change, nodes will be re-rendered.

Or just use CSS counters to assign the numbers and let the browser worry about them.

@marijn what kind of decoration do you suggest to always rerender node view? Currently I’m generating a random number for each node and add such a class but I don’t think it’s elegant. Ideally I’d like to avoid introducing visible changes to the node. Here is my current code:

   return [
      new Plugin({
        props: {
          decorations(state) {
            let nodes = getAllNodesOfType(editor, 'figcaption');
            return DecorationSet.create(editor.state.doc, [
              ...nodes.map((x) =>
                Decoration.node(x.pos, x.pos + x.node.nodeSize, {
                  class: Math.random().toString(),
                })
              ),
            ]);
          },
        },
      }),
    ];

The decoration doesn’t have to add anything to the actual DOM. Any different property in the spec will make it count as a different decoration. If you don’t have any meaningful value that you can derive from whatever is triggering these re-renders, random numbers work fine.

OK, I moved it to spec prop as id key. However, is it possible to get decorations for a given node or position? Then I could replace {"id": Math.random()} by {toggle: !prevToggle} where prevToggle is previous decoration. Not sure which one is a better (faster) solution

If you have access to the decoration set (which you should, since you are providing it from your own plugin), you can use its find method to find decorations in it at a given position.

You mean to store decoration set in a plugin state and access it from there and update each time?

EDIT OK I toggle editor.storage and it works fine, thanks