NodeView for placeholders?

There’s some discussion on adding placeholders in the archives

e.g. How to: input-like placeholder behavior

People seem to be going with decorations. I was wondering if a NodeView might be a simpler solution (in the situation where a specific node-type needs the placeholder). It would be stateless, and you wouldn’t have to worry about finding the node to be decorated.

Is there a way to implement a NodeView in terms of the default behaviour? E.g. render the node as normal, but get a chance to make changes. For a placeholder, you could conditionally add a class to the node (and use ::after and content: "..." in the stylesheet to add the placeholder).

I guess this is complicated by the fact that PM sometimes doesn’t render the node at all, because the normal contenteditable browser behaviour will be enough. I assume this is the reason that putting conditional logic in the schema’s toDOM doesn’t work for this use case. I can test for node.content.size == 0 and add my class, but toDOM is not re-run for every change.

Thanks in advance for any insight.

ProseMirror will render all nodes. It just won’t re-render them from toDOM when their content changes. Implementing a node view that just provides the ‘default’ behavior should be simple enough, so just doing the straightforward thing in this case should work.

Thanks for the advice. Not managed to get it working yet. This is my first foray into NodeViews and it seems I’m misunderstanding something about how they work.

Something along these lines is what I assumed you meant by “the straightforward thing”

// Schema toDom for 'fieldName':
toDom: (node) => ['label', node.content.size == 0 ? {class: 'empty'} : {}, 0]

// nodeViews editor prop
nodeViews: {
  'fieldName': node => ({dom: DOMSerializer.fromSchema(schema).serializeNode(node)})
}

But the ‘empty’ class is still not added/removed on editing the content.

I tried adding an update method to my NodeView, but it didn’t seem to be called at all.

As a sanity check, I tried messing with the dom element by adding a random attribute, and I noticed that attribute was not showing up in the browser. Some print debugging showed that my node-view function is being called for all the fieldName nodes as expected, but the object it returns seems to be having no effect.

Update - I realised the nodeview should also have a contentDOM, but this didn’t change the fact that the dom element is not showing up in the page. (I assume it’s allowed for dom and contentDOM to be the same element in a case like this?)

I’ve solved this now, posting in case it proves useful to others. The issues I was having with the NodeView seemingly not having any effect were because I’m on TipTap. I switched to using the TipTap way of registering a NodeView and it all works now. It’s converted back to regular ProseMirror here:

// Schema toDom for 'fieldName':
toDom: (node) => ['label', node.content.size == 0 ? {class: 'blank'} : {}, 0]

// nodeViews editor prop
nodeViews: {
  'fieldName': node => {
    // You'd want to create the DOMSerializer beforehand in a real situation
    const dom = DOMSerializer.fromSchema(schema).serializeNode(node) as Element
    const classes = dom.classList
    return {
      dom, contentDOM: dom,
      update(node) {
        const isBlank = node.content.size == 0, wasBlank = classes.contains('blank')
        if (isBlank && !wasBlank) {
          classes.add('blank')
          return true
        } else if (!isBlank && wasBlank) {
          classes.remove('blank')
          return true
        }
        return false
      }
    }
  }
}

// css
label.blank::before {
  content: "My Placeholder"; ...
}