data_grid is a controlled, virtualized grid for interactive tabular data.
data_grid supports two modes:
rowsmode: app passesrowsdirectly (existing controlled model).data_sourcemode: grid fetches rows viaDataGridDataSource.
rows mode is still fully supported and unchanged.
Data-source mode centralizes server fetch via fetch_data(req GridDataRequest).
Core pieces:
DataGridDataSourceGridDataRequestGridDataResultGridMutationRequestGridMutationResultGridDataCapabilitiesGridAbortController/GridAbortSignal
grid_id: source-aware request routingquery: typed sort/filter/quick filter (GridQueryState)page:GridCursorPageReqorGridOffsetPageReqsignal: cancellation tokenrequest_id: monotonic id for stale-response guards
rows: returned page rowsnext_cursor/prev_cursor: cursor navigation tokensrow_count:?int; usenonewhen total unknown/infinitehas_more: paging continuation hintreceived_count: returned row count for this page
capabilities() returns explicit support flags:
supports_cursor_paginationsupports_offset_paginationsupports_numbered_pagesrow_count_knownsupports_createsupports_updatesupports_deletesupports_batch_delete
Grid uses this metadata to select pagination behavior and display counts safely.
Grid CRUD is explicit-save.
- Cell edits stage locally.
Savesends staged changes to source viamutate_data(...).Cancelrestores last committed rows.- Source-mode save applies optimistic UI, then background refetch.
Mutation request fields:
kind(.create,.update,.delete)rows(create/update payload)row_ids(delete payload)edits(cell-level updates)query,signal,request_id
Mutation result fields:
createdcanonical created rows (final IDs)updatedcanonical updated rowsdeleted_idsrow_countoptional total after mutation
Use cursor pagination by default. It is stable for infinite scroll and changing data.
Offset pagination is supported for static snapshots and numbered-page workflows.
Set mode in DataGridCfg:
pagination_kind: .cursor(default)pagination_kind: .offset
Set page size with page_limit.
Grid starts async fetches and cancels stale requests automatically.
Internal behavior:
- each new fetch aborts the previous active request
- each response checks
request_idbefore apply - stale or aborted responses are ignored
This prevents scroll/filter race corruption.
New fields:
data_source DataGridDataSourcepagination_kind GridPaginationKindcursor stringpage_limit introw_count ?intloading boolload_error stringshow_crud_toolbar boolallow_create boolallow_delete boolon_rows_change fn ([]GridRow, mut Event, mut Window)on_crud_error fn (string, mut Event, mut Window)
When data_source is set, fetched rows are used for render. Local rows paging is disabled.
import gui
@[heap]
struct App {
pub mut:
source ?gui.DataGridDataSource
query gui.GridQueryState
selection gui.GridSelection
}
fn view(mut w gui.Window) gui.View {
mut app := w.state[App]()
return w.data_grid(
id: 'users-grid'
columns: columns()
data_source: app.source
pagination_kind: .cursor
page_limit: 200
query: app.query
selection: app.selection
show_filter_row: true
show_quick_filter: true
show_crud_toolbar: true
on_query_change: fn (q gui.GridQueryState, mut _ gui.Event, mut w gui.Window) {
w.state[App]().query = q
}
on_selection_change: fn (s gui.GridSelection, mut _ gui.Event, mut w gui.Window) {
w.state[App]().selection = s
}
on_crud_error: fn (msg string, mut _ gui.Event, mut _ gui.Window) {
eprintln(msg)
}
)
}Built-in concrete sources:
InMemoryDataSourceGridOrmDataSource
See examples/data_grid_data_source_demo.v for a 50k-row demo.
GridOrmDataSource adapts database-backed fetches and mutations to
DataGridDataSource.
Core types:
GridOrmColumnSpec: whitelist mapping from grid column ids to DB fieldsGridOrmQuerySpec: normalized query + paging (limit,offset,cursor)GridOrmPage: rows + paging metadataGridOrmFetchFn: callback for DB executionGridOrmCreateFn/GridOrmUpdateFn/GridOrmDeleteFn/GridOrmDeleteManyFn
Initial query subset:
- quick filter
- filter ops:
contains,equals,starts_with,ends_with - sort: asc/desc
Unsupported columns/ops are dropped by grid_orm_validate_query(...).
examples/data_grid_orm_demo.v shows:
- V ORM table creation (
create table) - V ORM inserts (
insert ... into ...) - relationship modeling (
TeamRow->[]MemberRow) - SQL pushdown for quick filter, column filters, sort, and pagination
Minimal setup:
source := &gui.GridOrmDataSource{
columns: [
gui.GridOrmColumnSpec{
id: 'name'
db_field: 'm.name'
},
gui.GridOrmColumnSpec{
id: 'team'
db_field: 't.name'
},
]
fetch_fn: fn (spec gui.GridOrmQuerySpec, signal &gui.GridAbortSignal) !gui.GridOrmPage {
return fetch_from_db(spec, signal)
}
create_fn: fn (rows []gui.GridRow, signal &gui.GridAbortSignal) ![]gui.GridRow {
return create_rows(rows, signal)
}
update_fn: fn (rows []gui.GridRow, edits []gui.GridCellEdit, signal &gui.GridAbortSignal) ![]gui.GridRow {
return update_rows(rows, edits, signal)
}
delete_many_fn: fn (row_ids []string, signal &gui.GridAbortSignal) ![]string {
return delete_rows(row_ids, signal)
}
}Use it in data_grid exactly like other sources:
window.data_grid(
id: 'orm-grid'
columns: columns
data_source: source
query: app.query
selection: app.selection
)window.data_grid(
id: 'users-grid'
columns: columns
rows: rows
query: app.query
selection: app.selection
on_query_change: on_query_change
on_selection_change: on_selection_change
)Use runtime counters for diagnostics:
stats := window.data_grid_source_stats('users-grid')Fields include loading/error/request counts and stale/cancel counters.
These helpers are unchanged:
grid_data_from_csvgrid_rows_to_tsvgrid_rows_to_csvgrid_rows_to_xlsxgrid_rows_to_pdf
tableremains unchanged.- Keep
GridRow.idstable and unique. - Grouping is contiguous; pre-sort by group keys for stable large groups.
- Row editing uses staged save when
show_crud_toolbaris enabled. - Tab order follows numeric
id_focus, not tree order.