Update MatchDecorator when state field changes

I’m trying to figure out how to get a MatchDecorator to re-decorate in response to data changing outsode of the editor instance.

I’m using the Atomic Ranges decoration example.

I have an array of “variables” in a variablesStateField. Each variable item contains extra info so I can colour the decorations based on the category. If a variable isn’t in the list, I style the widget to show it’s missing.

When the list of valid variables changes (outside of the editor instance), I dispatch an effect to update the variables:

view.dispatch({
   effects: variablesStateEffect.of(variables), // new variables array
});

The state field then updates:

export const variablesStateField = StateField.define<readonly Variable[]>({
  create: () => [],
  update: (variables, tr) => {
    const variablesEffect = tr.effects.find((effect) => effect.is(variablesStateEffect));

    if (
      variablesEffect != null &&
      variablesEffect.is(variablesStateEffect) &&
      variablesEffect.value != variables
    ) {
      console.log("variablesStateField.update", variables);
      return variablesEffect.value;
    }

    return variables;
  },
});

But, I can’t figure out how to get the match decorator to re-run the decoration process so it can update the styling of the widgets.

const placeholderMatcher = new MatchDecorator({
  regexp: variableInputRegex,
  decoration: (match, view) => {     // How to get this to run again when the state effect runs?
    const variables = view.state.field(variablesStateField);
    const variableName = match[1];
    const variable = variables.find((variable) => variable.name === variableName);

    if (variableName == null) {
      return null;
    }

    return Decoration.replace({
      widget: new PlaceholderWidget(variableName, variable),
    });
  },
});

Many thanks!

Sounds like you need to fully recreate the decoration set with createDeco when the set of variable changes. I.e. set up the view plugin that maintains the decorations to check the value of that state field, and only use updateDeco when it didn’t change in an update.

I think I got close last night. The data flows through and calls createDeco, but the view doesn’t update.

const placeholders = ViewPlugin.fromClass(
  class {
    placeholders: DecorationSet;
    constructor(view: EditorView) {
      console.log("placeholders.constructor", view);
      this.placeholders = placeholderMatcher.createDeco(view);
    }
    update(update: ViewUpdate) {
      console.log("placeholders.update", update);
      this.placeholders = placeholderMatcher.createDeco(update.view);
    }
  },
  {
    decorations: (instance) => {
       console.log("placeholders.decorations", instance.placeholders);
       return instance.placeholders;
    },
    provide: (plugin) =>
      EditorView.atomicRanges.of((view) => {
        console.log("placeholders.provide", view.plugin(plugin)?.placeholders, plugin);
        return view.plugin(plugin)?.placeholders || Decoration.none;
      }),
  }
);

When I dispatch the effect I see a call flow which looks correct in terms of the right data flow:

variablesStateField.update // new variables
placeholders.update // using new variables list
placeholderMatcher.decoration // creates new PlaceholderWidget
placeholders.decorations

But the editor view doesn’t update with the new widget. Do I need to trigger an update in the view so it re-renders with the new widgets?

If I can get that working, then I can change placeholders.update to check if the variables have changed, rather than calling createDeco on every update.

No. If you dispatch an update, and your view plugin updates its decorations in its update method, the view will use those. Are you sure the variables in the state field are changing as intended?

Figured it out, the matcher was updating correctly, but the widget’s eq method was only comparing by name so wouldn’t detect the change:

class PlaceholderWidget extends WidgetType {
  constructor(readonly name: string, readonly variable?: Variable) {
    super();
    this.variable = variable;
  }
  // WRONG! This won't detect if the variable changes
  eq(other: PlaceholderWidget) {
    return this.name == other.name;
  }
  toDOM() {
    return renderVariableChip(this.name, this.variable);
  }
}

So I changed it to:

eq(other: PlaceholderWidget) {
  return this.name == other.name && this.variable == other.variable;
}

Thanks so much for your help!

I’m amazed at what I’ve been able to customise in a couple of days. The documentation is excellent.