npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@egi/smart-table

v0.4.0

Published

Model-backed React data table with editing, search, selection, validation, and persistence

Readme

SmartTable Guide

SmartTable is a reusable model-backed React table built on PrimeReact's DataTable. It loads rows through the configured environment, formats values through the package's local SmartTools, and provides shared search, editing, selection, validation, save, delete, refresh, and print surfaces.

Read this guide before adding a table or changing SmartTable behavior.

Basic Structure

Define columns and the table definition in the owning class component:

private buildColumns(): ColumnDefinition[] {
    return [
        {field: "code", header: this.props.t("resources.code"), type: "text", readonly: true, sortable: true, width: 185},
        {field: "active", header: this.props.t("common.active"), type: "checkbox", readonly: true, width: 73}
    ];
}

private buildTableDef(): TableDefinition {
    return {
        operations: "read-only",
        reloadButton: true,
        search: true,
        source: {model: ResourceModel, params: {orderBy: ["code"]}},
        title: this.props.t("resources.title")
    };
}

Render it inside a container with a defined or flex-derived height:

<SmartTable
    ref={this.tableRef}
    columns={this.buildColumns()}
    tableDef={this.buildTableDef()}
/>

The source may use a generated model class or plain row objects. SmartTable obtains the row key from source.primaryKey, then model metadata, falling back to pk. Keep that primary key stable for the table's lifetime.

Table Capabilities

operations is required and declares the mutation UI SmartTable may expose: "read-only", "all", one of "create", "update", or "delete", or an array containing the required mutable operations. The special values are never combined in an array. This is a component capability contract, not an access check; the consumer derives it from its own policy.

The remaining table options use positive names. Omit a feature to keep it off:

  • search, rowCounter, rowCheckbox, and text-selection options expose standard table facilities.
  • saveButton, cancelButton, printButton, and reloadButton expose standard controls. Strings customize the visible Save/Cancel label or the icon-only Print/Reload tooltip.
  • onCreate, onSave, onPrint, onReload, onRowChange, onUpdateRows, and onSelectionChange supply behavior independently of control visibility.
  • topBarButtons and bottomBarButtons add application-specific actions.
  • scrollMode selects "none", "standard", or "virtual" scrolling.
  • source.window opts a read-only virtual table into bounded server-backed ranges; omitting it preserves eager loading.
  • columnResizeMode selects "none", "fit", or "expand"; columnToggleButton exposes the visibility chooser.
  • viewState persists field-keyed widths, hidden columns, and sorting in browser storage or through an application-owned async adapter.

Public refs use the SmartTableApi interface. Its save(), print(), and reload() methods execute the same configured action as the corresponding standard button. A custom onSave, onPrint, or onReload replaces the built-in action and must not call the same public method recursively.

TableDefinition attribute reference

