Skip to content

Commit

Permalink
Implement Tabulator.on_edit callbacks (#2887)
Browse files Browse the repository at this point in the history
* Implement Tabulator.on_edit callbacks

* Fix flakes

* Update docs

* Do not send value as part of event

* Add unit test
  • Loading branch information
philippjfr authored Nov 6, 2021
1 parent 25c708a commit 15d2878
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 5 deletions.
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)

0 comments on commit 15d2878

Please sign in to comment.