Plugin Props

Is it possible to pass a prop into a plugin? For example, passing in a specific character to render for a widget decoration plugin.

It depends when you want to pass in the prop. If you know the character or prop ahead of time, you could just initialize the plugin with it. Otherwise, you would have to use tr.setMeta to communicate with the plugin and have it re-render the widget in response.

I know the prop ahead of time, so would it just involve init(prop)?

To make a widget decoration, you need to define a toDOM method. And then to create a widget decoration, you need to create a plugin with the decorations pluginSpec.props set. As reference, look at GitHub - PierBover/prosemirror-cookbook: A series of examples for understanding ProseMirror.

These seem like options for passing props into the editor, but is there a way to have a prop passed into a widget decoration plugin that is already written? If I wanted to pass in whatever the actual widget is such as whatever character I would like to render?

I’m a bit confused by what you mean. Could you post source code to describe the problem?

For example, this is a plugin I have for rendering a character to represent a nonbreaking space in the UI:

const nonbreakingSpace = new Plugin({
  state: {
    init() {
      return DecorationSet.empty;
    },
    apply(tr, set) {
      set = set.map(tr.mapping, tr.doc);
      const widget = document.createTextNode('⎵');
      const action = tr.getMeta(this);
      if (action && action.add) {
        const deco = Decoration.widget(action.add.pos - 1, widget, { id: action.add.id });
        DecorationSet.create(tr.doc, [Decoration.widget(tr.selection.from - 1, widget)]);
        set = set.add(tr.doc, [deco]);
      } else if (action && action.remove) {
        set = set.remove(set.find(null, null, (spec) => spec.id == action.remove.id));
      }
      return set;
    },
  },
  props: {
    decorations(state) {
      return this.getState(state);
    },
  },
});

I would like to pass a prop into the plugin instead of explicitly stating

document.createTextNode('⎵');

because I have a couple other plugins that have the same exact code as above, only with a different text node as the widget decoration depending on which special character it is. Essentially, right now there is a lot of duplicated code because I have the same code for each special character plugin, and the only difference is the text node being created. Where I actually run the plugin,

tr.setMeta(nonbreakingSpace, { add: { pos: tr.selection.from } });

I would like to just be one plugin where I can pass in the desired text node.

Is there a reason why you’re using multiple plugins instead of one plugin if there is duplicated logic?

The easy answer here would be to use the setMeta property to specify which character you want:

const charReplace = new PluginKey("charReplace")
const charReplacePlugin = new Plugin({
  key: charReplace,
  state: {
    init() {
      return DecorationSet.empty;
    },
    apply(tr, set) {
      set = set.map(tr.mapping, tr.doc);
      const action = tr.getMeta(charReplace);
      if (action && action.add) {
        const deco = Decoration.widget(action.add.pos - 1, document.createTextNode(action.charToAdd), { id: action.add.id });
        DecorationSet.create(tr.doc, [Decoration.widget(tr.selection.from - 1, widget)]);
        set = set.add(tr.doc, [deco]);
      } else if (action && action.remove) {
        set = set.remove(set.find(null, null, (spec) => spec.id == action.remove.id));
      }
      return set;
    },
  }
})

tr.setMeta(charReplace, { add: { pos: tr.selection.from }, charToAdd: '⎵'});

The harder answer (that involves more rewriting) is to not use setMeta at all. It looks like you’re using code from the upload example, but most of that logic is not required for character replacement: setMeta is used there because uploading is asynchronous, but the decorations you compute are / can be synchronous.

If I had to tackle the problem (which I presume is Rendering Different Characters in Place of Actual Transformation), I would do more of the following:

const charReplace = new PluginKey("charReplace")
const charReplacePlugin = new Plugin({
  key: charReplace,
  state: {
    init(config, state: EditorState) {
      // Iterate over `state.doc` using `doc.nodesBetween` to compute widget decorations based on the entire initial document.
      let decos = []
      state.doc.nodesBetween(0, state.doc.nodeSize, (node, pos) => {
        // character replacement logic here e.g. something like
        // const deco = Decoration.widget(pos - 1, ...)
        // decos.push(deco)
      })
      return decos.length ? DecorationSet.create(state.doc, decos) : DecorationSet.empty;
    },
    apply(tr, set) {
      set = set.map(tr.mapping, tr.doc);
      // Iterate only over parts of the document that `tr` has modified
      // For reference, look at https://discuss.prosemirror.net/t/find-new-node-instances-and-track-them/96/7
      changedRanges = getChangedRanges(tr)
      for (range in changedRanges) {
        // Remove previous decorations if they exist in the range using `find` and `remove`
        // character replacement logic here
        tr.doc.nodesBetween(range.from, range.to, ...)
      }
      return set;
    },
  }
})