How to update the value of an <input>?

I defined some NodeSpec

    value: {
      content: 'text*',
      defining: true,
      attrs: {type: {default: "text"},
              value: {default: ''}},
      parseDOM: [{tag: 'input', getAttrs: node => {
        switch(node.type){
        case 'checkbox':
        case 'radio':
          return {type: node.type, value: node.checked}
        default:
          return {type: node.type, value: node.value}
        }
      }}],
      toDOM: node => {
        const {type, value} = node.attrs
        switch(type){
        case 'checkbox':
        case 'radio':
          return ["input", {type, checked: value}]
        default:
          return ["input", {type, value}]
        }
      }
    }

Now I want to update the state value when the user alter it. I found in the document some handle* EditorProps. For checkbox (and radio), I can do it by customize handleClickOn as

function handleClickOn(editorView, pos, node, nodePos, event) {
  if (node.type.name === 'value' && node.type.attrs.type == 'checkbox')
    editorView.dispatch(toggleAction(editorView.state, nodePos, node))
    return true
  }
}
function toggleAction(state, pos, node) {
  return state.tr.setNodeMarkup(pos, null, {value: !node.attrs.value})
}

For other input types, I guess I should turn to handleDOMEvents, to listen on input or change event. But there are only two parameters: view: EditorView and event: dom.Event, missing node: Node. I don’t know how to implement it without the Node parameter.

I think the nicest way to do this would be to define a node view that, when it detects change/input events on its field, uses the getPos callback it was given on creation to figure out where the field is, and dispatches the appropriate transaction.

You can also use posAtDOM to go from the event’s target property to a document position, but when the node is re-rendered (over which you don’t have control, without a node view), that’ll annoyingly mess with focus and cursor position inside the field.

Thank you @marijn for your advice. I will try it out.

I tried both ways. This is my feedback:

  1. I created a nodeview for the value node (according to the ImageView example in the reference document)
 class ValueView {
     constructor(node, view, getPos) {
         const {type, value} = node.attrs
         this.dom = document.createElement('input')
         this.dom.addEventListener("input", e => {
             e.preventDefault()
             console.log(getPos())
             view.dispatch(view.state.tr.setNodeMarkup(getPos(), null,
                                                       {type, value: e.target.innerText}))
         })
     }
     stopEvent() { return true }
 }

But I cannot see any change of the value attribute in the output of state.toJSON() no matter what I input in the ValueView.

  1. I added the EditorView prop
handleDOMEvents: {
             input: (view, event) => {
                 console.log('input', event)
                 var pos = view.posAtDOM(event.target);
                 console.log(pos)
                 view.dispatch(view.state.tr.setNodeMarkup(pos, null,
                                                           {type: event.target.type, value: event.target.innerText}))
             }
         },        

It reports an error:

Uncaught Error: NodeType.create can't construct text nodes
    at NodeType.create (index.js:1997)
    at Transaction.Transform.setNodeMarkup (index.js:777)
    at input (index.svelte:157)
    at index.js:3029
    at EditorView.someProp (index.js:4465)
    at runCustomHandler (index.js:3027)
    at HTMLDivElement.view.dom.addEventListener.view.eventHandlers.<computed> (index.js:3022)

innerText does not retrieve the content of an input field.

Oops, my mistake. I corrected it as e.target.value. Now it works … weridly.

I input ‘o’ firstly. But in the nodeview, it is cleared, and nothing changes from state.toJSON(). Next I input ‘k’. Again it is cleared in the nodeview, but the attrs value changed to ‘o’ this time. And then I input ‘o’. Again it is cleared in the nodeview, but the attrs value changed to ‘k’ this time. …

It seems there is a time-delay. And I cannot input more than one character in the nodeview since it is cleared immediately.

Can you tell what is wrong?

You might need an ignoreMutation method and an update method that re-syncs the DOM with the node’s attribute.

I added both the methods, but the weird time-delay effect is still there.

I set breakpoints in those two methods, seems they are never called.

Here is the final code:

class ValueView {
     constructor(node, view, getPos) {
         const {type, value} = node.attrs
         this.dom = document.createElement('input')
         this.dom.addEventListener("input", e => {
             e.preventDefault()
             console.log(getPos())
             view.dispatch(view.state.tr.setNodeMarkup(getPos(), null,
                                                       {type, value: e.target.value}))
         })
     }
     stopEvent() { return true }
     ignoreMutation(){
         return true
     }
     update(node) {
         console.log('debug', node)
         if (node.type.name != "value") return false
         this.dom.value = node.attrs.value
         return true
     }
 }

You’re not doing this.dom.value = value in your constructor. If I fix that, this appears to work just fine.

Yes, it works!

But I am still a little confused.

The nodeview is created only once, and node.attrs.value is an empty string ‘’ during construction. And When the user input something in the nodeview, the dom.value keeps updated.

I cannot see why/how this.dom.value = value matters.

My guess is that changes in the view triggers rerendering of the viewNode, I noticed the same behaviour when i was implementing drag and drop functionality.

But it does not trigger the constructor, let alone this.dom.value = value.