Changing doc.attrs?



the Transform API somewhat limits the ability to change state.doc attributes. Is this limited by design ? I’m using such attributes because i identify the editor container with the editor doc.


Yes, an editor or transaction modifies the content of a given document node, not the document mode itself. That’s just how it’s defined. Putting in a different document node requires creating a new state.


So if i get this right, i just need to extend a Step that changes doc.attrs, and add it to a transaction.


That might actually work, I think.


What we did is add one extra node below the top document node (“Article”) and then add everything else inside of that. That way we can use this all for global document settings such as language and document/citation style and don’t need to maintain a different system for settings ourselves.


Well, that’s a good and simple solution, for sure, but i wanted to experiment with editor’s doc being the document body, and i did not see how to make it work as you describe.


Is there a practical advantage to doing this (instead of just adding one level above the top level node), or is mainly due to data-puritist reasons?


hmmm it’s because i’m writing a CMS where one edit the page body. It’s kind of “purist” reason, but also because i don’t like to introduce non-semantic stuff when aiming for semantic stuff.


Ah ok. Purism is of course fine. I just wanted to make sure I wasn’t missing some practical reason for doing this.


Well it’s also about consistence: everything that’s part of the prosemirror model should be treated equally - the doc node included. And it is ! just not when it comes to positions.


This is another thing that should change between version 1.X and 2.X. The work-around is still working, so we’d be OK with it staying this way for 1.X.

@kapouer Did you have luck adding a new step for this?


It’s still on my TODO list, though i suppose it’s going to be quite easy to do.


I’ve adapted the approach that johanneswilm describes (adding a top-level node under the doc node), but have discovered some issues with it:

  • Select-all (e.g. mod-a) and delete removes the top level node, clearing any data stored in it.
  • Delete or backspace at the correct positions in an empty document can delete the top-level node.

I’ve tried making the node unselectable in the schema, but that didn’t help.

The data I’m storing in this top-level node would be better suited to a plugin’s state, but in my case it’s conceptually part of the document, and the document is what I’m storing. i.e. I’m serializing the document node - not the entire state - for storage, and using the document change as a trigger for storing being necessary (only the document is exposed to external code). Having plugin state be at the same level as the doc and selection makes this more complex.

How to control gapcursor

To add an update: I’ve dropped the extra node approach and implemented custom Steps that modify doc.attrs - as suggested - and it is working very well.

How to update `attrs` of `doc`?

Great - is it code you would want to share or are each one of us three going to do this by ourselves? :slight_smile:


Hi johanneswilm - I’m happy to share, but I haven’t implemented this in a generic way, so it’s not particularly useful verbatim. It’s pretty simple though:

class AddTodo extends Step {
  constructor(todo) {

    this.todo = todo;

  apply(doc) {

    return StepResult.ok(doc);

  invert() {
    return new RemoveTodo(this.todo);

  map(mapping) {
    return this;

  toJSON() {
    return {
      stepType: "addTodo",
      todo: this.todo

  static fromJSON(json) {
    return new AddTodo(json.todo);
Step.jsonID("addTodo", AddTodo);

which is used as: AddTodo(todo));

Modifying the doc.attrs value directly seems a bit wrong, but appears to work as desired. Note that I’m only ever using this in conjunction with a step that actually modifies the document content (in a standard way), so I’m not sure how well this would work if it’s the only step being applied.

Is there a way to get a Node View from a Node object?

Thanks, @Jordan this helped

I have made a quick tweak to the above so you can set and remove individual keys. Might save other people some time.


const transaction =
  .step(new SetDocAttr('foo', 'bar'))
  .step(new SetDocAttr('bar', ['foo', 'foo']));


Custom Step:

// @flow
import { Step, StepResult } from 'prosemirror-transform';

class SetDocAttr extends Step {
  constructor(key: string, value: any, stepType?: string = 'SetDocAttr') {
    this.stepType = stepType;
    this.key = key;
    this.value = value;
  apply(doc) {
    this.prevValue = doc.attrs[this.key];
    doc.attrs[this.key] = this.value;
    return StepResult.ok(doc);
  invert() {
    return new SetDocAttr(this.key, this.prevValue, 'revertSetDocAttr');
  map() {
    return null;
  toJSON() {
    return {
      stepType: this.stepType,
      key: this.key,
      value: this.value,
  static fromJSON(json) {
    return new SetDocAttr(json.key, json.value, json.stepType);


Thank you guys, I had exactly the same problem and the solution posted really saves my day! Cheers.