Creating a Step for all node attr changes

I’m trying to generate a step which changes all matching nodes with a chang attr. It’s running through Tiptap, but I don’t see how that changes what I’m observing.

import { Step, StepResult } from 'prosemirror-transform'

class SetAllNodeAttr extends Step {
  constructor(change, nodeName, stepType = 'SetAllNodeAttr') {
    if (!nodeName) throw Error('NoNodeName')
    super()
    this.stepType = stepType
    this.nodeName = nodeName
    this.change = change
  }
  apply(doc) {
    this.prevValue = []
    let fragment = doc.content
    for (const node of fragment.content) {
      if (node.type.name === this.nodeName) {
        this.prevValue.push(
          Object.keys(this.change)
            .reduce((acc, key) => {
              acc[key] = node.attrs[key]
              return acc
            }, {})
          )
        node.attrs = {
          ...node.attrs,
          ...this.change
        }
        let index = fragment.content.indexOf(node)
        fragment.replaceChild(index, node)
      }
    }
    doc.content = fragment
    return StepResult.ok(doc)
  }
  invert() {
    return this.prevValue.map((change) => {
      new SetAllNodeAttr(
        change,
        this.nodeName,
        'revertSetAllNodeAttr'
      )
    })
  }
  map() {
    return this;
  }
  toJSON() {
    return {
      stepType: this.stepType,
      change: this.change,
      nodeName: this.nodeName
    }
  }
  static jsonID(id, stepClass) {
    console.log('jsonID', id, stepClass)
  }
  static fromJSON(schema, json) {
    return new SetAllNodeAttr(
      json.change,
      json.nodeName,
      json.stepType
    )
  }
}

The step appears to apply after changes, but nothing rerenders. When looking at the Doc and some other comments, is there something else that needs to occur to get the transaction to finish?

Also, in the docs, there’s some language like:

static jsonID(id: string, stepClass: constructor) To be able to serialize steps to JSON, each step needs a string ID to attach to its JSON representation. Use this method to register an ID for your step classes. Try to pick something that’s unlikely to clash with steps from other modules.

I don’t know what registering in this context means. Any support would help.

Both steps and nodes are immutable object, and should not be changed. So don’t fill in prevValue in apply (compute it on demand in invert) and definitely don’t assign to node.attrs, since that will both break the old version of the document and, as you noticed, not cause the editor to update because the nodes didn’t change. You’ll have to rebuild the document in your apply method (though it is a good idea to reuse nodes that don’t have the targeted node type anywhere in them).

Just what the docs say—globally associate a string with the step type, so that that can be used to recognize the JSON representation of the step.

OK. So is this the most efficient means then?

  apply(doc) {
    for (const { node, pos } of flatten(doc)) {
      let resolvedPos = doc.resolve(pos)
      if (node.type.name === this.nodeName) {
        let newNode = node.type.create({
          ...node.attrs,
          ...this.change
        }, node.content)
        let index = resolvedPos.index(resolvedPos.depth)
        let fragment = doc.content.replaceChild(index, newNode)
        doc = doc.copy(fragment)
      }
    }
    return StepResult.ok(doc)
  }