How I can attach attribute with dynamic value when new paragraph is inserted?

Hi guys,

I would like to attach custom attribute to each paragraph node that prosemirror insert during user interaction. Custom attribute should be with dynamic value something like GUID. Basically I would like to register listener which will "listen" for inserting/deleting of new paragraph and when this happens will ask backend service for GUID value and attach it to paragraph attribute. Can you advise me how I can achieve this?

Thank you in advance!

2 Likes

The recommended way to do this is to add an attribute with a default value of null to your paragraph node type, and register a plugin with an appendTransaction function that scans the document and assigns a new ID to each document with a duplicate or missing ID.

5 Likes

Hi there,

Just faced with the same task and want to share my solution to save time of the next generation of ProseMirror lovers :slight_smile:

Plugin file:

import {Plugin} from "prosemirror-state";
import {uuidv4} from "uuidv4";

const isTargetNodeOfType = (node, type) => (node.type === type);

const isNodeHasAttribute = (node, attrName) => Boolean(node.attrs && node.attrs[attrName]);

const attrName = "guid";

export const createPlugin = (guidGenerator = uuidv4) => {
  return new Plugin({
    appendTransaction: (transactions, prevState, nextState) => {
      const tr = nextState.tr;
      let modified = false;
      if (transactions.some((transaction) => transaction.docChanged)) {
        // Adds a unique id to a node
        nextState.doc.descendants((node, pos) => {
          const {paragraph} = nextState.schema.nodes;
          if (isTargetNodeOfType(node, paragraph) && !isNodeHasAttribute(node, attrName)) {
            const attrs = node.attrs;
            tr.setNodeMarkup(pos, undefined, {...attrs, [attrName]: guidGenerator()});
            modified = true;
          }
        });
      }

      return modified ? tr : null;
    },
  });
};

Schema file:

import {Schema} from "prosemirror-model";
import {marks, nodes} from "prosemirror-schema-basic";

const baseNodes = {
  ...nodes,
  // customize basic paragraph with guid
  paragraph: {
    content: "inline*",
    group: "block",
    attrs: {
      guid: {default: ""},
    },
    parseDOM: [
      {
        tag: "p",
        getAttrs: (dom) => ({guid: dom.getAttribute("data-guid")}),
      },
    ],
    toDOM(node) {
      const {guid} = node.attrs;
      return ["p", {"data-guid": guid}, 0];
    },
  },
};

const base = new Schema({nodes: baseNodes, marks: marks});
...

Thank you @marijn for such a wonderful library!

Good luck!

13 Likes

signed up just to say thank you for this snippet. had almost exactly need to have unique id for nodes. but all this seems a bit overhead just to set attribute, don’t you think? or there is another simpler way already? also, got small issue, as i’m using typescript, there was an error with typing in dom.getAttribute() because dom should be Node | string type. isn’t it?

  /**
   * A function used to compute the attributes for the node or mark
   * created by this rule. Can also be used to describe further
   * conditions the DOM element or style must match. When it returns
   * `false`, the rule won't match. When it returns null or undefined,
   * that is interpreted as an empty/default set of attributes.
   *
   * Called with a DOM Element for `tag` rules, and with a string (the
   * style's value) for `style` rules.
   */
  getAttrs?: ((p: Node | string) => { [key: string]: any } | false | null | undefined) | null;

That’s because its a {tag} parse format and not a {style} parse, I think.