Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Tabulator.on_edit callbacks #2887

Merged
merged 5 commits into from
Nov 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,29 @@
" 'str': {'type': 'autocomplete', 'values': True}\n",
"}\n",
"\n",
"pn.widgets.Tabulator(df[['float', 'bool', 'str']], editors=bokeh_editors)"
"edit_table = pn.widgets.Tabulator(df[['float', 'bool', 'str']], editors=bokeh_editors)\n",
"\n",
"edit_table"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When editing a cell the data stored on the `Tabulator.value` is updated and you can listen to any changes using the usual `.param.watch(callback, 'value')` mechanism. However if you need to know precisely which cell was changed you may also attach an `on_edit` callback which will be passed a `TableEditEvent` containing the:\n",
"\n",
"- `column`: Name of the edited column\n",
"- `row`: Integer index of the edited row\n",
"- `value`: The updated value"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"edit_table.on_edit(lambda e: print(e.column, e.row, e.value))"
]
},
{
Expand Down
12 changes: 12 additions & 0 deletions panel/models/tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Any, Bool, Dict, Either, Enum, Instance, Int, List, Nullable,
String, Tuple
)
from bokeh.events import ModelEvent
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.models.layouts import HTMLBox
from bokeh.models.widgets.tables import TableColumn
Expand All @@ -25,6 +26,17 @@
'bootstrap4', 'materialize', 'bulma', 'semantic-ui', 'fast'
]

class TableEditEvent(ModelEvent):

event_name = 'table-edit'

def __init__(self, model, column, row, value=None):
self.column = column
self.row = row
self.value = value
super().__init__(model=model)


def _get_theme_url(url, theme):
if 'bootstrap' in theme:
url += 'bootstrap/'
Expand Down
15 changes: 14 additions & 1 deletion panel/models/tabulator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {isArray} from "@bokehjs/core/util/types"
import {HTMLBox} from "@bokehjs/models/layouts/html_box"
import {build_views} from "@bokehjs/core/build_views"
import {ModelEvent, JSON} from "@bokehjs/core/bokeh_events"
import {div} from "@bokehjs/core/dom"
import {Enum} from "@bokehjs/core/kinds"
import * as p from "@bokehjs/core/properties";
Expand All @@ -12,6 +13,17 @@ import {debounce} from "debounce"
import {transform_cds_to_records} from "./data"
import {PanelHTMLBoxView, set_size} from "./layout"

export class TableEditEvent extends ModelEvent {
event_name: string = "table-edit"

constructor(readonly column: string, readonly row: number) {
super()
}

protected _to_json(): JSON {
return {model: this.origin, column: this.column, row: this.row}
}
}

declare const Tabulator: any;

Expand Down Expand Up @@ -408,7 +420,7 @@ export class DataTabulatorView extends PanelHTMLBoxView {
cell.getRow().toggleSelect();
}
}
columns.push(column)
columns.push({...column})
}
}
for (const column of this.model.columns) {
Expand Down Expand Up @@ -760,6 +772,7 @@ export class DataTabulatorView extends PanelHTMLBoxView {
this._tabulator_cell_updating = true
this.model.source.patch({[field]: [[index, value]]});
this._tabulator_cell_updating = false
this.model.trigger_event(new TableEditEvent(field, index))
}
}

Expand Down
17 changes: 16 additions & 1 deletion panel/tests/widgets/test_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
)

from panel.depends import bind
from panel.widgets import Button, DataFrame, Tabulator, TextInput
from panel.models.tabulator import TableEditEvent
from panel.widgets import Button, TextInput
from panel.widgets.tables import DataFrame, Tabulator

pd_old = pytest.mark.skipif(LooseVersion(pd.__version__) < '1.3',
reason="Requires latest pandas")
Expand Down Expand Up @@ -1184,3 +1186,16 @@ def test_tabulator_download_menu_custom_kwargs():
assert filename.value == 'file.csv'
assert filename.name == 'Enter filename'
assert button.name == 'Download table'

def test_tabulator_patch_event():
df = makeMixedDataFrame()
table = Tabulator(df)

values = []
table.on_edit(lambda e: values.append((e.column, e.row, e.value)))

for col in df.columns:
for row in range(len(df)):
event = TableEditEvent(model=None, column=col, row=row)
table._on_edit(event)
assert values[-1] == (col, row, df[col].iloc[row])
26 changes: 24 additions & 2 deletions panel/widgets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,13 +403,13 @@ def stream(self, stream_value, rollover=None, reset_index=True):

Arguments
---------
stream_value (Union[pd.DataFrame, pd.Series, Dict])
stream_value: (Union[pd.DataFrame, pd.Series, Dict])
The new value(s) to append to the existing value.
rollover: int
A maximum column size, above which data from the start of
the column begins to be discarded. If None, then columns
will continue to grow unbounded.
reset_index (bool, default=True):
reset_index: (bool, default=True)
If True and the stream_value is a DataFrame,
then its index is reset. Helps to keep the
index unique and named `index`
Expand Down Expand Up @@ -865,6 +865,7 @@ def __init__(self, value=None, **params):
configuration = params.pop('configuration', {})
self.style = None
self._child_panels = {}
self._on_edit_callbacks = []
super().__init__(value=value, **params)
self._configuration = configuration
self.param.watch(self._update_children, self._content_params)
Expand All @@ -886,6 +887,11 @@ def _cleanup(self, root):
p._cleanup(root)
super()._cleanup(root)

def _on_edit(self, event):
event.value = self.value[event.column].iloc[event.row]
for cb in self._on_edit_callbacks:
cb(event)

def _get_theme(self, theme, resources=None):
from ..io.resources import RESOURCE_MODE
from ..models.tabulator import _get_theme_url, THEME_PATH, THEME_URL
Expand Down Expand Up @@ -1206,6 +1212,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
child_panels, doc, root, parent, comm
)
self._link_props(model, ['page', 'sorters', 'expanded', 'filters'], doc, root, comm)
model.on_event('table-edit', self._on_edit)
return model

def _update_model(self, events, msg, root, model, doc, comm):
Expand Down Expand Up @@ -1400,3 +1407,18 @@ def download_menu(self, text_kwargs={}, button_kwargs={}):
table.filename = cb_obj.value
""")
return filename, button

def on_edit(self, callback):
"""
Register a callback to be executed when a cell is edited.
Whenever a cell is edited on_edit callbacks are called with
a TableEditEvent as the first argument containing the column,
row and value of the edited cell.

Arguments
---------
callback: (callable)
The callback to run on edit events.
"""
self._on_edit_callbacks.append(callback)