EditableTable
A grid edited in place: text, number, select, checkbox and date cells, checked, sorted, with rows added and deleted.
Demo
| (required) | (required) | Size | Active | Delete | ||
|---|---|---|---|---|---|---|
| KB-01 | Mechanical keyboard | 14 | Full | 2026-11-20 | ||
| MS-02 | Wireless mouse | 0 | — | 2026-11-04 | ||
| CB-USB | USB-C cable | 120 | 1 m | — | ||
| ST-09 | Monitor stand | 6 | — | — |
4 rows
| Item (required) | Owner (required) | Amount | Approved | Code |
|---|---|---|---|---|
| Laptops | Grace | 7200 | — | |
| Conference | Ada | 1850.5 | — |
2 rows
Edit a cell.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/editable_table.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/editable_table.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/editable_table.qwe
let editable_table_demo_stock = [ { sku: "KB-01", name: "Mechanical keyboard", qty: 14, size: "Full", active: true, restock: "2026-11-20" }, { sku: "MS-02", name: "Wireless mouse", qty: 0, size: null, active: true, restock: "2026-11-04" }, { sku: "CB-USB", name: "USB-C cable", qty: 120, size: "1 m", active: true, restock: null }, { sku: "ST-09", name: "Monitor stand", qty: 6, size: null, active: false, restock: null },]let editable_table_demo_columns = [ { key: "sku", label: "SKU", required: true, sortable: true, width: "110px" }, { key: "name", label: "Product", required: true, sortable: true }, { key: "qty", label: "Qty", type: "number", min: 0, max: 9999, sortable: true, width: "90px" }, { key: "size", label: "Size", type: "select", options: ["1 m", "2 m", "Compact", "Full"], width: "110px" }, { key: "active", label: "Active", type: "checkbox", width: "80px" }, { key: "restock", label: "Restock on", type: "date", min: "2026-01-01", sortable: true, width: "150px" },]component EditableTableDemoLive() { state log = [] state total = 0 <div.demo-stack> <EditableTable id="et-budget" label="Budget" can_add={true} new_row={{ item: "", owner: "Ada", amount: 0, approved: false }} rows={[ { item: "Laptops", owner: "Grace", amount: 7200, approved: true }, { item: "Conference", owner: "Ada", amount: 1850.5, approved: false }, ]} columns={[ { key: "item", label: "Item", required: true }, { key: "owner", label: "Owner", type: "select", options: ["Ada", "Grace", "Linus"], required: true }, { key: "amount", label: "Amount", type: "number", min: 0 }, { key: "approved", label: "Approved", type: "checkbox" }, { key: "code", label: "Code", readonly: true }, ]} on_cell={(i, k, v) => { log = take(push(log, `row ${i + 1}: ${k} = ${v}`) | reverse, 3) | reverse }} on_change={(rows) => { total = sum(rows | map((r) => r.amount ?? 0)) }}/> <p.demo-note>{len(log) == 0 ? "Edit a cell." : join(log, " · ")} {total > 0 ? `Total: ${total}` : ""}</p> </div>}view EditableTableDemo() = <div.demo-stack> <EditableTable id="et-stock" label="Stock" name="stock" can_add={true} can_delete={true} rows={editable_table_demo_stock} columns={editable_table_demo_columns}/> <EditableTableDemoLive/> </div>Source
ui/editable_table.qwe
// EditableTable: a grid of rows to edit in place, like a small spreadsheet: click// a cell (or press Enter, F2 or just type) to change it. Each column has a type:// text, number, select, checkbox or date; numbers and dates are checked, and a value// that is not right is shown so and not kept. Changed cells are marked.//// <EditableTable id="stock" label="Stock" name="items" can_add={true} can_delete={true} rows={items}// columns={[// { key: "sku", label: "SKU", required: true, sortable: true },// { key: "name", label: "Product", sortable: true },// { key: "qty", label: "Qty", type: "number", min: 0, sortable: true },// { key: "size", label: "Size", type: "select", options: ["S", "M", "L"] },// { key: "active", label: "Active", type: "checkbox" },// { key: "restock", label: "Restock on", type: "date" },// ]}/>//// id: a prefix for its ids, unique on the page. rows: a list of maps. columns, in// order, each { key, label, type?, options?, required?, min?, max?, readonly?,// sortable?, width? }: type "text" (default), "number" (on the right; min and max// bound it; whole numbers stay whole), "select" (options: strings or { label,// value }), "checkbox" (true or false: a click, Space or Enter flips it) or// "date" ("2026-03-14"; min and max as dates). required: it cannot be left empty.// readonly: shown, not edited. sortable: its header sorts the rows (again: the// other way); edits do not move a row until it is sorted again. width: a CSS width.// label: the table's name. can_add: an "Add row" button (add_label), whose row// starts as new_row (a map), or empty. can_delete: a × at the end of each row.// name: a <form> gets the rows under it, as JSON. empty: what it says with no rows.// The keys, not editing: the arrows, Home and End (with Ctrl: the first and last// cell), Page Up and Page Down (the first and last row), Enter or F2 to edit, a// letter or digit to start over with it, Delete to empty a cell. Editing: Enter// keeps it and goes down, Tab and Shift+Tab keep it and go across, Escape puts it// back. Leaving a cell keeps a good value.// on_change(rows): inside one of your components, called with all the rows after// each change. on_cell(index, key, value): called with the row's place in `rows`// (new rows after them), the column's key and the new value.import ui/theme.qwe use ui_clslet editable_table_css = css`.ui-et { display: grid; gap: 10px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-et-scroll { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-et table { width: 100%; border-collapse: separate; border-spacing: 0; }.ui-et th, .ui-et td { box-sizing: border-box; height: 38px; padding: 0 12px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--ui-border); }.ui-et td + td, .ui-et th + th { border-left: 1px solid var(--ui-border); }.ui-et tbody tr:last-child td { border-bottom: 0; }.ui-et th { background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }.ui-et-req { margin-left: 2px; color: var(--ui-danger); }.ui-et .ui-et-right { text-align: right; font-variant-numeric: tabular-nums; }.ui-et .ui-et-center { text-align: center; }.ui-et-sort { display: inline-flex; align-items: center; gap: 4px; margin: -4px -6px; padding: 4px 6px; border: 0; border-radius: 4px; background: none; color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }.ui-et-sort:hover { color: var(--ui-fg); }.ui-et-sort:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-et-right .ui-et-sort { flex-direction: row-reverse; }.ui-et-arrow { width: 12px; height: 12px; opacity: .35; }[aria-sort] .ui-et-sort { color: var(--ui-fg); }[aria-sort] .ui-et-arrow { opacity: 1; color: var(--ui-accent); }.ui-et td { position: relative; cursor: cell; outline: none; }.ui-et td[aria-readonly="true"] { color: var(--ui-muted); cursor: default; }.ui-et td:focus { box-shadow: inset 0 0 0 2px var(--ui-accent); }.ui-et-dirty::after { content: ""; position: absolute; top: 0; right: 0; border: 5px solid transparent; border-top-color: var(--ui-warning); border-right-color: var(--ui-warning); }.ui-et-new td:first-child { box-shadow: inset 3px 0 0 var(--ui-success); }.ui-et-new td:first-child:focus { box-shadow: inset 3px 0 0 var(--ui-success), inset 0 0 0 2px var(--ui-accent); }.ui-et-empty-val { color: var(--ui-muted); }.ui-et td.ui-et-editing { padding: 0; box-shadow: inset 0 0 0 2px var(--ui-accent); background: var(--ui-surface); }.ui-et td.ui-et-editing.ui-et-bad { box-shadow: inset 0 0 0 2px var(--ui-danger); }.ui-et-input { box-sizing: border-box; width: 100%; min-width: 80px; height: 36px; margin: 0; padding: 0 11px; border: 0; outline: none; background: transparent; color: var(--ui-fg); font: inherit; }.ui-et-right .ui-et-input { text-align: right; }.ui-et-err { position: absolute; z-index: 3; top: 100%; left: -1px; margin-top: 2px; padding: 4px 8px; border-radius: 4px; background: var(--ui-danger); color: #fff; font-size: 12px; font-weight: 500; white-space: nowrap; box-shadow: var(--ui-shadow); }.ui-et-err-up { top: auto; bottom: 100%; margin: 0 0 2px; }.ui-et-check { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; vertical-align: middle; border: 1.5px solid var(--ui-strong); border-radius: 4px; background: var(--ui-surface); color: var(--ui-accent-fg); }.ui-et-check[aria-checked="true"] { border-color: var(--ui-accent); background: var(--ui-accent); }.ui-et-check svg { width: 10px; height: 10px; }.ui-et-act { width: 1%; text-align: center !important; }.ui-et-del { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--ui-muted); cursor: pointer; }.ui-et-del:hover { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-et-del svg { width: 12px; height: 12px; }.ui-et-none td { height: 64px; color: var(--ui-muted); text-align: center; cursor: default; }.ui-et-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 14px; color: var(--ui-muted); font-size: 13px; }.ui-et-add { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border: 1px dashed var(--ui-strong); border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-accent); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-et-add:hover { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-et-add:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-et-add svg { width: 12px; height: 12px; }`// A select column's options as maps of { label, value }.fn editable_table_options(c: map) -> list = (c.options ?? []) | map((o) => type_of(o) == "map" ? { label: o.label ?? str(o.value), value: o.value ?? o.label } : { label: str(o), value: o })// How a cell shows its value (checkboxes aside).fn editable_table_show(c: map, v) -> str { if v == null || v == "" { return "" } if c.type == "select" { return (find(editable_table_options(c), (o) => o.value == v) ?? { label: str(v) }).label } str(v)}// The text a cell's editor starts with.fn editable_table_text(c: map, v) -> str = v == null ? "" : str(v)// What a typed text means in a column: { ok: true, value } or { ok: false, error }.fn editable_table_check(c: map, text: str) -> map { let t = trim(text) let kind = c.type ?? "text" if t == "" { return c.required == true ? { ok: false, error: "Required" } : { ok: true, value: kind == "text" ? "" : null } } if kind == "number" { let n = re.test(t, "^-?\\d+$") ? int(t) : (re.test(t, "^-?(\\d+\\.?\\d*|\\.\\d+)$") ? float(t) : null) if n == null || is_err(n) { return { ok: false, error: "Not a number" } } if c.min != null && n < c.min { return { ok: false, error: `At least ${c.min}` } } if c.max != null && n > c.max { return { ok: false, error: `At most ${c.max}` } } return { ok: true, value: n } } if kind == "date" { if !re.test(t, "^\\d{4}-\\d{2}-\\d{2}$") { return { ok: false, error: "A date: YYYY-MM-DD" } } if c.min != null && t < c.min { return { ok: false, error: `On or after ${c.min}` } } if c.max != null && t > c.max { return { ok: false, error: `On or before ${c.max}` } } return { ok: true, value: t } } if kind == "select" { let o = find(editable_table_options(c), (o) => str(o.value) == t) return o == null ? { ok: false, error: "Not one of the options" } : { ok: true, value: o.value } } { ok: true, value: text }}// A new row: new_row, or each column's empty value.fn editable_table_blank(columns: list, new_row: map?) -> map = new_row ?? reduce(columns, {}, (m, c) => set(m, c.key, c.type == "checkbox" ? false : (c.type ?? "text") == "text" ? "" : null))fn editable_table_right(c: map) -> bool = c.type == "number" || c.align == "right"component EditableTable(id: str, columns: list = [], rows: list = [], label: str? = null, name: str? = null, can_add: bool = false, can_delete: bool = false, new_row: map? = null, add_label: str = "Add row", empty: str = "No rows yet", on_change = null, on_cell = null, class: str? = null) { style global editable_table_css state data = for [i, r] in enumerate(rows) => { id: i, values: r, orig: r } state order = range(0, len(rows)) state next_id = len(rows) state sort_key = null state sort_desc = false state ar = 0 // the active cell: its row's place in `order`, and its column state ac = 0 state editing = false state draft = "" state typed = false // the edit began with a key typed, not Enter state said = "" let ncols = len(columns) + (can_delete ? 1 : 0) let by_id = reduce(data, {}, (m, d) => set(m, str(d.id), d)) let shown = order | map((i) => by_id[str(i)]) | filter((d) => d != null) let n = len(shown) let r_at = n == 0 ? 0 : min([ar, n - 1]) let c_at = min([ac, ncols - 1]) let col = columns[c_at] let check = editing && col != null ? editable_table_check(col, draft) : { ok: true } let out = data | map((d) => d.values) let changed = sum(data | map((d) => d.orig == null ? 0 : count(columns, (c) => d.values[c.key] != d.orig[c.key]))) let added = count(data, (d) => d.orig == null) let dirty = (d: map, c: map) => d.orig == null || d.values[c.key] != d.orig[c.key] let editable = (c: map?) => c != null && c.readonly != true let cell_sel = (r: int, c: int) => `[data-cell="${r}-${c}"]` let focus_cell = (root, r: int, c: int) => { js("queueMicrotask", () => { let el = root.querySelector(cell_sel(r, c)) el && el.focus() }) } // root: the table's .ui-et, found by a handler before it changes anything, since // what the event came from can leave the page once the table is drawn again. let go = (root, r: int, c: int) => { ar = max([0, min([r, n - 1])]) ac = max([0, min([c, ncols - 1])]) focus_cell(root, ar, ac) } // Sets one value of the row with this id, and tells the callbacks. let put = (rid: int, key: str, v) => { let place = index_of(data | map((d) => d.id), rid) let d = data[place] if d.values[key] != v { data = for x in data => x.id == rid ? { id: x.id, values: set(x.values, key, v), orig: x.orig } : x on_cell && on_cell(place, key, v) on_change && on_change(data | map((x) => x.values)) } } let start = (c: map, d: map, text: str?) => { if editable(c) && c.type != "checkbox" { editing = true typed = text != null draft = text ?? editable_table_text(c, d.values[c.key]) } } // Keeps what is being edited if it is good; says what is wrong if not. let keep = () => { let res = editing ? editable_table_check(col, draft) : { ok: true } if !res.ok { said = `${col.label ?? col.key}: ${res.error}` } else if editing { editing = false let d = shown[r_at] if d != null { put(d.id, col.key, res.value) } } res.ok } let flip = (d: map, c: map) => { if editable(c) { put(d.id, c.key, d.values[c.key] != true) } } let clear = (d: map, c: map) => { if editable(c) { let res = editable_table_check(c, "") if res.ok { put(d.id, c.key, c.type == "checkbox" ? false : res.value) } else { said = `${c.label ?? c.key}: ${res.error}` } } } let remove = (root, d: map) => { let at = index_of(order, d.id) ?? 0 let left = n - 1 // taken before `n` follows the rows editing = false data = data | filter((x) => x.id != d.id) order = order | filter((i) => i != d.id) said = "Row deleted" on_change && on_change(data | map((x) => x.values)) if left > 0 { ar = min([at, left - 1]) focus_cell(root, ar, ac) } else { js("queueMicrotask", () => { let b = root.querySelector(".ui-et-add") b && b.focus() }) } } let add = (e) => { let root = e.target.closest(".ui-et") if keep() { // `n` follows the rows, so the new row's place is taken before adding it. let at = n let nid = next_id let first = index_of(columns | map((c) => editable(c) && c.type != "checkbox"), true) ?? 0 next_id += 1 data = push(data, { id: nid, values: editable_table_blank(columns, new_row), orig: null }) order = push(order, nid) ar = at ac = first said = "Row added" on_change && on_change(data | map((x) => x.values)) focus_cell(root, at, first) } } let sort_on = (c: map) => { if keep() { let desc = sort_key == c.key ? !sort_desc : false sort_key = c.key sort_desc = desc let blank = shown | filter((d) => d.values[c.key] == null || d.values[c.key] == "") let full = shown | filter((d) => d.values[c.key] != null && d.values[c.key] != "") | sort_by((d) => { let v = d.values[c.key] type_of(v) == "str" ? lower(v) : v }) // Empty cells go last, either way. let ids = ((desc ? reverse(full) : full) + blank) | map((d) => d.id) order = ids } } <div class={ui_cls(["ui-et", class])}> <div.ui-et-scroll> <table role="grid" aria-label={label} aria-rowcount={n + 1} aria-colcount={ncols} @keydown={(e) => { let k = e.key let root = e.target.closest(".ui-et") let d = shown[r_at] let tag = e.target.tagName if editing { if k == "Escape" { e.preventDefault() editing = false said = "Edit cancelled" focus_cell(root, r_at, c_at) } else if k == "Enter" && tag != "SELECT" { e.preventDefault() if keep() { go(root, r_at + 1, c_at) } } else if k == "Tab" { e.preventDefault() if keep() { let flat = r_at * ncols + c_at + (e.shiftKey ? -1 : 1) let to = max([0, min([flat, n * ncols - 1])]) go(root, floor(to / ncols), to % ncols) } } } else if tag == "TD" && d != null { // From the cell the key came to, not the one last drawn: a second key // can come before the table is drawn again. let place = split(e.target.dataset.cell ?? "", "-") | map((x) => int(x)) let r_on = len(place) == 2 && place[0] < n ? place[0] : r_at let c_on = len(place) == 2 && place[1] < ncols ? place[1] : c_at ar = r_on ac = c_on d = shown[r_on] let c = columns[c_on] let ctrl = e.ctrlKey || e.metaKey let to = match k { "ArrowDown" => [r_on + 1, c_on] "ArrowUp" => [r_on - 1, c_on] "ArrowRight" => [r_on, c_on + 1] "ArrowLeft" => [r_on, c_on - 1] "Home" => ctrl ? [0, 0] : [r_on, 0] "End" => ctrl ? [n - 1, ncols - 1] : [r_on, ncols - 1] "PageDown" => [n - 1, c_on] "PageUp" => [0, c_on] _ => null } if to != null { e.preventDefault() go(root, to[0], to[1]) } else if c == null { // The delete column. if k == "Enter" || k == " " || k == "Delete" { e.preventDefault() remove(root, d) } } else if c.type == "checkbox" && (k == " " || k == "Enter") { e.preventDefault() flip(d, c) } else if k == "Enter" || k == "F2" { e.preventDefault() start(c, d, null) } else if k == "Delete" || k == "Backspace" { e.preventDefault() clear(d, c) } else if len(k) == 1 && k != " " && !ctrl && !e.altKey && ((c.type ?? "text") == "text" || c.type == "number") { e.preventDefault() start(c, d, k) } } }}> <colgroup> {for c in columns => <col style={c.width != null ? `width: ${c.width}` : null}/>} {can_delete && <col style="width: 44px"/>} </colgroup> <thead> <tr aria-rowindex="1"> {for [ci, c] in enumerate(columns) => <th scope="col" aria-colindex={ci + 1} class={editable_table_right(c) ? "ui-et-right" : (c.type == "checkbox" ? "ui-et-center" : null)} aria-sort={sort_key == c.key ? (sort_desc ? "descending" : "ascending") : null}> {c.sortable == true ? <button.ui-et-sort type="button" @click={() => { sort_on(c) }}> {c.label ?? c.key}{c.required == true && <span.ui-et-req aria-hidden="true">*</span>} <svg.ui-et-arrow viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d={sort_key != c.key ? "M3.5 4.5L6 2l2.5 2.5M3.5 7.5L6 10l2.5-2.5" : (sort_desc ? "M6 2v8M3 7l3 3 3-3" : "M6 10V2M3 5l3-3 3 3")}/> </svg> </button> : [c.label ?? c.key, c.required == true && <span.ui-et-req aria-hidden="true">*</span>]} {c.required == true && <span.ui-sr> (required)</span>} </th>} {can_delete && <th scope="col" aria-colindex={ncols}><span.ui-sr>Delete</span></th>} </tr> </thead> <tbody> {n == 0 && <tr.ui-et-none><td colspan={str(ncols)}>{empty}</td></tr>} {for [ri, d] in enumerate(shown) => <tr key={d.id} aria-rowindex={ri + 2} class={d.orig == null ? "ui-et-new" : null}> {for [ci, c] in enumerate(columns) => { let here = ri == r_at && ci == c_at let v = d.values[c.key] let ed = here && editing <td role="gridcell" data-cell={`${ri}-${ci}`} aria-colindex={ci + 1} tabindex={here && !editing ? "0" : "-1"} aria-readonly={c.readonly == true ? "true" : null} class={ui_cls([editable_table_right(c) && "ui-et-right", c.type == "checkbox" && "ui-et-center", dirty(d, c) && d.orig != null && "ui-et-dirty", ed && "ui-et-editing", ed && !check.ok && "ui-et-bad"])} @focus={(e) => { if !editing && e.target.tagName == "TD" { ar = ri ac = ci } }} @click={(e) => { if !ed { let root = e.target.closest(".ui-et") if keep() { ar = ri ac = ci if c.type == "checkbox" { flip(d, c) } else { start(c, d, null) } if c.type == "checkbox" || !editable(c) { focus_cell(root, ri, ci) } } else { focus_cell(root, r_at, c_at) } } }}> {ed ? (c.type == "select" ? <select.ui-et-input aria-label={c.label ?? c.key} @mount={(el) => { el.focus() }} @change={(e) => { let root = e.target.closest(".ui-et") draft = e.value if keep() { focus_cell(root, ri, ci) } }} @blur={() => { if editing { if !keep() { editing = false } } }}> {c.required != true && <option value="" selected={draft == ""}>—</option>} {for o in editable_table_options(c) => <option value={str(o.value)} selected={str(o.value) == draft}>{o.label}</option>} </select> : <input.ui-et-input type={c.type == "date" ? "date" : "text"} value={draft} inputmode={c.type == "number" ? "decimal" : null} min={c.type == "date" ? c.min : null} max={c.type == "date" ? c.max : null} aria-label={c.label ?? c.key} aria-invalid={check.ok ? null : "true"} aria-describedby={check.ok ? null : `${id}-err`} required={c.required == true} @mount={(el) => { el.focus() if c.type != "date" { if typed { el.setSelectionRange(len(draft), len(draft)) } else { el.select() } } }} @input={(e) => { draft = e.value }} @blur={() => { // Leaving the cell keeps a good value and drops a bad one. if editing { if !keep() { editing = false } } }}/>) : (c.type == "checkbox" ? <span.ui-et-check role="checkbox" aria-checked={v == true ? "true" : "false"} aria-label={c.label ?? c.key}> {v == true && <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 5.2l2 2 4-4.4"/></svg>} </span> : (editable_table_show(c, v) == "" ? <span.ui-et-empty-val aria-label="empty">—</span> : editable_table_show(c, v)))} {dirty(d, c) && d.orig != null && <span.ui-sr>, changed</span>} {ed && !check.ok && <span id={`${id}-err`} class={ui_cls(["ui-et-err", ri == n - 1 && n > 1 && "ui-et-err-up"])}>{check.error}</span>} </td> }} {can_delete && <td role="gridcell" data-cell={`${ri}-${ncols - 1}`} aria-colindex={ncols} class="ui-et-act" @focus={() => { if !editing { ar = ri ac = ncols - 1 } }} tabindex={ri == r_at && c_at == ncols - 1 && !editing ? "0" : "-1"}> <button.ui-et-del type="button" tabindex="-1" aria-label={`Delete row ${ri + 1}`} @click={(e) => { remove(e.target.closest(".ui-et"), d) }}> <svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M2 2l6 6M8 2L2 8"/></svg> </button> </td>} </tr>} </tbody> </table> </div> <div.ui-et-foot> {can_add ? <button.ui-et-add type="button" @click={add}> <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M6 1.5v9M1.5 6h9"/></svg> {add_label} </button> : <span></span>} <span.ui-et-status> {n} row{n == 1 ? "" : "s"}{changed > 0 ? ` · ${changed} changed` : ""}{added > 0 ? ` · ${added} new` : ""} </span> </div> <span.ui-sr aria-live="polite">{said}</span> {name != null && <input type="hidden" name={name} value={json.stringify(out)}/>} </div>}Related
SelectableTableRows with checkboxes, a select-all that shows when only some are checked, a count of the selected, and hidden inputs for a form.7 views · 6 downloadsAuditLogWho did what to what and when, newest first, filtered by person and action, searched, and paged.5 views · 7 downloadsComparisonTableFeatures down the side and options across, with ticks, crosses and values in the cells, and one option picked out.5 views · 6 downloadsCsvTableA table made from CSV text, with quoted fields, a header row, numbers lined up on the right and a row limit.6 views · 7 downloadsDataGridSort, search, filter by a column's values, pick rows and page through them, in one table.5 views · 8 downloadsDataTableRows of data sorted by their headers, with search, pages, number, money, percent and date columns.5 views · 9 downloads
Thread
Thread 0 posts
On EditableTable: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.