Access current Plugin state when creating NodeView

I am writing a Plugin that includes several NodeViews in its props. The plugin also maintains some global state that all of the NodeViews created by this plugin need to know about. Accordingly, each of the callbacks I provide to props.nodeViews must get the current plugin state before returning a NodeView.

(below is a simplified example – see also the real code on GitHub)

interface IPluginState { specialNumber:number };

let pluginKey = new PluginKey<IPluginState>("my-plugin");

let pluginSpec:PluginSpec<IPluginState> = {
	key: pluginKey,
	state: {
		init(config, instance){
			return { specialNumber: 5 };
		},
		apply(tr, value, oldState, newState){
			let pluginState = mathPluginKey.getState(oldState);
			if(pluginState) { /* do something state-dependent */ }
			return value;
		},
	},
	props: {
		nodeViews: {
			"my_nodeview" : function(node: ProseNode, view: EditorView, getPos:boolean|(()=>number)) {
				/** @todo is this necessary?
				* Docs says that for any function proprs, the current plugin instance
				* will be bound to `this`.  However, the typings don't reflect this.
				*/
				let pluginState = pluginKey.getState(view.state);
				if(!pluginState){ throw new Error("no plugin found!"); }

				// THIS is unbound
				console.log(this) // <-- prints `undefined`

				// set up NodeView
				return new MyNodeView(
					node, view, getPos as (() => number), 
					pluginState.specialNumber
				);
			}
		}
	}
};

Question

This solution works, but the dependence on the external pluginKey seems not ideal, and (without getting into too much detail) is getting in the way as I try to refactor my plugin to be a bit more flexible if users need extra customization. My question is:

When ProseMirror creates a NodeView using a function specified in the props.nodeViews map of a Plugin, is there a more elegant way to get the current state of that plugin?

For instance, the documentation for PluginSpec.props says that

“Props that are functions will be bound to have the plugin instance as their this binding.”

However, this seems not to apply to the functions passed to props.nodeViews, which appear to be unbound (this === undefined) when called. (I dug through the ProseMirror source a bit to find out where these are actually called, but wasn’t successful in finding the actual call location since the relevant code is split across multiple repositories. Help locating the source would be appreciated!)

Possible Solutions

  • Continue using the pluginKey to get the current plugin state, although this is not ideal because of the dependence on a global variable.
  • When calling one of the functions specified in props.nodeViews, bind this to the current plugin instance
  • (Suggestions Welcome!)

Thanks!

I am new with prosemirror, so take my suggestion with a grain of salt. The statement above is applicable to props that are functions and the prop nodeViews is an object and not a function. The important part here is that any method inside the prop nodeViews is not a prop and hence not binded to the plugin instance.

Continue using the pluginKey to get the current plugin state, although this is not ideal because of the dependence on a global variable.

As far as I have seen, this is the way of doing things in prose mirror and is also followed by other derivatives of PM.

This solution works, but the dependence on the external pluginKey seems not ideal, and (without getting into too much detail) is getting in the way as I try to refactor my plugin to be a bit more flexible if users need extra customization.

It might look a bit weird first time working with plugin states, but it is quite the opposite of using a global variable. You are not using a global variable directly, if you were, you wouldn’t have the need to pass in the (view.state) to derive the correct plugin state. This mechanism helps to decouple the plugin state from its implementation and is quite powerful when used correctly.

Let me know if I am misunderstanding this or if you have any feedback on my understanding of plugin state.

Thanks for the reply!

Oh, yeah, I do understand that nodeViews isn’t a top-level function, so its members aren’t bound. I’m mostly wondering:

If top-level Plugin functions like transformPastedHTML are bound to the plugin–then why doesn’t it also make sense to bind the functions passed in props.nodeViews? In the first case, ProseMirror assumes that functions like transformPastedHTML etc will change behavior based on the plugin state. Why is it the default assumption that the props.nodeViews collection of functions should not depend on the plugin state in the same way?

I get that the plugin itself isn’t being referenced as a global variable, but I still need to keep a global reference pluginKey around to access the state, yes? It’s certainly not the end of the world, but it is a minor inconvenience in my case.

For your first part I will defer to someone with more knowledge on topic.

I do not agree that it is inconvenient. There is no need to keep it as a global variable, if you want you can keep the pluginKey in a closure like done in tiptap. Also, regarding exposing an API to access state for your library consumer, I would recommend doing something like prosemirror-collabs's getVersion method, which allows to get the version saved in the state. Also, good luck with your library, it looks great !