Setting a timestamp attribute on creation, maybe defaultAttrs issue?

I’m having issues setting a timestamp attribute to nodes. Here’s a short snippet in the plugin update method:

// set timestamp if there was a change and no previous timestamp
if (prevState && prevState.doc.eq(state.doc) && !node.attrs.ts) {
  node.attrs.ts = new Date().toISOString();
}

The issue is that the ts attribute will be the same for all nodes with type of paragraph, even when creating a new paragraph node. I am not getting this issue with headers of different levels. If I inspect these paragraph nodes, I notice that node.type.defaultAttrs.ts are all set to the same timestamp. I couldn’t find documentation for this property.

Specifying attributes in the schema doesn’t solve this issue:

// add ts attribute to schema
schema.nodes.paragraph.attrs = {
  ts: {
    default: null,
  }
}

will throw an error: Uncaught RangeError: No value supplied for attribute ts

This error goes away if I manually set defaultAttrs:

schema.nodes.paragraph.defaultAttrs = {
  ts: null,
}

but again, the ts attribute for all paragraph nodes will be set to the same initial timestamp. Any suggestions?

Nodes, and all the objects they contain, including attributes, are supposed to be immutable values, and should thus not be updated.

If you want to change an attribute you’ll have to create a transaction.

Ahhh right, I feel silly. I’ve refactored to:

view.dispatch(state.tr.setNodeMarkup(pos, null, attrs))

and it seems to be working! However, I’m still unclear with the correct way of specifying attributes in the schema. I could have sworn I saw an example that had a pattern that looked like:

attrs = {
  foo: {
     default: 'bar',
     hasDefault: true
   }
}

But I can’t find it again. For now I’ve gotten it to work by manually setting schema.nodes.paragraph.defaultAttrs, is this correct?

That looks like the way it was in some pre-1.0 version. Now it’s enough to say attrs: {foo: {default: "bar"}} (see the docs). Don’t monkey-patch .defaultAttrs—that might break in future versions.

Hmmm in that case, maybe I’m still doing something wrong. If I do not include the hasDefault: true attribute, OR if I do not manually add .defaultAttrs the app will crash with this error:

This is the function that is throwing the error:

function computeAttrs(attrs, value) {
  var built = Object.create(null);

  for (var name in attrs) {
    var given = value && value[name];

    if (given === undefined) {
      var attr = attrs[name];

      if (attr.hasDefault) {
        given = attr.default;
      } else {
        throw new RangeError("No value supplied for attribute " + name);
      }
    }

    built[name] = given;
  }

  return built;
}

hasDefault is a property on the internal Attribute type, which you shouldn’t even be touching from user code, so yes, it does look like you’re doing something wrong. How are you creating the Schema object?

import { schema } from 'prosemirror-markdown';

// TODO: create our own schema
schema.nodes.paragraph.attrs = {
  ...schema.nodes.paragraph.attrs,
  ts: {
    default: null,
    // temporary hack
    hasDefault: true
  },
};

schema.nodes.heading.attrs = {
  ...schema.nodes.heading.attrs,
  ts: {
    default: null,
    hasDefault: true
  }
};

So… I realized that was the hacky way and I have refactored to:

const extendedSchema = new Schema({
  nodes: schema.spec.nodes
    .update('paragraph', {
      ...schema.spec.nodes.get('paragraph'),
      attrs: {
        ts: {
          default: null,
        }
      },
    }).update('heading', {
      ...schema.spec.nodes.get('heading'),
      attrs: {
        ...schema.spec.nodes.get('heading').attrs,
        ts: {
          default: null,
        }
      }
    }),
  marks: schema.spec.marks,
});

which broke a bunch of stuff but I realized the issues were from using defaultMarkdownParser. I’m currently building my own markdown parser and it seems like I’m on the right path. Thanks!!