diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index 5d29793032..a002a1b5e1 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -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)." ] @@ -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": {}, @@ -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." ] } diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index aaa613fe5d..97485cf6f5 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -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) { @@ -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) { diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 4ba7cc8bde..bd846eecd9 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -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)