"binary fragment but received text fragment" error when assigning Tabulator to empty DataFrame in Django via bokeh-django

I stumbled upon Panel while building a Django app. Very excited to find that I could embed Panel’s extensive features/widgets/components etc through the bokeh-django package. That helped to reduce the effort on the UI side considerably. However, I hit a roadblock that hopefully I might get some hints on how to solve the problem.

Objective:
Button to reset a Tabulator to empty (meaning, no changes in columns, zero rows).

Sample Standalone Code:

import panel as pn
import pandas as pd

pn.extension()
pn.extension('tabulator')  # tried with and without this

button = pn.widgets.Button(name="Reset Table",button_type='primary')
data = {
            'row1': {'col1': 1, 'col2': 'a', 'col3': True},
            'row2': {'col1': 2, 'col2': 'b', 'col3': False},
            'row3': {'col1': 3, 'col2': 'c', 'col3': True}
        }

df = pd.DataFrame.from_dict(data, orient='index')
table = pn.widgets.Tabulator(df,layout='fit_data_table')
 
def instantiate_table_edits(event,table):
    if event:
        print('clicked')
        df_empty = pd.DataFrame(columns=['col1','col2','col3'])
        table = pn.widgets.Tabulator(df_empty,layout='fit_data_table')
        # table.value = df   # no difference
        return table
    else:
        return table

pn.panel(
    pn.Column(
        pn.pane.HTML('Please click to reset table'),
        pn.Row(
            button,
            pn.bind(instantiate_table_edits, button,table=table),
        ),
        # sizing_mode='stretch_width',
    )
).servable()
# execute: panel serve app.py --dev

What happened:
In django, it does nothing and the console output (inspect mode) show an error “binary fragment but received text fragment” everytime the button is clicked (see screenshot below)

What I’ve tried:
Jupyter and standalone Panel app (code above) works. Although I did notice a change in the layout of the tabulator after resetting it using the code above, executed with panel server.
Browser inspect console output grumbled about “[Violation] Added non-passive event listener to a scroll”, but at least the button works.
Screenshot from 2025-02-28 20-31-19
Screenshot from 2025-02-28 20-31-33

How to reproduce the problem
(Un)Luckily, the sample app provided with bokeh-django also had the same problem when I added the button and tabulator, so hopefully it’s not the complexity or errors in my django environment. I’ve posted the issue on bokeh-django git site. The steps to reproduce the problem is outlined there in case if anyone is interested to reproduce in the django environment. (sorry for duplicate posting here but trying to reach a different audience to see what I’m missing)

Appreciate any ideas or directions I should take.

One thought is maybe instead of rebuilding the Tabulator, you simply change its data.
https://panel.holoviz.org/how_to/best_practices/user_experience.html#reuse-objects-for-efficiency

Thanks for the info @ahuang11 . I rewrote based on that. It worked as a stand-alone app but still have the same problem through bokeh-django. Although, I did try something that got it to somewhat “work”. Here’s the stand-alone app and I’ll describe the additions after the code below:

import panel as pn
import pandas as pd

pn.extension()
pn.extension('tabulator')

# tabulator - prefilled with rows
data = [[1,'a',True],[2,'b',False],[3,'c',True]]
df = pd.DataFrame(data,columns=['col1','col2','col3'])
table = pn.widgets.Tabulator(df) 

# watch for any changes in tabulator.value
def tabulator_value_changed(event):
    print(f"-----tabulator_value_changed--{event=}-------")

table.param.watch(tabulator_value_changed, 'value')

# button to fill table
button_fill = pn.widgets.Button(name="Fill Table",button_type="primary")
def fill(event):
    if event:
        print('clicked')
        data = [[1,'a',True],[2,'b',False],[3,'c',True]]
        df_fill = pd.DataFrame(data,columns=['col1','col2','col3'])
        table.value=df_fill
button_fill.on_click(fill)

# button to empty table
button_empty = pn.widgets.Button(name="Empty Table",button_type="danger")
def empty(event):
    if event:
        print('empty clicked')
        df_empty = pd.DataFrame(data=[],columns=['col1','col2','col3'])
        table.value=df_empty
        
button_empty.on_click(empty)

pn.panel(
    pn.Column(
        pn.Row(
            button_fill,
            button_empty
        ),
        pn.Row(
            table,
        )
    )
).servable()
# execute using:  panel serve app.py --dev

Screenshot from 2025-03-03 22-17-23
The code works as a stand-alone app.

When I add this approach to bokeh-django, I did not work. However, when I added a line to assign the table.value=None before reassigning it to a dataframe, it kind of worked:

def empty(event):
    if event:
        print('empty clicked')
        df_empty = pd.DataFrame(data=[],columns=['col1','col2','col3'])
        table.value=None  # <<<----------ADDED THIS
        table.value=df_empty
def fill(event):
    if event:
        print('clicked')
        data = [[1,'a',True],[2,'b',False],[3,'c',True]]
        df_fill = pd.DataFrame(data,columns=['col1','col2','col3'])
        table.value=None  # <<<----------ADDED THIS
        table.value=df_fill

Click on “Empy table” button once, it empties the table. Click on the “Fill table” button, twice, both times it gives the error and does nothing. Third time it fills the table, no error. Its almost as if the table.value was not reset or refreshed until the next event cycle. I tried using pn.bind thinking it would refresh but it’s still not.

One thing I noticed is that the way the bokeh-django sample code had in the ShapeViewer.py is:

class ShapeViewer(param.Parameterized):
......
def panel(self):
    return pn.panel(
            pn.Column(
                pn.Row(
                    button_fill,
                    button_empty
                ),
                pn.Row(
                    table,
                )
            )
        )

and view.py:

def shape_viewer_handler(doc: Document) -> None:
    panel = shape_viewer()
    panel.server_doc(doc)   # <<<<------ maybe there's something wrong here

and url.py:

bokeh_apps = [
    ....
    autoload("shapes", views.shape_viewer_handler),
    ....
]

Here’s the browser inspect error: