Data grid
<ema-grid> is the workhorse of the system: client or server data,
pagination, search, per-column filters, grouping with aggregates, drill-down details,
inline editing, full keyboard navigation, column resize / show-hide, icon and badge cells,
and proper two-axis scrolling with a sticky header.
Client-side grid (all features)
200 rows generated in the browser. Try everything:
- Search in the toolbar · Filters button toggles the per-column filter row — click the small operator button in any filter cell to pick the comparison (contains / equals / greater / between / empty …) based on the column's data type
- Group button groups by Region or Status (collapsible groups, sum/avg aggregates)
- Columns button shows/hides columns · drag the edge of a header to resize
- Click the chevron for drill-down details · checkboxes for selection
- Click a cell, then navigate with ↑↓←→, press Enter on Customer / Status / Amount to edit inline (green-marked cells are dirty), Esc cancels, Tab commits and moves on, PageUp/PageDown switch pages, Space selects
Server-side grid
Set .source to an async function instead of .data. The grid sends a query object
(page, page size, sort, filters, search, groupBy) on every change, shows a loading overlay,
ignores stale responses, and renders whatever the server returns. This demo simulates an API with 350ms latency over 5,000 records.
Column definition
| Property | Type | Description |
key | string | Row field to display (required) |
label | string | Header text |
width / minWidth | number | Initial / minimum pixel width (drag header edge to resize) |
sortable | boolean | Default true. Click header: asc → desc → off |
filter | 'text' | 'select' | 'number' | 'date' | Adds a control to the filter row with a type-specific operator menu (see table below) |
options | array | Choices for select filters / editors (defaults to distinct values) |
type | 'text' | 'number' | 'date' | 'badge' | 'bool' | Built-in formatting |
badgeMap | object | {value: variant} for type:'badge' |
cellIcon | string | fn(row) | Icon rendered before the value |
icon | string | Icon in the header |
render | fn(value, row) → html | Full custom cell renderer (overrides type) |
editable | boolean | {type, options} | Enables inline editing (text / number / select editor) |
groupable | boolean | Appears in the toolbar Group menu |
aggregate | 'sum' | 'avg' | Shown in group header rows |
align | 'left' | 'center' | 'right' | Cell alignment |
hidden / resizable | boolean | Initial visibility / allow resizing (default true) |
Advanced column filters
Every filter cell has an operator button (the small symbol on the left). Click it to choose
the comparison; the available operators depend on the column's filter type.
"Between" shows a second range input; "Is empty / not empty" applies immediately without a value.
| Filter type | Operators |
text | Contains ~ · Does not contain !~ · Equals = · Not equals ≠ · Starts with ^ · Ends with $ · Is empty ∅ · Is not empty !∅ |
number | Equals = · Not equals ≠ · Greater > · Greater or equal ≥ · Less < · Less or equal ≤ · Between ↔ (range) · Is empty / not empty |
date | On date = · Before < · After > · Between ↔ (range, inclusive) · Is empty / not empty — uses native date pickers |
select | Is = · Is not ≠ — options from col.options or distinct values |
Filter state is structured — each active filter is
{ op, value, value2, type }. In server mode the same objects arrive in
query.filters, so your backend can map operators to SQL/queries. For mock backends,
reuse the grid's own matcher:
grid.source = async ({ filters, ...q }) => {
let rows = DB;
for (const [key, f] of Object.entries(filters)) {
rows = rows.filter((r) => EMA.gridFilterMatch(f, r[key])); // honors op/value/value2/type
}
// …sort, paginate, return {rows, total}
};
Grid attributes & properties
| Attribute | Description |
grid-title | Toolbar title |
height | Max body height, enables vertical scrolling with sticky header (e.g. 440px, 60vh) |
page-size / page-sizes | Initial size / selector options (default 10,25,50,100) |
selectable | Checkbox selection column |
striped | Zebra rows |
exportable | CSV export button in the toolbar |
no-search | Hide the toolbar search box |
key | Row identity field (default id) — used for selection, expansion and dirty tracking |
| Property / method | Description |
.columns | Column definitions (see above) |
.data | Row array — client mode |
.source | async (query) ⇒ {rows, total} — server mode. Query: {page, pageSize, sort, filters, search, groupBy} |
.detail | (row) ⇒ html drill-down renderer; adds the expand column |
.actions | [{name, icon, label, danger}] action icon buttons per row → action event |
.groupBy / .page / .pageSize | Read or set grid state |
.refresh() | Re-query (server) or re-render (client) |
.getSelectedRows() / .clearSelection() | Selection helpers |
.exportCsv(filename?) | Download visible columns as CSV |
Events
| Event | event.detail |
cell-edit | {row, key, oldValue, value} — fired after an inline edit commits |
row-click / row-activate | {row} — click / Enter on a non-editable cell |
row-expand | {row, expanded} |
selection-change | {rows} |
action | {action, row} |
filter-change | {filters} — map of key → {op, value, value2, type} |
page-change / sort-change / search-change / group-change / columns-change / column-resize | State change notifications |
load-error | {error} — server source threw |
Keyboard reference
| Key | Action |
| ↑ ↓ ← → | Move cell focus |
| Enter / F2 | Edit focused cell (if editable), otherwise row-activate |
| Enter (while editing) | Commit and move down |
| Tab / Shift+Tab (while editing) | Commit and edit next / previous cell |
| Esc | Cancel edit |
| Home / End | First / last column (Ctrl+ → first / last row) |
| PageUp / PageDown | Previous / next page |
| Space | Toggle row selection |