Skip to content

DataTable

oldmangan edited this page Oct 7, 2026 · 7 revisions

A table of dict rows: sortable, filterable, selectable, editable in place, with a right-click menu, resizable, reorderable and frozen columns, drop-to-upload, and CSV export.

from wapyt import ColumnConfig, DataTableConfig, TableAction

table = self.add_datatable("files", DataTableConfig(
    columns=[
        ColumnConfig(id="name", header="Name", icon_by="icon"),
        ColumnConfig(id="size", header="Size", align="right", width=90, sort_by="size_bytes"),
    ],
    selection="multi",
    filterable=True,
    group_dirs_first="is_dir",
    context_actions=[TableAction("download", "Download", "mdi-download")],
))
table.set_rows(rows)
table.on_activate(lambda p: open_entry(p["row"]))

Sorting and filtering are local

They run in the browser on the rows already loaded, with no round trip. Pre-format display values in Python (size as "1.2 MB") and point sort_by at the raw field so the order stays right. For server-side sorting, see how Monguana points every column's sort_by at a hidden rank key.

Columns

ColumnConfig(id, header, width, align, sortable, sort_by, type, ellipsis, icon_by, editable, editor, options, required, hidden, css, cell_class_by):

  • type="icon": the value is an MDI class, drawn as a glyph.
  • icon_by="key": draw the MDI class in row[key] before a text cell's value; row[key + "_title"] is its tooltip.
  • ellipsis (default on): truncate, with a tooltip of the full text that shows only when the text is actually cut off.

Editing cells

Added in #37.

DataTableConfig(columns=[
    ColumnConfig(id="name", header="Name", editable=True, required=True),
    ColumnConfig(id="qty", header="Qty", align="right", editable=True, editor="number"),
    ColumnConfig(id="status", header="Status", editable=True, editor="select",
                 options=["open", {"value": "done", "label": "Done"}]),
    ColumnConfig(id="done", header="Done", editable=True, editor="checkbox"),
    ColumnConfig(id="due", header="Due", editable=True, editor="date"),
])

def saved(e):                       # {id, column, value, old_value, row}
    if not api.update(e["id"], e["column"], e["value"]):
        table.set_cell(e["id"], e["column"], e["old_value"])
        table.set_cell_error(e["id"], e["column"], "The server refused this change")

table.on_edit(saved)
editor Edits with value in on_edit
text (default) a text box string
number a text box that must parse as a number number, or None when emptied
select a dropdown of options; the cell shows the option's label the option's value
checkbox a checkbox drawn in the cell; click or Space toggles and saves bool
date the browser's date picker "YYYY-MM-DD", or None
  • Start editing by double-clicking an editable cell (other cells still fire on_activate), or on a focused cell press Enter or F2, or just start typing. Arrow keys move between editable cells, and the whole table is one Tab stop.
  • Finish with Enter (save), Tab / Shift+Tab (save and move on), or Escape (cancel; an enclosing ModalWindow stays open). Clicking away saves, or drops a value that is not valid.
  • Validation: a number that does not parse, or an emptied required cell, keeps the editor open with the reason.
  • Saving changes the row in place. Only that cell is redrawn, so the row stays where it is until the next sort, filter or set_rows.
  • When the server says no, set_cell puts a value back without firing on_edit, and set_cell_error marks the cell red with the message as its tooltip (None clears it; a successful edit clears it too).
  • edit_cell(id, column) opens an editor from code, for example on a row you just added; cancel_edit() closes one without saving.

Context menu

TableAction(id, label, icon, danger, separator) entries in context_actions. Right-clicking a selected row keeps a multi-selection; right-clicking any other row selects just that row. The menu is a ContextMenu: ↑/↓, Home/End and Enter work, Escape closes it, Shift+F10 or the Menu key opens it from the keyboard, and focus returns to where it was.

Table options

DataTableConfig
id_field Row key holding the stable id (default "id")
selection "none", "single" (default) or "multi" (checkboxes, ctrl/shift clicks)
sort_by, sort_dir Initial sort
group_dirs_first Row key whose truthy rows stay on top whatever the sort
resizable_columns, reorderable_columns, min_column_width Opt-in column layout editing
frozen_columns Leading columns that stay put while the table scrolls sideways; see Frozen columns
row_class_by Row key holding class names for that row; see Row and cell classes
drop_upload Accept dropped files
filter_placeholder, empty_text, loading_text Copy

Once any column is resized, every column gets a pixel width and the table can scroll sideways. Persist layouts from on_columns; restore them with set_columns (widths and hidden columns included).

Row and cell classes

Added in #42. Style rows and cells from their data, with your own CSS:

DataTableConfig(row_class_by="status_class", columns=[
    ColumnConfig(id="task", header="Task"),
    ColumnConfig(id="due", header="Due", css="num", cell_class_by="due_class"),
])
# rows: {"id": "1", "task": "Invoice", "due": "...", "status_class": "overdue", "due_class": "late"}
Option Adds
DataTableConfig(row_class_by="key") The class names in row[key] (a space-separated string or a list) to the row
ColumnConfig(css="...") Fixed classes to the column's header and every cell
ColumnConfig(cell_class_by="key") The class names in row[key] to that column's cell in each row

Only values that look like class names are applied, so data cannot break the markup. Classes stay through edits and set_cell. The older row["_class"] still works.

Hidden columns

Added in #42.

ColumnConfig(id="internal_id", header="ID", hidden=True)
table.hide_column("owner"); table.show_column("owner")
  • A hidden column keeps its place and width, so showing it puts it back where it was.
  • It does not render, match the filter, freeze, edit, or export (unless named in to_csv(columns=[...])).
  • on_columns fires with reason="hide" or "show", and get_column_state() includes hidden, so a saved layout restores it.
  • Methods: hide_column, show_column, set_column_hidden(id, hidden), is_column_hidden.

Frozen columns

DataTableConfig(columns=[...], frozen_columns=2)   # dhxpyt's leftSplit
table.set_frozen_columns(3)                        # later; 0 unfreezes

Added in #38. The first N columns stay in place while the rest scroll sideways; with selection="multi" the checkbox column goes with them.

  • Only matters once the table is wider than its panel, which happens when the column widths add up to more than the panel (fixed widths or resizable_columns).
  • They stay right as things change: resizing a column or the panel moves the frozen offsets, and sorts, set_rows and edits keep them.
  • They look solid: frozen cells carry the row's hover and selected tints, so scrolled cells never show through, and the top-left corner stays on top when you scroll both ways. The frozen edge has a line, and a shadow once the table is scrolled.

CSV export

csv_text = table.to_csv()                     # the CSV as a string
table.export_csv("documents.csv")             # a browser download; returns the row count
table.export_csv("picked.csv", selected_only=True, columns=["name", "size"])

Added in #39. It exports what the table shows: the filtered, sorted rows, in the current column order, with a select column's labels.

Option
selected_only Only the selected rows
columns Column ids to include, in this order (icon columns are left out unless named)
raw Stored values instead of what the cells show
header Include the header row (default on)
delimiter One character, default ,
safe Default on; see below
bom export_csv only: a UTF-8 byte-order mark (default on), so Excel reads accented text
  • Safe to open in a spreadsheet. With safe on, text that Excel or Sheets would run as a formula (starting with =, +, -, @, a tab or a carriage return) gets a leading ', so a malicious value from a database cannot run when someone opens the file. Numbers, including negative ones, are left alone.
  • Standard CSV quoting (commas, quotes, line breaks, leading or trailing spaces) and CRLF line ends; booleans as true / false, empty values empty.
  • The download is built in the browser, with no server round trip. For exports bigger than what is loaded (every document a query matches), build the file on the server and use File Transfer.

Events

Method Payload
on_select {ids, id, rows}
on_activate {id, row} (double-click; on an editable cell, a double-click edits instead)
on_edit {id, column, value, old_value, row}
on_action {action, id, row, selected}
on_sort {column, direction}
on_filter {value}
on_columns {reason: "resize" | "reorder" | "hide" | "show", column, columns: [{id, width, hidden}]}
on_drop {files: [{name, size, type}]} (metadata only)

Dropped files: read the handles with get_dropped_files() and pass them to filetransfer.adopt(). Bytes never cross into Python.

Methods

set_rows, get_rows, get_row, set_columns, get_column_state, move_column, get_selected_ids, get_selected_rows, select, clear_selection, sort, set_filter, set_busy, set_empty_text, set_cell, set_cell_error, edit_cell, cancel_edit, set_frozen_columns, hide_column, show_column, set_column_hidden, is_column_hidden, to_csv, export_csv, destroy.

Selecting rows updates them in place without redrawing the table (since #37), so focus and row positions hold steady while you click.

Not yet

No footer rows or row spans; see the Roadmap.

Clone this wiki locally