How to bind HTML contents using ReactiveHTML

I’m facing a couple issues here:

  1. contenteditable is not passed downstream to the nested #shadow-root contents

  2. When I manually inject contenteditable="true" into the browser’s HTML and edit the contents, I get back a DOMEvent


 message: Message 'PATCH-DOC' content: {'events': [{'kind': 'MessageSent', 'msg_type': 'bokeh_event', 'msg_data': {'type': 'event', 'name': 'dom_event', 'values': {'type': 'map', 'entries': [['model', {'id': 'p1156'}], ['node', 'div'], ['data', {'type': 'map', 'entries': [['detail', 0], ['target', {'type': 'map', 'entries': [['boundingClientRect', {'type': 'map'}]]}], ['currentTarget', {'type': 'map', 'entries': [['boundingClientRect', {'type': 'map'}]]}], ['relatedTarget', None], ['type', 'oninput']]}]]}}}]}
 error: ValueError("String parameter 'content' only takes a string value, not value of type <class 'dict'>.")
Traceback (most recent call last):
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/server/protocol_handler.py", line 97, in handle
    work = await handler(message, connection)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/server/session.py", line 94, in _needs_document_lock_wrapper
    result = func(self, *args, **kwargs)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/server/session.py", line 288, in _handle_patch
    message.apply_to_document(self.document, self)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/protocol/messages/patch_doc.py", line 104, in apply_to_document
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.payload, setter=setter))
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 443, in invoke_with_curdoc
    return f()
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/protocol/messages/patch_doc.py", line 104, in <lambda>
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.payload, setter=setter))
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/document.py", line 376, in apply_json_patch
    DocumentPatchedEvent.handle_event(self, event, setter)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/events.py", line 246, in handle_event
    event_cls._handle_event(doc, event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/events.py", line 281, in _handle_event
    cb(event.msg_data)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 390, in trigger_event
    model._trigger_event(event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/util/callback_manager.py", line 113, in _trigger_event
    self.document.callbacks.notify_event(cast(Model, self), event, invoke)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 260, in notify_event
    invoke_with_curdoc(doc, callback_invoker)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 443, in invoke_with_curdoc
    return f()
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/bokeh/util/callback_manager.py", line 109, in invoke
    cast(EventCallbackWithEvent, callback)(event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/reactive.py", line 487, in _server_event
    self._comm_event(doc, event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/reactive.py", line 474, in _comm_event
    state._handle_exception(e)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/io/state.py", line 431, in _handle_exception
    raise exception
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/reactive.py", line 472, in _comm_event
    self._process_bokeh_event(doc, event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/reactive.py", line 409, in _process_bokeh_event
    self._process_event(event)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/panel/reactive.py", line 1955, in _process_event
    cb(event)
  File "/Users/airbook/Applications/Developer/python/repos/resuumi_playground/test.py", line 15, in _div_input
    self.content = event.data
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/param/parameterized.py", line 369, in _f
    return f(self, obj, val)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/param/parameterized.py", line 1201, in __set__
    self._validate(val)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/param/parameterized.py", line 1349, in _validate
    self._validate_value(val, self.allow_None)
  File "/Users/airbook/miniforge3/envs/resuumi/lib/python3.10/site-packages/param/parameterized.py", line 1345, in _validate_value
    raise ValueError("String parameter %r only takes a string value, "
ValueError: String parameter 'content' only takes a string value, not value of type <class 'dict'>.
import panel as pn
import param

pn.extension()

class EditableDiv(pn.reactive.ReactiveHTML):
    content = param.String(default="Edit me")

    _template = """
    <div id="div" class="test" contenteditable="true" 
         oninput="${_div_input}">${content}</div>
    """

    def _div_input(self, event):
        print(event.data)
        self.content = event.data

EditableDiv().servable()

Originally: contenteditable=“true” doesn’t update pn.pane.HTML.object · Issue #4984 · holoviz/panel (github.com)

I think you need to insert the text in the div as a literal value with {{...}}. Then you can use a script to to set data.content.

I’ve refactor and renamed to the below. Hope it helps.

import panel as pn
import param

pn.extension()


class EditableDiv(pn.reactive.ReactiveHTML):
    value = param.String()
    placeholder = param.String(default="Edit me")

    _template = """
    <div id="div" class="test" contenteditable="true" 
         oninput="${script('some_script')}">{{placeholder}}</div>
    """

    _scripts = {
        "some_script": "data.value=div.innerText;console.log(event.data);console.log(div.innerText)"
    }


editable = EditableDiv()

pn.Column(editable, editable.param.value, margin=50).servable()
2 Likes

Thank you for helping me figure this out!

I cleaned it up a bit to use object instead of value:

import panel as pn
import param

pn.extension()

class EditableDiv(pn.reactive.ReactiveHTML):
    object = param.String(
        default="--",
        doc="The HTML content"
    )

    _template = """
    <div id="div" contenteditable="true" 
         oninput="${script('sync')}">{{ object }}</div>
    """

    _scripts = {"sync": "data.object = div.innerHTML"}

editable = EditableDiv()
pn.Column(editable, editable.param.object, margin=50).servable()

One final issue I have is that I can’t make it sync bidirectionally, e.g. changing the TextInput to update the HTML.

I tried:
_scripts = {"sync": "data.object = div.innerHTML; div.innerHTML = data.object"}

Have been playing around with ReactiveHTML myself and this is what I came up with:

class EditableDiv(pn.reactive.ReactiveHTML):
    object = param.String(
        default="--",
        doc="The HTML content"
    )

    _template = """
    <div id="div" contenteditable="true"
         onfocusout="${script('sync')}">{{object}}</div>
    <input id="object" hidden value="${object}"></input>
    """

    _scripts = {"sync": "data.object = div.innerHTML;",
                "object": "div.innerHTML = data.object;"
                }

A hidden node with value set to synced parameter will trigger matching ID JS code execution

Also changed the “oninput” to “onfocusout” because otherwise each character entered causes the parameter to change and trigger a reupdate, which moves the cursor back to the front. Probably undesired behavior.

2 Likes

Welcome to the community @gib . Thanks so much for sharing :heart:

1 Like

I wonder why this:

  1. isn’t editable
  2. partially shows div
import panel as pn
import param

pn.extension()

class EditableDiv(pn.reactive.ReactiveHTML):
    object = param.String(default="--", doc="The HTML content")

    _template = """
    <div id="paragraph" contenteditable="true" onfocusout="${script('sync')}">
        {{object}}
    </div>
    """

    _scripts = {
        "sync": "data.object = div.innerHTML;",
    }
d = EditableDiv(object="<div>To whom it may concern,<br><br>...</div>")
d