ResponsiveTable
ResponsiveTable<T extends ResponsiveTableItem> is a responsive data table with built-in column filtering, pagination, select-all, and row state management. It is an alternative to Table<T> designed for layouts that need runtime column toggling and responsive row rendering.
Package: io-oidis-usercontrols
Basic Usage
Declare a ResponsiveTable in the panel, bind it in innerHtml(), and configure it in the controller:
// Panel
export class ProductPanel extends BasePanel {
public readonly table : ResponsiveTable<ProductTableItem>;
constructor() {
super();
this.table = new ResponsiveTable<ProductTableItem>();
}
public innerHtml() : string {
return `
<div data-oidis-bind="${this.table}"></div>
`;
}
}
// Controller — bind in beforeLoad()
public async beforeLoad($instance : ProductPanel) : Promise<void> {
$instance.table.BindData(
async ($offset : number, $limit : number) => {
return await this.client.ListProducts({ offset: $offset, limit: $limit });
},
async ($data : IProduct, $item : ProductTableItem) => {
$item.Data($data);
$item.itemId.Text($data.Id);
$item.text.Text($data.Name);
},
async ($item : ProductTableItem) => {
EventsBinding.SingleClick($item, async () : Promise<void> => {
this.currentRecord = $item.Data();
this.switchToEditMode($instance);
});
}
);
await $instance.table.ShowData();
}
The 3-Callback BindData Pattern
ResponsiveTable uses the same 3-callback pattern as Table<T>, with slight signature differences:
$instance.table.BindData(
// 1. Fetch — retrieve paginated data
async ($offset : number, $limit : number) : Promise<IModelListResult> => {
return await this.client.List({ offset: $offset, limit: $limit });
},
// 2. Row mapping — populate each item with data
async ($data : IMyData, $item : MyTableItem, $index : number) : Promise<void> => {
$item.Data($data);
$item.itemId.Text($data.Id);
$item.text.Text($data.Name);
},
// 3. Row create (optional) — setup row-level interactions
async ($item : MyTableItem, $index : number) : Promise<void> => {
EventsBinding.SingleClick($item, async () : Promise<void> => {
// Handle row click
});
}
);
// Trigger initial data load
await $instance.table.ShowData();
Callback Details
Callback 1: Fetch ($offset : number, $limit : number) => IModelListResult
- Called every time
ShowData()is invoked or the user navigates to a new page - Receives pagination offset and limit from the table
- Must return
{ data: any[], size: number, offset: number, limit: number } - The
sizecount drives pagination controls
Callback 2: Row Mapping ($data : any, $item : T, $index : number) => void
- Called once per record in the result set
$datais the raw record from the fetch result$itemis the typedResponsiveTableItemrow- Store data on the row with
$item.Data($data)for later retrieval
Callback 3: Row Create ($item : T, $index : number) => void (optional)
- Called once when a row is first created (not on re-bind)
- Bind click handlers and other one-time event setup here
- Row instances are reused across page changes
Column Configuration
Column visibility is configured through the IResponsiveTableColumn interface:
| Property | Type | Purpose |
|---|---|---|
id | string | Unique column identifier |
text | string | Display text for column header |
icon | string | SVG icon HTML for the filter chip |
controls | GuiCommons[] | References to cell elements in each row |
enabled | boolean | Whether column is currently visible |
hidden | boolean | Whether column is excluded from filter UI |
modifiers? | string[] | Optional CSS modifier classes |
width? | string | Optional column width |
sortAscendingByDefault? | boolean | Direction a fresh sort by this column starts with (true reads best for text, false for numbers) |
The default filter configuration provides two columns: ID ("id") and Content ("text"). Override getFilterConfig() to define custom columns:
protected getFilterConfig() : IResponsiveTableColumn[] {
return [
{ id: "id", text: "ID", icon: "...", controls: [], enabled: true, hidden: false },
{ id: "name", text: "Name", icon: "...", controls: [], enabled: true, hidden: false },
{ id: "status", text: "Status", icon: "...", controls: [], enabled: false, hidden: false }
];
}
Column Filter Management
The column filter allows users to toggle column visibility at runtime. Filter state is automatically persisted to localStorage.
| Method | Signature | Purpose |
|---|---|---|
FilterSelectorVisible | ($value? : boolean) : boolean | Show/hide the column filter dropdown |
Filter | () : void | Apply current filter configuration to all rows |
Init | ($filter? : string[]) : void | Initialize or restore filter state from localStorage |
// Initialize with specific columns enabled
$instance.table.Init(["id", "name", "status"]);
// Hide the filter selector (columns still respect persisted state)
$instance.table.FilterSelectorVisible(false);
// Manually re-apply the current filter
$instance.table.Filter();
Init() reads persisted filter state from localStorage using the component class name as key. If no persisted state exists, it falls back to the getFilterConfig() defaults. Pass an explicit $filter array to override persisted state.
Header Sorting
The table tracks a sort key and direction and exposes them to subclasses. Header cells are plain
markup (not child components), so a single click listener on the table resolves clicks through the
data-sort attribute emitted by getSortableHeaderLabel():
// Subclass: render sortable header labels inside the header markup
headerCells += this.getSortableHeaderLabel($column.label, $column.key);
// Consumer: seed defaults and react to clicks
table.SortKey("score");
table.SortAscending(false);
table.setOnSortChange(($key : string, $ascending : boolean) : void => {
// re-fetch or re-sort, then table.ShowData()
});
Clicking the active column flips the direction; clicking a new column starts at that column's
sortAscendingByDefault (defaults to ascending). Setting SortKey()/SortAscending() repaints the
↑/↓ indicators without firing the handler.
Resizable Columns
ColumnsResizable(true) lets the user drag a column's width from its header. It works like sorting: the
subclass places a handle into each header cell with getResizeHandle($key) (a data-resize span on the
cell's right edge — the header cell must be position: relative), and one listener on the table follows
the drag. A double click on the handle restores the default width.
The table does not size cells itself. It keeps the widths per table class in its persistence (they survive
a reload) and publishes them as CSS custom properties on the table element (the column id is converted to
kebab-case, e.g. lastChecked → last-checked):
| Property | Value |
|---|---|
--column-width-<id> | <width>px |
--column-flex-<id> | 0 0 <width>px |
Column cells opt in from the stylesheet, falling back to their normal size:
// Subclass constructor + header markup
this.ColumnsResizable(true);
headerCell += this.getResizeHandle($column.id);
@media (min-width: 992px) {
.my-table-col--name {
flex: var(--column-flex-name, 2 2 0);
}
}
Because only the rules that read the properties change, a narrow card layout that stacks the cells (and does not use them) is unaffected by the stored widths.
Pagination
ResponsiveTable includes a built-in Pagination component. Pagination is managed automatically when using BindData() and ShowData().
| Method | Signature | Purpose |
|---|---|---|
ItemsPerPage | ($value? : number) : number | Get/set items per page (default 25) |
PagesLimit | ($value? : number) : number | Get/set max page buttons |
SelectPage | ($index : number, $from? : number) : void | Navigate to a page offset |
setOnPageSelect | ($handler : ($index : number) => Promise<void>) : void | Set page change callback |
getIndex | () : number | Get current pagination offset |
// Configure items per page
$instance.table.ItemsPerPage(50);
// Force refresh and reset to first page
await $instance.table.ShowData(true);
When using BindData(), page change callbacks are wired automatically — ShowData() is called on every page navigation. You only need setOnPageSelect() if you require additional logic beyond the default data fetch.
Selection
ResponsiveTable provides built-in select-all and per-row selection:
| Method | Signature | Purpose |
|---|---|---|
setOnAllSelect | ($handler : ($pageIndex : number, $selected : boolean) => Promise<void>) : void | Select-all checkbox handler |
setOnItemSelect | ($handler : ($item : T) => Promise<void>) : void | Individual item select handler |
// Handle select-all
$instance.table.setOnAllSelect(async ($pageIndex : number, $selected : boolean) : Promise<void> => {
// $selected is the new checkbox state
// $pageIndex is the current pagination offset
});
// Handle individual selection
$instance.table.setOnItemSelect(async ($item : ProductTableItem) : Promise<void> => {
const record : IProduct = $item.Data();
// Handle selection change
});
The select-all checkbox automatically reflects the aggregate state: checked when all visible items are selected, unchecked when none are selected, and indeterminate when some are selected.
ResponsiveTableItem
ResponsiveTableItem is the base row class. Each row contains built-in selection, ID, and text controls:
| Property | Type | Purpose |
|---|---|---|
select | CheckBox | Row selection checkbox |
itemId | Label | ID column label |
text | Label | Text column label |
idContent | GuiCommons | ID cell container (used for filter mapping) |
textContent | GuiCommons | Text cell container (used for filter mapping) |
Key Methods
| Method | Signature | Purpose |
|---|---|---|
setState | ($value : ResponsiveTableItemType) : void | Apply a visual state class |
Restore | () : void | Reset item for reuse (unchecks select, clears labels) |
The default innerHtml() renders each row as a responsive Bootstrap grid with bordered cells containing icons and labels.
Row States
The ResponsiveTableItemType enum provides visual state classes applied to rows:
| State | CSS Class | Visual Effect |
|---|---|---|
DELETED | Deleted | Strikethrough + muted text |
ARCHIVED | Archived | Muted/grayed appearance |
IN_PROGRESS | InProgress | In-progress indicator |
COMPLETED | Completed | Completed indicator |
// In row mapping callback
async ($data : IProduct, $item : ProductTableItem) : Promise<void> => {
$item.Data($data);
$item.itemId.Text($data.Id);
$item.text.Text($data.Name);
if ($data.IsDeleted) {
$item.setState(ResponsiveTableItemType.DELETED);
} else if ($data.IsArchived) {
$item.setState(ResponsiveTableItemType.ARCHIVED);
}
};
Subclassing
Custom Table Item
Extend ResponsiveTableItem to add project-specific columns:
export class ProductTableItem extends ResponsiveTableItem {
public readonly price : Label;
public readonly category : Label;
public readonly priceContent : GuiCommons;
public readonly categoryContent : GuiCommons;
constructor() {
super();
this.price = new Label();
this.category = new Label();
this.priceContent = new GuiCommons();
this.categoryContent = new GuiCommons();
}
protected innerHtml() : string {
return `
<div class="row g-0 ResponsiveTableItem" style="border-radius: 6px;border-width: 1px;border-style: solid;margin-bottom: 3px;" data-oidis-bind="${this}">
<div class="col-auto text-start align-self-center" style="width: 25px;">
<input class="form-check-input" type="checkbox" data-oidis-bind="${this.select}" style="margin-left: 4px;" />
</div>
<div class="col">
<div class="row g-0">
<div class="col-12 col-md-6 col-lg-auto" data-oidis-bind="${this.idContent}">
<div style="border-radius: 4px;border: 1px solid rgb(115,115,115);margin: 2px;min-width: 106px;font-weight: bold;">
<span data-oidis-bind="${this.itemId}"></span>
</div>
</div>
<div class="col" data-oidis-bind="${this.textContent}">
<div style="border-radius: 4px;border: 1px solid rgb(115,115,115);margin: 2px;">
<span data-oidis-bind="${this.text}"></span>
</div>
</div>
<div class="col" data-oidis-bind="${this.priceContent}">
<div style="border-radius: 4px;border: 1px solid rgb(115,115,115);margin: 2px;">
<span data-oidis-bind="${this.price}"></span>
</div>
</div>
<div class="col" data-oidis-bind="${this.categoryContent}">
<div style="border-radius: 4px;border: 1px solid rgb(115,115,115);margin: 2px;">
<span data-oidis-bind="${this.category}"></span>
</div>
</div>
</div>
</div>
</div>`;
}
}
Nothing in the framework instantiates members for you. A member declared as = undefined stays undefined — the template renders the text undefined and GuiCommons.Register() logs <Class>.<member> was declared but instance is missing. Did you miss it in constructor?.
Custom Table
Extend ResponsiveTable to provide custom filter columns, item instances, localization, and filter mapping:
export class ProductTable extends ResponsiveTable<ProductTableItem> {
constructor() {
super();
this.localization = {
selectAll: "Select all",
show : "Show",
showMore : "More columns",
noData : "No products found"
};
}
protected getNewItemInstance() : ProductTableItem {
return new ProductTableItem();
}
protected getFilterConfig() : IResponsiveTableColumn[] {
return [
{ id: "id", text: "ID", icon: "...", controls: [], enabled: true, hidden: false },
{ id: "text", text: "Name", icon: "...", controls: [], enabled: true, hidden: false },
{ id: "price", text: "Price", icon: "...", controls: [], enabled: true, hidden: false },
{ id: "category", text: "Category", icon: "...", controls: [], enabled: false, hidden: false }
];
}
protected setItemToFilterMapping($item : ProductTableItem) : void {
this.assignFilterTo("id", $item.idContent);
this.assignFilterTo("text", $item.textContent);
this.assignFilterTo("price", $item.priceContent);
this.assignFilterTo("category", $item.categoryContent);
}
}
When subclassing ResponsiveTable, always override getNewItemInstance(), getFilterConfig(), and setItemToFilterMapping() together. The filter mapping must match the column IDs defined in the filter config, and the item instance must have the corresponding content containers.
Localization
The IResponsiveTableLocalization interface controls UI labels:
| Property | Type | Default | Purpose |
|---|---|---|---|
selectAll | string | "select all" | Select-all checkbox label |
show | string | "Show" | Column filter prefix label |
noData | string | "No data found" | Empty state alert message |
showMore | string | "Show more" | Hidden columns dropdown button text |
Set localization in the constructor before rendering:
constructor() {
super();
this.localization = {
selectAll: "Vybrat vše",
show : "Zobrazit",
showMore : "Další sloupce",
noData : "Žádná data"
};
}
API Reference
ResponsiveTable<T>
| Method | Signature | Purpose |
|---|---|---|
Enabled | ($value? : boolean) : boolean | Get/set enabled state (cascades to table and pagination) |
FilterSelectorVisible | ($value? : boolean) : boolean | Get/set column filter dropdown visibility |
ItemsPerPage | ($value? : number) : number | Get/set items per page |
PagesLimit | ($value? : number) : number | Get/set max pagination buttons |
getIndex | () : number | Get current pagination offset |
SelectPage | ($index : number, $from? : number) : void | Navigate to page; $from controls pagination visibility |
setOnPageSelect | ($handler) : void | Set page change handler |
setOnAllSelect | ($handler) : void | Set select-all handler |
setOnItemSelect | ($handler) : void | Set per-item select handler |
Filter | () : void | Apply current column filter to all rows |
Init | ($filter? : string[]) : void | Initialize/restore filter from localStorage |
Clear | () : void | Remove all items, show no-data alert |
Restore | () : void | Hide and reset all items for reuse |
AddItem | () : Promise<T> | Create and add a new row item |
getItem | ($index : number) : T | Get item by index |
IsEmpty | () : boolean | Whether the table has no items |
BindData | ($onFetch, $onRowMapping, $onRowCreate?) : void | Bind the 3-callback data pattern |
SortKey | ($value? : string) : string | Get/set the active sort column key (setter repaints indicators) |
SortAscending | ($value? : boolean) : boolean | Get/set the sort direction (setter repaints indicators) |
setOnSortChange | ($handler) : void | Enable header-click sorting and set the change handler |
ColumnsResizable | ($value? : boolean) : boolean | Get/set drag-to-resize of columns (widths persisted, exposed as CSS custom properties) |
ApplyColumnWidths | () : void | Re-publish the stored widths after the table element was re-rendered |
ShowData | ($resetOffset? : boolean) : Promise<void> | Fetch and render data; pass true to reset to first page |
ResponsiveTableItem
| Method / Property | Type | Purpose |
|---|---|---|
select | CheckBox | Row selection checkbox |
itemId | Label | ID column label |
text | Label | Text column label |
idContent | GuiCommons | ID cell container |
textContent | GuiCommons | Text cell container |
setState | ($value : ResponsiveTableItemType) : void | Apply visual state class |
Restore | () : void | Reset for reuse |
Protected Methods (for subclassing)
| Method | Signature | Purpose |
|---|---|---|
getNewItemInstance | () : T | Factory method for row items |
getFilterConfig | () : IResponsiveTableColumn[] | Define available columns |
setItemToFilterMapping | ($item : T) : void | Map item containers to column IDs |
assignFilterTo | ($id : string, $control : GuiCommons) : void | Register a control under a column ID |
filterRow | ($item : T) : void | Custom per-row filter logic (override) |
getFilterSelectorInstance | () : DropDownList | Factory for the "show more" dropdown |
getHeaderItem | ($column : IResponsiveTableColumn) : InlineItem | Factory for filter chip items |
getItems | () : T[] | Access the internal items array |
getFilterById | ($id : string) : IResponsiveTableColumn | Lookup column config by ID |
getSortableHeaderLabel | ($label : string, $key : string) : string | Build a clickable header label span with a sort indicator |
renderSortIndicators | () : void | Repaint the ↑/↓ indicators from the current sort state |
getResizeHandle | ($key : string) : string | Build the column-resize drag handle for a header cell (empty while resizing is off) |
See also: Tables | User Controls | Pagination