| Attribute | Required | Default | Description | |-----------|----------|---------|-------------| | afterLoad | No | — | Runs after loaded and transformed rows have entered table state; receives the rows and SmartTableApi. | | afterSave | No | — | Runs after the built-in table-owned save succeeds. | | beforeSave | No | — | Async save guard receiving changed rows, deleted rows, and the API; return false to cancel saving. | | bottomBarButtons | No | [] | Custom actions rendered in their supplied order from right to left in the bottom toolbar. Their presence creates that toolbar. | | cancelButton | No | false | Shows built-in Cancel when truthy. true uses the standard label; a string supplies the label. | | columnResizeMode | No | "none" | Controls column resizing. "fit" transfers width to the next visible column; "expand" grows the scrollable table. | | columnToggleButton | No | false | Shows the icon-only visibility chooser. true uses the localized label; a string supplies it. | | onCreate | No | — | Handles the Create button, for example by opening a modal dialog. Requires the create operation. | | dateConfigYear | No | Current year | Reserved for abbreviated date expansion, which is currently disabled in SmartTable editors. | | defaultSortField | No | — | Initial client-side sort field, or initial server sort in windowed mode. Omit it for manual row-order tables. | | defaultSortOrder | No | — | Initial client-side order: 1 ascending or -1 descending. | | emptyMessage | No | Localized standard text | Message displayed when no rows are available. | | headerContent | No | — | Additional React content displayed in the title bar. | | inputRowUpdate | No | — | Async hook for adjusting the standalone inline insert row after a field update and optionally requesting the next focus field. | | insertLine | No | false | Enables SmartTable's inline insert row. Use onCreate instead when creation belongs in a separate workflow. Requires the create operation. | | localization | No | {} | Plain translation-key/message overrides for this table. These take precedence over environment and built-in messages. | | onDeleteRows | No | Built-in model deletion | Replaces deletion persistence for staged saves and confirmed immediate deletion. | | onPrint | No | Table-only browser print workflow | Replaces the built-in print action. | | onReload | No | Reload from source | Replaces the built-in reload action. | | onRowChange | No | — | Receives the updated row, changed field, and details containing the primary key, old/new values, resulting dirty state, and table API after an actual committed native cell or checkbox change. | | onSave | No | Built-in bulk save | Replaces the built-in save action. | | onSelectionChange | No | — | Receives the selected row array and API after row selection changes. | | onUpdateRows | No | Built-in model update | Replaces persistence for changed rows while retaining validation, Save/Cancel state, inserts, deletes, beforeSave, afterSave, error handling, and reload behavior. | | operations | Yes | — | Neutral mutation capabilities: "read-only", "all", one mutable operation, or an array of mutable operations. | | postLoad | No | Identity | Pure transformation applied to plain loaded rows before they enter table state. | | printButton | No | false | Shows icon-only Print in the top toolbar when truthy. A string supplies its tooltip. | | reloadButton | No | false | Shows icon-only Reload in the top toolbar when truthy. A string supplies its tooltip. | | recordLabels | No | {singular: "Record", plural: "Records"} | Supplies neutral record terminology. An omitted plural appends s only for English; other locales reuse the singular label. | | rowCheckbox | No | false | Shows the row-selection checkbox column for multiple selection. It does not represent business data. | | rowCounter | No | false | Shows the filtered row count in the bottom toolbar when present, otherwise in the top toolbar. | | rowOrder | No | — | Configures persisted manual row ordering and optional tri-state column sorting. | | saveButton | No | false | Shows Save in the bottom toolbar when truthy. A string supplies its label. With delete enabled and Save hidden, deletion is confirmed and persisted immediately instead of becoming pending. | | scrollMode | No | "standard" | Selects "none", "standard", or "virtual" DataTable scrolling. | | search | No | false | true enables client search; a non-empty field tuple enables debounced server search. | | searchCaseSensitive | No | false | Makes search-field matching distinguish letter case. | | selectionMode | No | — | Enables "single" or "multiple" row selection. Multiple selection normally uses rowCheckbox. | | sortMode | No | PrimeReact default | Selects "single" or "multiple" column sorting. Row ordering requires single sorting. | | source | Yes | — | Optional generated model, primary-key override, read parameters, and opt-in server-backed window configuration. | | textSelection | No | false | Enables browser text selection inside table contents initially. Inputs remain selectable even when false; with the update operation, row double-click enters edit mode. | | textSelectionButton | No | false | Shows the icon-only top-toolbar text-selection toggle. A string supplies its tooltip. | | title | Yes | — | Localized table title. | | titleSubtext | No | — | Secondary React content displayed below the title. | | topBarButtons | No | [] | Custom actions rendered at the start of the top toolbar's right-hand group. | | viewState | No | — | Persists widths, visibility, and single/multiple sorting. Storage defaults to local browser storage or can be an async external adapter. |

Column Types

Use the native type whenever it matches the data. Native types provide shared formatting, editing, validation, alignment, filtering, and accessibility.

| Type | Intended use | Important behavior | |------|--------------|--------------------| | text | Strings and read-only display fields | Uses a text editor when mutable. | | number | Decimal numbers | Right-aligned, localized formatting and numeric validation. | | int | Safe JavaScript integers | Right-aligned; rejects fractions, trailing text, and values outside the safe-integer range. | | decimal | Exact fixed-scale decimals | Stores canonical strings and uses SmartDB normalization without conversion to JavaScript number. Plain rows require value.scale. | | bigint | Exact integral values | Stores canonical strings and sorts them numerically without conversion to JavaScript number. | | date | Calendar dates | Supports JavaScript Date, canonical ISO-date strings, and SmartDB PlainDate. Generated PlainDate metadata is inferred. Set editorControl: "picker" to opt into a calendar picker. | | datetime | Date/time values | Uses local SmartTools formatting and display-format validation with relaxed day, month, and year widths. Set editorControl: "picker" to opt into a calendar and 24-hour time picker with seconds. | | dropdown | Values from a droplist/domain | Requires source; uses shared droplist loading and labels. | | choice | Typed finite choices | Uses value.options or a droplist source and preserves option value types. | | autocomplete | Query-backed suggestions | Requires autoComplete. | | reference | Stable typed identifiers with async labels | Uses a row-scoped value.query, optional initial resolve, request cancellation, and stale-result protection. | | checkbox | Boolean values | Centered automatically; supports read-only, table-owned, and controlled modes. | | action | Row action buttons | Configure actionButtons; normally set noPrint: true. Action columns are not listed in the visibility chooser. |

Do not render an <input>, checkbox, dropdown, date field, or action button in render when a native column type already supports it.

render takes precedence over the native cell renderer. Reserve it for a presentation the table cannot express, such as combining a code and description in one display cell. A custom renderer owns its accessibility and interaction behavior and should therefore be the exception.

ColumnDefinition attribute reference

| Attribute | Required | Default | Description | |-----------|----------|---------|-------------| | actionButtons | For action | [] | Row actions rendered by an action column. Give standard actions a semantic type such as "edit" or "delete". | | ariaLabel | No | — | Produces a row-specific accessible label, especially for native checkboxes. | | autoComplete | For autocomplete | — | Async suggestion provider receiving field, row, query, and an optional abort signal. Calls use a 150 ms trailing debounce; the loading indicator follows the active promise. | | bodyClassName | No | — | CSS class or per-row callback forwarded to PrimeReact Column.bodyClassName. | | bodyStyle | No | — | Fixed style object forwarded to PrimeReact Column.bodyStyle. | | controlled | No | false | Declares that the parent owns this column's value and persistence. Commonly paired with getValue and onBlur. | | disabled | No | false | Row-specific predicate that disables an interactive native checkbox. | | editorControl | No | "input" | Chooses the native editing control. "picker" currently enables PrimeReact Calendar for date and datetime; the control union can grow with future column editors. | | field | Yes | — | Unique row property name used for values, editing, validation, and sorting. | | getValue | No | Direct row field | Derives the displayed, searched, or checkbox value without mutating the row. | | header | Yes | — | Localized column header. | | hideZero | No | false | Displays numeric zero as an empty value. | | hidden | No | false | Hides the column initially. Hidden columns are absent from screen, print, and client search but remain in row data and payloads. | | hideable | No | true | Controls whether the visibility chooser can hide or restore the column. | | ignoreGlobalFilter | No | false | Excludes the column from client-side table search. | | filterValue | No | Raw row value | Supplies a typed client filter value independently of display. | | mandatory | No | false | Requires a non-empty value during inline insert and editing validation. | | minWidth | No | 80 | Minimum width in pixels for a flexible column. | | noPrint | No | false | Excludes the column from printed output. | | numberFormat | No | "1.2-2" | Angular-style number pattern forwarded to SmartTools.formatNumber. | | onBlur | No | — | Receives committed edits and native checkbox changes. | | printWidth | No | — | Reserved compatibility attribute; current SmartTable printing does not consume it. | | presentation | No | Plain text | Adds a storage-neutral badge or protocol-checked link; screen and print share the same content. | | readonly | No | false | Prevents editing. Read-only checkboxes render as a tick rather than an interactive control. | | resizable | No | true | Controls whether the column exposes its own resize handle when table resizing is enabled. | | render | No | Native renderer | Custom cell renderer. It overrides the native renderer and owns overflow and accessibility behavior. | | searchValue | No | Formatted display text | Supplies searchable text independently of storage and rendering. | | serverFilterField | No | field | Maps a declared server-search field to the field understood by the adapter/server. | | serverSortField | No | field | Declares the corresponding server-side sort field for lazy/server integrations. | | sortValue | No | Raw row value | Supplies a typed client sort value; exact numerics use value-correct comparison. | | sortable | No | false | Enables the PrimeReact header sort control for this field. | | source | For dropdown | — | Droplist definition or domain code used to resolve dropdown values and labels. | | type | Yes | — | Native column type, including exact decimal/bigint, typed choice, and async reference in addition to the legacy types. | | value | No | Inferred from type or generated model metadata | Declares numeric storage/rounding/format, explicit temporal storage, typed choice identity, or reference lookup semantics. | | validators | No | — | Reserved compatibility attribute; current SmartTable validation does not consume custom validators. | | width | No | Flexible | Fixed width in pixels. | | wrap | No | "auto" | Selects "auto", "nowrap", "word", or "anywhere" wrapping for standard value cells. |

Detailed Column Behavior

Exact numbers

Generated SmartDB metadata is authoritative. When the source model marks an attribute with losslessNumberType: "decimal" | "bigint", SmartTable enables exact semantics even when the compatibility column still says type: "number". Marked values are normalized on load and after each accepted edit, remain strings in row state and Save payloads, and are never passed through Number, parseFloat, or parseInt. Conflicting explicit kind, precision, or scale is reported as a column configuration error.

Semantic decimal and bigint columns also accept matching calculation objects, such as SmartDB's Decimal, from getValue and inputRowUpdate. Cell values are normalized before display. Changed insert-row values returned by inputRowUpdate are localized for the editor and converted back to canonical strings when the row is committed.

For plain rows, declare the same contract explicitly:

