Allow Node to Dispatch Transaction to Change Node Attributes


#1

Hello,

I have ProseMirror rendering a node that is a react component. The react component allows the user to select an itemName. When the user selects an itemName I want to set the itemName attribute of the node to match the itemName that was selected by the user. In react, I would pass a function to the child component and invoke it with the necessary parameters. I have not found the proper way to do this in ProseMirror.

I know the function will need state, dispatch, and the itemName string. The following code works as a command bound to a keystroke, but will only set itemName to a hardcoded value, as I cannot figure out how to pass a parameter to a command bound to a key.

 const setItemName = (state, dispatch ) => {
       if (state.selection instanceof NodeSelection) {
         let {$from } = state.selection
       //dispatch a transaction to set the itemName attribute of the targetNode
       dispatch(state.tr.setNodeMarkup($from.pos, null, {...node.attrs, itemName: ‘hardcoded string’ }))
       }
       return true
     }

Also, I cannot figure out how to invoke a command from inside the node.

What is the proper approach? Should it be a command? Do I need to write a plugin to perform this function? If so, how can I allow a node to invoke the function, pass itemName, and dispatch the transaction?

I tried passing state and dispatch as attrs and invoking dispatch from inside the component, but state seems to be a static object and I can’t get the reference to the current node selection that way.

 const acceptor = {
       inline: true,
       attrs: {
         state,
         dispatch,
         itemName: {default: null},
         itemType: {default: null},
       },
       group: "inline",
       draggable: true,
       parseDOM: [{getAttrs: function getAttrs(dom) {
         return {
           itemName: dom.getAttribute("itemName"),
           itemType: dom.getAttribute("itemType"),
         }
       }}],
       toDOM(node) { let dom = document.createElement("span")
       render(<Acceptor {...node.attrs}/>, dom)
       return dom }
     }

#2

It’s probably easiest to use a node view for the node around the component. That gets passed an editor view and a function with which it can determine the node’s position in the document. That tends to be enough to implement actions that change the node—something like editorView.dispatch(editorView.state.tr.setNodeMarkup(getPos(), ...)).


#3

Thanks, that worked!

Here’s the code I came up with:

import React from 'react';
import ReactDOM from 'react-dom';
import {Node} from "prosemirror-model";
import {EditorView, NodeView} from "prosemirror-view";
import AnyReactComponent from '../../AnyReactComponenet';


class ReactComponentView {
  constructor(node, view, getPos) {
    this.node = node
    this.view = view
    this.getPos = getPos
    this.itemType = node.attrs.itemType;
    this.itemName = node.attrs.itemName;
    const span = document.createElement('span');
        ReactDOM.render(<AnyReactComponent {...node.attrs}
          setItemName={(itemName) => this.setItemName(itemName)}/>, span);
        return {
            dom: span,
        }
  }

  setItemName = itemName => {
    this.view.dispatch(
      this.view.state.tr.setNodeMarkup(
        this.getPos(),
        null,
        {...this.node.attrs, itemName }
      )
    );
}

}

export default ReactComponentView;