Skip to content

Commit

Permalink
Add simple nested editor to Tabulator (#7251)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro committed Sep 12, 2024
1 parent fe43b8b commit c5e3125
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
62 changes: 59 additions & 3 deletions examples/reference/widgets/Tabulator.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The list of valid *Tabulator* formatters can be found in the [Tabulator documentation](https://tabulator.info/docs/5.5/format#format-builtin).\n",
"The list of valid *Tabulator* formatters can be found in the [Tabulator documentation](https://tabulator.info/docs/{{TABULATOR_VERSION}}/format#format-builtin).\n",
"\n",
"Note that the equivalent specification may also be applied for column titles using the `title_formatters` parameter (but does not support Bokeh `CellFormatter` types)."
]
Expand Down Expand Up @@ -265,6 +265,62 @@
"edit_table.on_edit(lambda e: print(e.column, e.row, e.old, e.value))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Nested editor\n",
"Suppose you want an editor to depend on values in another cell. The `nested` type can be used. The `nested` type needs two arguments, `options` and `lookup_order`; the latter describes how the `options` should be looked up. \n",
"\n",
"Let's create a simple DataFrame with three columns, the `2` column now depends on the values in the `0` and `1` column. If the `0` is `A`, the `2` column should always be between 1 and 5. If the `0` column is `B`, the `2` column will now also depend on the `1` column. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"options = {\n",
" \"A\": [1, 2, 3, 4, 5],\n",
" \"B\": {\"1\": [6, 7, 8, 9, 10], \"2\": [11, 12, 13, 14, 15]},\n",
"}\n",
"tabulator_editors = {\n",
" \"0\": {\"type\": \"list\", \"values\": [\"A\", \"B\"]},\n",
" \"1\": {\"type\": \"list\", \"values\": [1, 2]},\n",
" \"2\": {\"type\": \"nested\", \"options\": options, \"lookup_order\": [\"0\", \"1\"]},\n",
"}\n",
"\n",
"nested_df = pd.DataFrame({\"0\": [\"A\", \"B\"], \"1\": [1, 2], \"2\": [None, None]})\n",
"nested_table = pn.widgets.Tabulator(nested_df, editors=tabulator_editors, show_index=False)\n",
"nested_table"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Some things to note about the `nested` editor:\n",
"- Only string keys can be used in `options` dictionary. \n",
"- Care must be taken so there is always a valid option for the `nested` editor.\n",
"- No guarantee is made that the value shown is a `nested` editor is a valid option.\n",
"\n",
"For the last point, you can use an `on_edit` callback, which either change the value or clear it. Below is an example of how to clear it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def clear_nested_column(event):\n",
" if event.column in [\"0\", \"1\"]:\n",
" nested_table.patch({\"2\": [(event.row, None)]})\n",
"\n",
"nested_table.on_edit(clear_nested_column)"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -1313,9 +1369,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"These and other available *Tabulator* options are listed at http://tabulator.info/docs/5.4/options. \n",
"These and other available *Tabulator* options are listed at http://tabulator.info/docs/{{TABULATOR_VERSION}}/options. \n",
"\n",
"Obviously not all options will work though, especially any settable callbacks and options which are set by the internal Panel tabulator module (for example the `columns` option).\n",
"Obviously not all options will work though, especially any settable callbacks and options which are set by the internal Panel tabulator module.\n",
"Additionally it should be noted that the configuration parameter is not responsive so it can only be set at instantiation time."
]
}
Expand Down
19 changes: 19 additions & 0 deletions panel/models/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,20 @@ const datetimeEditor = function(cell: any, onRendered: any, success: any, cancel
return input
}

const nestedEditor = function(cell: any, editorParams: any) {
//cell - the cell component for the editable cell

const row = cell.getRow().getData()
let values = editorParams.options
for (const i of editorParams.lookup_order) {
values = row[i] in values ? values[row[i]] : []
if (Array.isArray(values)) {
break
}
}
return values ? values : []
}

function find_column(group: any, field: string): any {
if (group.columns != null) {
for (const col of group.columns) {
Expand Down Expand Up @@ -955,6 +969,11 @@ export class DataTabulatorView extends HTMLBoxView {
tab_column.editor = dateEditor
} else if (tab_column.editor === "datetime") {
tab_column.editor = datetimeEditor
} else if (tab_column.editor === "nested") {
tab_column.editorParams.valuesLookup = (cell: any) => {
return nestedEditor(cell, tab_column.editorParams)
}
tab_column.editor = "list"
}
} else if (ctype === "StringEditor") {
if (editor.completions.length > 0) {
Expand Down
43 changes: 43 additions & 0 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,49 @@ def test_tabulator_editors_tabulator_multiselect(page, exception_handler_accumul
assert not exception_handler_accumulator


@pytest.mark.parametrize("opt0", ['A', 'B'])
@pytest.mark.parametrize("opt1", ["1", "2"])
def test_tabulator_editors_nested(page, opt0, opt1):
df = pd.DataFrame({"0": ["A"], "1": [1], "2": [None]})

options = {
"A": list(range(5)),
"B": { "1": list(range(5, 10)), "2": list(range(10, 15))},
}
tabulator_editors = {
"0": {"type": "list", "values": ["A", "B"]},
"1": {"type": "list", "values": [1, 2]},
"2": {"type": "nested", "options": options, "lookup_order": ["0", "1"]},
}

widget = Tabulator(df, editors=tabulator_editors, show_index=False)
serve_component(page, widget)

cells = page.locator('.tabulator-cell.tabulator-editable')
expect(cells).to_have_count(3)

# Change the 0th column
cells.nth(0).click()
item = page.locator('.tabulator-edit-list-item', has_text=opt0)
expect(item).to_have_count(1)
item.click()

# Change the 1th column
cells.nth(1).click()
item = page.locator('.tabulator-edit-list-item', has_text=opt1)
expect(item).to_have_count(1)
item.click()

# Check the last column matches
cells.nth(2).click()
items = page.locator('.tabulator-edit-list-item')
expect(items).to_have_count(5)

items_text = items.all_inner_texts()
expected = options[opt0][opt1] if opt0 == "B" else options[opt0]
assert items_text == list(map(str, expected))


@pytest.mark.parametrize('layout', Tabulator.param['layout'].objects)
def test_tabulator_column_layouts(page, df_mixed, layout):
widget = Tabulator(df_mixed, layout=layout)
Expand Down

0 comments on commit c5e3125

Please sign in to comment.