{
    field: "amount",
    header: "Amount",
    type: "decimal",
    value: {
        format: {currency: "CHF", style: "currency"},
        kind: "decimal",
        precision: 20,
        rounding: DecimalRoundingMode.ROUND_HALF_UP,
        scale: 2
    }
}

All decimal.js-compatible SmartDB rounding modes are supported. Percent display must declare whether storage is a fraction or percentage points. null and undefined remain distinct. Ordinary number columns retain their compatibility behavior, and primary keys remain finite safe JavaScript integers.

SmartTable imports the browser-safe SmartDB normalizers and does not depend on decimal.js. Applications only install SmartDB's optional decimal peer when they actually import generated decimal models or use Decimal calculation getters.

Temporal, choice, and reference semantics

Date-only columns support three storage modes:

  • "date" keeps the legacy JavaScript Date representation.
  • "iso-date" keeps a canonical YYYY-MM-DD string.
  • "plain-date" keeps a SmartDB PlainDate object and emits YYYY-MM-DD at JSON boundaries.

When a generated source model marks an attribute as type: "PlainDate", SmartTable infers the third mode; duplicating column.value is unnecessary and conflicting storage declarations are reported as configuration errors. A model-less source opts in explicitly with value: {kind: "date-only", storage: "plain-date"}. Canonical strings arriving from plain or projected reads are revived through SmartDB's strict plainDate.from contract; timestamp strings and JavaScript Date values are not accepted as implicit PlainDate conversions.

Both generated and explicit PlainDate columns can use editorControl: "picker". PrimeReact's temporary Date is only a UI carrier: SmartTable copies local year/month/day components in both directions, so it never converts the calendar day through an instant or timezone. SmartTable reaches the PlainDate runtime through a lazy boundary, so SmartTable alone does not put the Temporal fallback in an ordinary consumer's initial chunk. Importing SmartDB's root entry or a generated model can still include that fallback according to SmartDB's own browser bundle contract.

ISO-date storage is also validated and formatted without timezone conversion. Date-time storage must choose "instant" (an ISO value with Z or an offset), "local" (a wall-time value without an offset), or legacy "date". String temporal storage cannot use the Date-valued picker editor.

Typed choices compare option values with identity semantics and never stringify numeric values. Set searchBy and sortBy to "label" when localized labels are the intended user-facing meaning. Unknown/inactive values remain visible as their stored value.

References persist only SmartTableValueOption.value; labels are independent. Lookup context includes row identity, field, query, result limit, and an abort signal. Results are keyed per row and field, and a superseded generation cannot replace a newer one. resolve supplies labels for initially loaded identifiers; unresolved or inaccessible identifiers remain visibly represented by the value.

  • field must be unique within the table. It identifies the value used by the editor and PrimeReact sorting.
  • header must use an i18n-backed label.
  • readonly prevents editing. A read-only checkbox is displayed as a tick, not as an interactive disabled checkbox.
  • sortable enables the header sort control.
  • width fixes the width in pixels. Without width, the column remains flexible and receives an implicit 80px minimum width; set minWidth to override that minimum.
  • mandatory participates in insert/edit validation.
  • getValue derives the displayed, searched, or checkbox value without changing the source row.
  • ignoreGlobalFilter excludes a column from client-side global search.
  • noPrint excludes a column from printing.
  • hidden supplies initial visibility. hideable: false makes that state fixed for the mounted table; hidden: true, hideable: false is a valid permanently hidden data field when another data column remains visible.
  • resizable: false removes the column's own handle. In "fit" mode PrimeReact can still change it as the next sibling of a dragged column.
  • wrap selects a standard value cell's wrapping mode: "auto", "nowrap", "word", or "anywhere". Omit it or use "auto" for the type/width-based defaults. "nowrap" uses an ellipsis and supplies the complete formatted value as hover text only while clipping is present. "word" prefers word boundaries but breaks an uninterrupted token when necessary. "anywhere" permits a break between any characters.
  • ariaLabel supplies a row-specific accessible label for native checkboxes.
  • disabled controls whether a native checkbox is interactive for a row.
  • onBlur receives committed edits and checkbox changes.
  • bodyStyle forwards a fixed React.CSSProperties object to PrimeReact's Column.bodyStyle, for example {backgroundColor: "var(--surface-100)"}.
  • bodyClassName forwards either a CSS class string or PrimeReact's per-row (data, options) => string callback to Column.bodyClassName. Prefer classes for conditional field backgrounds so hover, selection, and theme states can be handled in local SCSS.

getValue does not replace PrimeReact's sort field. Sorting still uses field, so a sortable column should normally reference a real sortable row property.

Column resizing and visibility

Both features require the ordinary DataTable layout and are rejected when insertLine is enabled because the standalone insert row uses a separate flex layout. Invalid configuration is reported in the table status area and the requested feature stays disabled.

columnResizeMode: "fit" keeps total table width stable by resizing the dragged column and its next visible neighbor. The final visible boundary has no handle. "expand" changes only the dragged column and grows the table into horizontal scrolling. Width changes are captured after drag end and survive table state updates, data reloads, and visibility changes for the mounted component.

columnToggleButton displays a keyboard-accessible chooser with a select-all checkbox. Checking it restores every chooser column; clearing it hides every hideable column while retaining at least one visible data column. Columns with hideable: false remain visible. Widths are retained by field when columns are hidden or restored. Hidden columns remain available to formatting, validation, dirty tracking, saves, and explicitly configured server search, but client-side search cannot match text that the user cannot see. Sorting is preserved when its column is hidden. If that field controls optional manual row sorting, row dragging remains unavailable until the column is restored.

Persisted view state

Persistence is opt-in and owned by SmartTable rather than PrimeReact's opaque stateStorage payload. This keeps widths associated with column fields and includes the visibility state that SmartTable owns itself:

const tableDef: TableDefinition = {
    // ...ordinary required options
    viewState: {
        key: "delivery-board",
        storage: "local",       // "local" is also the default; "session" is available
        saveDebounceMs: 250
    }
};

The JSON-safe SmartTableViewState payload has version: 1, columnWidths, hiddenColumnFields, and optional single- or multi-sort metadata. Unknown versions, unknown fields, invalid widths, non-sortable fields, and configurations that would hide every column are ignored safely. Restoring an async value participates in the standard DataTable loading overlay. Save operations are debounced and serialized so an older request cannot overwrite a newer preference.

For per-user persistence in an external database, provide one stable adapter object. The server should derive the user from its authenticated session instead of trusting a client-supplied user identifier:

const userTablePreferences: SmartTableViewStateAdapter = {
    load: async (key) => {
        const response = await fetch(`/api/table-preferences/${encodeURIComponent(key)}`);
        return response.status === 404 ? undefined : response.json();
    },
    save: async (key, state) => {
        await fetch(`/api/table-preferences/${encodeURIComponent(key)}`, {
            body: JSON.stringify(state),
            headers: {"content-type": "application/json"},
            method: "PUT"
        });
    },
    remove: async (key) => {
        await fetch(`/api/table-preferences/${encodeURIComponent(key)}`, {method: "DELETE"});
    }
};

const tableDef: TableDefinition = {
    // ...ordinary required options
    viewState: {key: "delivery-board", storage: userTablePreferences}
};

The adapter receives the logical table key; user scoping, transport, authorization, and database schema remain application concerns. Change the key when the logical table or authenticated preference scope changes. Late responses from an earlier key are discarded. SmartTableApi.resetViewState() restores configured defaults and calls the adapter's optional remove; without remove, it stores the default payload instead. Load/save failures are reported in the table status area without preventing row data from loading or being edited.

Mutation Ownership

Authorization Boundary

SmartTable must remain standalone from application authorization. Never import or call session, resource, role, or access-check services in this component. The consumer evaluates permissions and supplies neutral capabilities such as operations, mutable or read-only columns, rowOrder, onCreate, and the standard button options.

These options control what the UI exposes; they are not security checks. Every mutation must still be authorized by the server. This separation keeps SmartTable independent of application-specific permission models and prevents the shared component from acquiring feature-specific policy.

Every mutable value must have exactly one state owner.

SmartTable-owned inline editing

Use ordinary mutable columns when SmartTable should track dirty rows and save them through the configured data adapter. Include "update" in operations and set saveButton: true and usually cancelButton: true. The table owns the edited row copies and Save/Cancel lifecycle.

Use beforeSave, afterSave, onUpdateRows, and onDeleteRows for focused behavior around that lifecycle. onUpdateRows(rows, table) replaces persistence only for existing changed rows; SmartTable still validates, runs the save guard, persists inserts and unhandled deletes, clears pending state after success, runs afterSave, and reloads. A rejected hook keeps the changes staged. Use onSave instead when one application-owned operation must persist every row category atomically. Call methods such as markDirty, deleteRow, or save through a SmartTableApi ref when external operations genuinely need to participate in the same table-owned edit session.

Use onRowChange(row, field, details) to observe committed native cell and checkbox changes without replacing SmartTable's state ownership. The first argument is the updated row record, so consumers can inspect dependent fields without another lookup; details.rowId also exposes its configured primary key. The details include value, previousValue, dirty, and table. Loads, refreshes, row ordering, and imperative dirty-state methods do not emit this hook.

Use markDirtyRows(rowIds) and clearDirtyRows(rowIds) when an external workflow only needs to change which current persisted rows participate in the next built-in save. These methods do not alter row values or the separate state for inserted and deleted rows. Use markDirty(rows) instead when external row values also need to be merged into the table.

After an application-owned operation persists existing rows, call refreshRows(rowIds) to read only those primary keys through the configured data adapter and replace them without disturbing table order or marking them dirty. SmartTable keeps unrelated row references stable and adopts each returned row as the new committed baseline. The adapter must support array values in where as an IN predicate.

Dialog- or parent-owned editing

Use this mode when a surrounding form owns draft state and persists it with its own Save action. Expose only the required neutral operations, omit saveButton, and use controlled columns so the table does not expose a competing persistence workflow.

When this mode includes the delete operation, SmartTable confirms each delete request and persists the requested rows immediately. It does not add those rows to the table's dirty state.

For a native checkbox whose value comes from parent state, set controlled: true and provide getValue plus onBlur:

{
    field: "assigned",
    header: t("permissions.assigned"),
    type: "checkbox",
    controlled: true,
    getValue: (_field, row) => this.state.assignedPks.includes(row.pk),
    disabled: (_field, row) => !this.canAssign(row),
    ariaLabel: (_field, row) => t("permissions.assignItem", {item: row.code}),
    onBlur: (_field, checked, row) => this.toggleAssign(row, checked)
}

A controlled checkbox delegates the update to its owner and does not mutate the table's row copy. Tables containing controlled columns disable PrimeReact cell memoization so parent-driven values and disabled states cannot become stale.

Use functional setState updates in the owning class when one checkbox update depends on existing arrays, maps, or related flags. This prevents rapid events from overwriting one another.

Never combine parent-derived getValue with an uncontrolled mutable checkbox. That creates two sources of truth and can produce stale or cross-influenced checkbox states.

Read-only Lists and Dialog CRUD

For a read-only list whose create/edit operations use dialogs:

  • Mark display columns readonly: true.
  • Set operations: "read-only", or operations: "create" when the table owns a dialog-launching create action.
  • Provide onCreate to open the create dialog.
  • Use an action column for edit/delete commands.
  • Call reload() on the table ref after successful persistence.

Use insertLine for inline creation, or onCreate when creation belongs in a dialog or another parent-owned workflow.

Toolbars

Standard controls use positive boolean | string options: saveButton, cancelButton, printButton, and reloadButton. Omitted or false means hidden. Save and Cancel use visible localized labels; Print and Reload are icon-only. A string customizes the corresponding label or tooltip. Callbacks are configured separately with onSave, onPrint, and onReload.

The bottom toolbar is displayed when Save, Cancel, or bottomBarButtons are visible. Print, Reload, and rowCounter use the bottom toolbar when it exists; otherwise they appear on the right side of the top toolbar. topBarButtons appear before the row count. Search always remains at the right edge, with separators between the row count, the complete button group, and search. Buttons within the same toolbar never have separators between them.

Browser text selection is disabled for table display content by default; text inputs remain selectable for editing. Set textSelection: true to enable it. Set textSelectionButton to show a toggle in the top toolbar. The toggle appears immediately left of top-toolbar Reload without a separator; a string value is used as its tooltip.

When text selection is disabled, a row double-click first looks for an action button with type: "edit" and executes its handler. This supports dialog-owned editing without declaring inline update capability. If no semantic Edit action exists and operations includes "update", double-click enters edit mode for the clicked editable cell. When text selection is enabled, double-click retains normal browser selection behavior and does not invoke either edit path.

Search

Set search: true for client-side search across visible, non-action columns. Search uses the formatted value, including getValue, and skips columns with ignoreGlobalFilter: true. Matching is case-independent by default; set searchCaseSensitive: true to opt into case-sensitive matching.

For server-side search, set search to a non-empty tuple of model field names, for example search: ["cd", "description"]. Input is debounced and ordinary eager loading adds a LIKE filter to the source query. Windowed mode passes normalized {caseSensitive, fields, term} metadata to readRange() so the adapter owns the database predicate and must honor the requested case behavior. Do not use derived display-only fields unless the column supplies serverFilterField.

Sorting

Set sortable: true on each sortable column. Use:

  • defaultSortField and defaultSortOrder for the initial client-side sort and sort indicator.
  • sortMode: "single" or sortMode: "multiple" for the interaction mode.
  • source.params.orderBy for deterministic API/database ordering.

For predictable initial results, keep defaultSortField consistent with the leading orderBy field. Sorting is currently field-based; custom sort functions are not exposed by SmartTable.

Server-backed Windowing

scrollMode: "virtual" alone keeps mounted DOM rows bounded but deliberately retains the eager data contract. Add source.window only for a table whose adapter can load globally correct absolute ranges and an exact matching total:

const tableDef: TableDefinition = {
    defaultSortField: "title",
    defaultSortOrder: 1,
    operations: "read-only",
    rowCounter: true,
    scrollMode: "virtual",
    search: ["title", "owner"],
    selectionMode: "single",
    source: {
        model: TaskModel,
        window: {pageSize: 100, retainedPages: 3}
    },
    title: "Tasks",
    virtualRowHeight: 28
};

pageSize is required and must be from 1 through 1,000. retainedPages defaults to five nearby pages in addition to every page intersecting the current rendered range. The logical sparse array preserves scroll height while only loaded page rows and a small neighboring cache are retained as objects.

The environment adapter implements the separate optional operation below. params are the application-owned base parameters; sort is deterministic and always ends with the stable primary key; search is present only for a non-empty debounced server search. The adapter must apply all of them to both rows and total from the same logical snapshot.

readRange: async (model, request) => {
    const result = await api.readPage(model, {
        count: request.count,
        first: request.first,
        params: request.params,
        search: request.search,
        sort: request.sort,
        signal: request.signal
    });
    return {rows: result.rows, total: result.total};
}

SmartTable aligns requests to pages, coalesces overlap, aborts obsolete generations, ignores stale results, and evicts distant pages. Explicit reload, source identity, sort, and search changes reset to the first range; refreshRows() invalidates the cache and refetches the visible range. The row counter reports the adapter total, not the number of retained rows. getData() returns only the currently retained rows in this mode. postLoad transforms one loaded page at a time, and afterLoad runs after that page enters table state.

The first release intentionally rejects configurations whose correct semantics require the complete result:

| Capability | Windowed support | |------------|------------------| | Column visibility/resizing and persisted view state | Yes | | Reload, row counter, text selection, row actions | Yes | | Single-row selection by stable primary key | Yes | | Global server search and server sorting | Yes | | Create/update/delete, Save/Cancel, manual row ordering | No | | Checkbox select-all or multiple-row selection | No | | Built-in browser print | No; provide onPrint | | Client search (search: true) or client sortValue | No |

Every sortable windowed column uses serverSortField ?? field. Choice/reference label sorting or searching requires an explicit serverSortField or serverFilterField; SmartTable never sorts or searches only the cached subset. The demo adapter shows one SmartDbServer translation using validated limit: {limit, offset}, orderBy, where, and a separate exact-count command.

Manual Row Ordering

Use rowOrder only when rows have a persisted numeric sequence field and the table loads the complete ordering scope. The canonical source order must start with that field ascending, the default client sort must be omitted, sorting must be single-column, and virtual scrolling is not supported.

For table-owned editing, reordered rows join the normal dirty-row Save/Cancel lifecycle:

operations: "update",
saveButton: true,
cancelButton: true,
rowOrder: {field: "sortOrder", mode: "table", step: 10}

For dialog-owned or transaction-specific workflows, use external persistence:

operations: "update",
rowOrder: {
    field: "sortOrder",
    mode: "external",
    step: 10,
    columnSort: {field: "cd"},
    onPersist: (orderedRows, changedRows) => persistOrder(orderedRows, changedRows)
}

External mode updates optimistically and restores the previous order when the callback fails. Each successful reorder renumbers the complete scope to step, 2 * step, 3 * step, .... The public static SmartTable.renumberRows() method exposes the same deterministic calculation for individual workflows and tests.

The unsorted DataTable view is canonical: it preserves the array order returned by source.params.orderBy, so PrimeReact reorders the same array it displays. Sorting any column disables dragging until removable sorting is cleared. Search, invalid cells, pending inserts or deletes, loading, and external persistence also disable dragging. Authorized users retain a dimmed handle with a localized reason; consumers that omit rowOrder, including read-only views, show no handle. Tables use their ordinary behavior unless rowOrder is configured.

Set rowOrder.columnSort.field to a sortable column when users should be able to switch between manual order and conventional column order. The removable single-sort cycle then has three explicit states: ascending, descending, and manual row order. Manual state displays PrimeReact's standard unsorted up/down-arrow icon with a dimmed treatment on the configured column. Leaving manual order for ascending column sort requires confirmation by default, but only when the current sequence differs from descending column order. Descending is the state immediately before manual in the tri-state cycle, so a matching sequence is known to be easily reproducible and does not need a warning; set warnWhenLeavingManualOrder: false to suppress it. Switching directly between ascending and descending does not warn. Each ascending or descending transition renumbers and persists the rows through the configured row-order owner. Returning to the third state removes the active sort marker; the rows remain in the last persisted sequence and dragging is re-enabled when the other row-order preconditions are satisfied.

SmartTable disables PrimeReact body-cell memoization for row-order tables so every move handle immediately reflects sort, search, loading, and persistence state changes. Consumers do not need to force row refreshes when switching between column and manual order.

Set rowOrder.reorderable: false only when an update-capable workflow should retain persisted tri-state column ordering but deliberately omit row-drag handles. It is not an authorization control: when the user lacks update permission, omit rowOrder and do not mark sequence-changing columns sortable. Omitting the option permits reordering.

Selection and Master/Detail Screens

Use selectionMode: "single" and onSelectionChange for a master/detail table. The callback receives the selected rows array and table handle. Use rowCheckbox for bulk row selection and getSelection/setSelection through the ref when needed.

Selection is separate from boolean data columns. Do not use a row-selection checkbox to represent a persisted business flag.

Loading and Transformation

SmartTable reads source.model with source.params, converts model instances to plain row objects, and then applies postLoad if supplied. Use postLoad for small presentation-oriented row transformations, not as a second business-state store. Use afterLoad for notifications or owner coordination after rows are in table state.

When source.model is null or omitted, the data adapter returns plain objects directly. Declare source.primaryKey when their stable key is not named pk.

Conversion Tools

Date and number conversion is intentionally local rather than part of the static environment contract. SmartTable imports the package's tools singleton directly. It defaults to dd.MM.yyyy, dd.MM.yyyy HH:mm:ss, and de-CH and can be configured once at application startup:

import {tools} from "@egi/smart-table";

tools.configure({dateFormat: "yyyy-MM-dd", dateTimeFormat: "yyyy-MM-dd HH:mm:ss", locale: "en-CH"});

Number columns use the optional localization adapter's current locale, or en when no adapter is configured. SmartTable date shortcuts and partial-date expansion are currently disabled, while their SmartBrowserTools implementation remains available. Editors instead require complete date and datetime values matching the configured display order and separators. Days and months may use one or two digits; years may use two or four digits, with two-digit years placed in the current century.

Date and datetime columns use ordinary text input by default. Opt into the PrimeReact Calendar control per column with editorControl: "picker". Picker selection returns a Date, while typed invalid text is retained so the same field-exit and pre-save validation messages apply.

Layout, Localization, and Accessibility

  • Place the table in a container with a usable height; the wrapper fills its parent and scrolls internally.
  • Keep feature-specific dimensions and appearance in the owning screen's SCSS.
  • Use width deliberately for checkbox/action columns and translated headers; verify longer German labels are not clipped.
  • Choose date/time and identifier column widths based on the application's typography, localization, and density settings.
  • All titles, headers, empty messages, actions, and accessibility labels must be i18n-backed.
  • Native checkbox columns are centered automatically.
  • Flexible text, dropdown, and autocomplete columns wrap at word boundaries when possible and break an uninterrupted value at the available line width only when necessary. Fixed-width columns and number, int, date, and datetime values are no-wrap by default and use an ellipsis plus conditional complete-value hover text. Override the default with wrap: "nowrap", wrap: "word", or wrap: "anywhere". Custom render cells own their own overflow behavior.
  • SmartTable uses a fixed table layout so flexible no-wrap columns shrink into the available table width without shrinking below their configured or implicit minimum. SmartTable applies the sum of all fixed/minimum column widths to the table itself, producing horizontal scrolling when that sum exceeds the viewport instead of collapsing later columns.
  • Use scrollMode: "virtual" only when the surrounding container supplies a measurable height. "standard" is the default scrolling behavior and "none" disables DataTable scrolling.
  • Use source.window only with a stable primary key, deterministic server order, and an adapter whose exact total matches its row predicate.

Current Limitations

SmartTable does not currently expose:

  • A generic row-expansion API.
  • A typed generic ColumnDefinition<T> contract.

Extend the shared component before implementing one of these capabilities in a single screen.

Review Checklist

Before finishing a SmartTable change, verify:

  • A native column type is used wherever available.
  • operations lists only UI capabilities that this consumer may expose; access services remain outside SmartTable.
  • Every mutable value has one state owner.
  • Parent-owned checkboxes are marked controlled.
  • Column headers and accessibility labels are translated.
  • Sortable fields exist on the row and initial sorting matches orderBy.
  • Row ordering, when configured, uses a complete canonical scope and exactly one persistence owner.
  • Search includes only meaningful columns.
  • The model primary key is stable.
  • Create, delete, Save/Cancel, and dialog actions do not provide competing flows.
  • The table container has sufficient height and translated headers are visible.
  • Typecheck and the production client build pass.