-
Notifications
You must be signed in to change notification settings - Fork 0
LayrzTable
A data table component with dynamic columns, sorting, search, multiselect, and row actions. Supports custom cell rendering and programmatic control via a controller.
Metadata
Mirrors: ThemedTable2<T> (layrz_theme)
Phase: M6 (Data display)
Domain: Data
Primitive: Depends on architecture decision (see below)
Status: Specification status DERIVED from layrz_theme, not yet confirmed by the team.
LayrzTable<T> is a generic data table widget for displaying and interacting with collections of domain objects. It supports:
- Dynamic column definitions with custom rendering, sorting, and alignment.
- Full-text search across all columns with debounced filtering.
- Multi-select mode with checkboxes and batch actions.
- Row actions — custom buttons per row for drill-down, edit, or delete workflows.
- Sorting — click column headers to sort ascending/descending; custom sort comparators per column.
- Responsive layout — horizontal scroll on narrow viewports, actions drawer on mobile.
-
Programmatic control —
LayrzTableController<T>for external sorting, searching, and refresh.
-
Generic over domain type —
LayrzTable<T>whereTis your domain object (User, Device, Order, etc.). - Column-centric definition — Columns are declared once; rendering, sorting, and tap behavior flow from column metadata.
- Search-as-you-type — Debounced full-text search across all cell values; no explicit "go" button.
-
Multiselect as opt-in — Enable or disable via constructor parameter; state lives in a
ValueNotifier. - Controller-driven events — Programmatic sorting, searching, and refresh via discrete event types.
-
Responsive surface — Rows built on
LayrzCard(link: LayrzCard) for consistent elevation and hover treatment.
// Design sketch — illustrative only
class LayrzTable<T> extends StatefulWidget {
/// List of domain objects to display.
final List<T> items;
/// Column definitions (headers, rendering, sorting, alignment).
final List<LayrzColumn<T>> columns;
/// Optional builder for action buttons per row.
/// If provided, [actionsCount] must be > 0.
final List<LayrzActionButton> Function(T item)? actionsBuilder;
/// Maximum number of actions per row.
/// If 0, actions column is omitted.
final int actionsCount;
/// Column label for the actions column.
final String actionsLabelText;
/// Breakpoint (logical pixels) at which actions switch to mobile layout.
final double actionsMobileBreakpoint;
/// Height of the table header row.
final double headerHeight;
/// Enable or disable the multiselect checkbox column.
final bool hasMultiselect;
/// Label text shown while the table is loading or computing.
final String loadingLabelText;
/// Enable or disable the search field above the table.
final bool canSearch;
/// Minimum width for flexible columns.
final double minColumnWidth;
/// Multiselect state notifier (List<T> of selected items).
/// Only used if [hasMultiselect] is true.
final ValueNotifier<List<T>>? multiselectValue;
/// Action buttons shown when 1+ items are selected.
/// Automatically appended with a "clear selection" button.
final List<LayrzActionButton> multiselectActions;
/// Programmatic control: sorting, searching, refresh.
final LayrzTableController<T>? controller;
/// Callback when filtered row count changes (after search/sort).
final void Function(int count)? onFilteredCountChanged;
/// Default behavior when a cell is tapped with no explicit tap handler.
/// Enum: none, copyToClipboard.
final LayrzTableOnTapBehavior onTapDefaultBehavior;
/// Copy-to-clipboard confirmation text.
/// Defaults to locale or fallback text.
final String? copyToClipboardText;
/// Delay before populating the table with initial data.
final Duration populateDelay;
// Constructor omitted for brevity
}// Design sketch — illustrative only
class LayrzColumn<T> {
/// Header text displayed in the column header.
final String headerText;
/// Function to extract a string value from the item for the cell.
/// Used for sorting and search.
final String Function(T item) valueBuilder;
/// Optional rich text builder for cell rendering.
/// Returns a list of InlineSpan for styled rendering (e.g., bold, colors, icons).
/// If not supplied, [valueBuilder] is rendered as plain text.
final List<InlineSpan> Function(T item)? richTextBuilder;
/// Cell alignment (left, center, right, etc.).
final Alignment alignment;
/// Enable or disable sorting for this column.
final bool isSortable;
/// Optional fixed width for this column.
/// If null, width is calculated flexibly based on available space.
final double? width;
/// Callback when this cell is tapped.
/// Receives the item and its index.
/// If null, the default behavior ([onTapDefaultBehavior]) is applied.
final void Function(T item, int index)? onTap;
/// Custom sort comparator for this column.
/// Receives two items and an ascending flag; returns <0, 0, or >0.
/// If null, sorting is lexicographic on [valueBuilder] result.
final int Function(T a, T b, bool ascending)? customSort;
// Constructor omitted for brevity
}// Design sketch — illustrative only
class LayrzTableController<T> {
/// Register a listener to receive table events.
void addListener(void Function(LayrzTableEvent event) listener);
/// Remove a registered listener.
void removeListener(void Function(LayrzTableEvent event) listener);
/// Clear all listeners.
void clearListeners();
/// Programmatically trigger a sort event.
/// [columnIndex] is the zero-based position in the columns list.
/// [ascending] determines sort order.
void sort({int columnIndex = 0, bool ascending = true});
/// Notify the table that external sorting has been applied (e.g., by a backend).
void onSort({int columnIndex = 0, bool ascending = true});
/// Programmatically trigger a search.
void search(String searchQuery);
/// Notify the table that external search has been applied.
void onSearch(String searchQuery);
/// Trigger a refresh (e.g., reload data from an external source).
void refresh();
/// Dispose of the controller (clears all listeners).
void dispose();
}// Design sketch — illustrative only
abstract class LayrzTableEvent<T> {}
class LayrzTableSortEvent<T> extends LayrzTableEvent<T> {
final int columnIndex;
final bool ascending;
// Constructor omitted
}
class LayrzTableOnSortEvent<T> extends LayrzTableEvent<T> {
final int columnIndex;
final bool ascending;
// Constructor omitted
}
class LayrzTableSearchEvent<T> extends LayrzTableEvent<T> {
final String search;
// Constructor omitted
}
class LayrzTableOnSearchEvent<T> extends LayrzTableEvent<T> {
final String search;
// Constructor omitted
}
class LayrzTableRefreshEvent<T> extends LayrzTableEvent<T> {}// Design sketch — illustrative only
enum LayrzTableOnTapBehavior {
/// No action when a cell is tapped (unless column has explicit [onTap]).
none,
/// Copy the cell content to the clipboard.
copyToClipboard,
}Columns are declared as a static list of LayrzColumn<T> objects:
final columns = [
LayrzColumn<User>(
headerText: 'Name',
valueBuilder: (user) => user.name,
alignment: Alignment.centerLeft,
isSortable: true,
),
LayrzColumn<User>(
headerText: 'Email',
valueBuilder: (user) => user.email,
richTextBuilder: (user) => [
TextSpan(
text: user.email,
style: TextStyle(color: Colors.blue),
),
],
onTap: (user, index) => print('Tapped: ${user.email}'),
),
LayrzColumn<User>(
headerText: 'Status',
valueBuilder: (user) => user.isActive ? 'Active' : 'Inactive',
customSort: (a, b, ascending) {
// Custom sort: active users first
int result = (a.isActive ? 0 : 1).compareTo(b.isActive ? 0 : 1);
return ascending ? result : -result;
},
),
];If canSearch = true, a search field appears above the table. It triggers LayrzTableSearchEvent as the user types (debounced). The table filters all rows by matching the search query against all cell values.
final controller = LayrzTableController<User>();
// Trigger a sort via controller
controller.sort(columnIndex: 0, ascending: false);
// Listen for sort events
controller.addListener((event) {
if (event is LayrzTableSortEvent) {
// Handle: sort by column index [event.columnIndex], order [event.ascending]
// Usually: reload data from backend or sort in-memory
}
});final selectedUsers = ValueNotifier<List<User>>([]);
LayrzTable<User>(
items: users,
columns: columns,
hasMultiselect: true,
multiselectValue: selectedUsers,
multiselectActions: [
LayrzActionButton(labelText: 'Delete', onTap: () { /* ... */ }),
LayrzActionButton(labelText: 'Export', onTap: () { /* ... */ }),
],
)
// Listen for changes
selectedUsers.addListener(() {
print('Selected ${selectedUsers.value.length} users');
});When items are selected, a toolbar appears with the supplied multiselectActions plus an auto-added "Clear" button.
LayrzTable<User>(
items: users,
columns: columns,
actionsCount: 3,
actionsBuilder: (user) => [
LayrzActionButton(
labelText: 'View',
onTap: () => print('View ${user.name}'),
),
LayrzActionButton(
labelText: 'Edit',
onTap: () => print('Edit ${user.name}'),
),
LayrzActionButton(
labelText: 'Delete',
onTap: () => print('Delete ${user.name}'),
),
],
)Each row displays up to actionsCount action buttons. On mobile (below actionsMobileBreakpoint), actions drawer into a menu.
The LayrzTableController<T> allows external sorting, searching, and refresh:
final controller = LayrzTableController<User>();
LayrzTable<User>(
items: users,
columns: columns,
controller: controller,
)
// Externally trigger search (e.g., from a separate search box)
controller.search('john@example.com');
// Externally trigger sort
controller.sort(columnIndex: 1, ascending: true);
// Notify the table of a refresh
controller.refresh();
// Listen for events
controller.addListener((event) {
if (event is LayrzTableSearchEvent) {
// Backend search triggered; fetch results
} else if (event is LayrzTableSortEvent) {
// Backend sort triggered; fetch results
}
});-
M1 Theme System (
LayrzTheme,LayrzThemeData) — Colors, text styles, and theme tokens for rendering. -
M2 Display Primitive (
LayrzCard) — Surface container for rows; provides elevation and hover state. -
M2 Button Component (
LayrzButton) — Action and multiselect toolbar buttons.
The following dependencies are FACTS recorded from the layrz_theme source audit:
-
sync_scroll_controller1.0.1 — IMPORT-COUPLED ONLY. Importspackage:flutter/material.dartat line 7 but uses ZERO Material symbols. Everything it touches (ScrollController,ScrollPosition,ScrollActivity,ScrollPhysics,ChangeNotifier,Curve) comes from widgets, rendering, foundation, or dart:ui. It is a single 379-line file exposing one public class,SyncScrollControllerGroup. Open concern: SDK constraint is>=2.17.6 <3.0.0(Dart 2 upper bound); whether it resolves under Dart 3.13 needs verifying. Reimplementation is cheap if needed (379 lines, one class). -
two_dimensional_scrollables0.3.9 — CLEAN — Zero Material or Cupertino imports; the onlyColors.references are inside///doc comments. ExposesTableView,TableViewport,TableViewCell,TableSpan,TableCellBuilderDelegate,TableCellListDelegate,TableVicinity,TableRowBorderand span types. Built on the Flutter SDK's ownTwoDimensionalViewportandRenderTwoDimensionalViewport(in the SDK atlib/src/widgets/two_dimensional_viewport.dartandtwo_dimensional_scroll_view.dart). Directly usable by a Material-free design system.
This is an OPEN QUESTION, not yet decided.
The layrz_theme implementation of ThemedTable2 uses only sync_scroll_controller — it does NOT use two_dimensional_scrollables. The older, legacy ThemedTable did use two_dimensional_scrollables.
LayrzTable has a genuine architectural choice:
-
Follow table2's approach — Use
sync_scroll_controllerto coordinate horizontal and vertical scrolling. Pros: proven in production. Cons: carries a Material import in the dependency graph (albeit unused); constrains to Dart 2 bounds. -
Build on
two_dimensional_scrollables— Use the cleanTableViewprimitives and avoid the Material import entirely. Pros: no Material import; fresh surface. Cons: different from the proven approach; may require layout rework. -
Go directly to SDK primitives — Use the SDK's
TwoDimensionalViewportdirectly (whichtwo_dimensional_scrollableswraps). Pros: zero external dependencies beyond Flutter. Cons: lower-level API; more boilerplate.
Key fact: Regardless of choice, LayrzTable's own chrome (cells, headers, actions, multiselect toolbar) must be rebuilt from scratch because ThemedTable2 itself imports package:flutter/material.dart.
The following are clarifications needed from the source or design team:
-
Generics and type parameter preservation — Do
LayrzColumn<T>,LayrzTableController<T>, andLayrzTableEvent<T>carry the type parameter unchanged? Or does event dispatch erase the type? -
Row action declaration and callback signature — The source uses a
CellTap<T>typedef for column onTap handlers. What is the callback signature for row actions? Do they receive the item, the index, or both? -
Multiselect state ownership — Does multiselect state live entirely in
multiselectValue(aValueNotifier<List<T>>), or does the controller manage selections? -
Column sizing and responsive behavior — How are flexible column widths calculated when
widthis null? Is there a priority order, or are they split equally? How do columns respond to viewport resize? -
Loading state — The
loadingLabelTextsuggests the table can display a loading state. Who owns the loading flag? Is there aisLoadingparameter, or is this managed by the controller/parent? -
Search across specific columns — Does search run across all columns, or can certain columns be excluded from search?
-
Isolation safety — The source has an
isolateSafetygetter on columns that stripsonTapandrichTextBuilder. Is this for sending columns to Dart isolates? Should this be exposed in layrz_ui? -
Tap feedback on cells — When
onTapDefaultBehavioriscopyToClipboard, what visual feedback is shown (toast, snackbar, tooltip)?
Critical path item: A design reference (Figma, annotated screenshot, or spec) must be attached to the LayrzTable component before implementation begins. This specification must cover:
- Table header chrome (cell padding, height, text styling, sort indicator appearance).
- Data cell styling, padding, and row height.
- Multiselect checkbox styling and column width.
- Actions column layout and button sizing (desktop vs. mobile).
- Horizontal scroll affordance and behavior.
- Loading state appearance.
- Empty state (no items) display.
- Search input styling and placeholder text.
- Multiselect toolbar styling and button layout.
- Light and dark theme variants for all states.
Last updated: 2026-08-13
Related documents: LayrzCard, Design Tokens, Architecture, Roadmap
Made with ❤️ by Golden M, Inc.
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput