Data Components
Reference for Spiderly's data table and data view components — paginated lists with filtering, sorting, selection, and Excel export.
Overview
Spiderly provides two components for displaying entity lists:
- SpiderlyDataTable — paginated table with server-side filtering, sorting, row selection, and Excel export.
- SpiderlyDataView — card-based list with filtering, useful for visual layouts.
Both support lazy loading (server-side pagination) and work with the generated API service methods.
SpiderlyDataTable
Selector: spiderly-data-table
Advanced paginated table with server-side filtering, sorting, selection, inline editing, and Excel export.
Key Inputs
| Input | Type | Default | Description |
|---|---|---|---|
cols | Column[] | — | Column definitions (see Column Configuration) |
filters | FilterSource | — | The page's filter store — the table's whole filter surface (see Filtering). Read once at init |
views | TableView[] | — | Saved filter presets rendered as tabs above the filter bar |
getPaginatedListObservableMethod | (filter) => Observable<PaginatedResult> | — | Server fetch method for lazy loading |
exportListToExcelObservableMethod | (filter) => Observable<any> | — | Method called on "Export to Excel" |
deleteItemFromTableObservableMethod | (id) => Observable<any> | — | Method called when a row is deleted |
deleteListFromTableObservableMethod | (ids) => Observable<any> | — | Method called when selected rows are bulk-deleted. When provided, selectionMode is auto-set to 'multiple' and checkboxes appear on each row. |
rows | number | config default | Page size |
rowsPerPageOptions | number[] | [10, 25, 50, 100] | Page-size choices in the paginator dropdown. A custom rows value is merged in automatically; the user's pick persists with the table state |
selectionMode | 'single' | 'multiple' | — | Row selection mode |
readonly | boolean | false | Hides all action buttons |
hasLazyLoad | boolean | true | true = server-side; false = client-side |
items | any[] | — | Data rows (only when hasLazyLoad is false) |
showAddButton | boolean | true | Show the "Add" button |
showExportToExcelButton | boolean | true | Show the "Export" button |
showReloadTableButton | boolean | false | Show the reload-table button |
showPaginator | boolean | true | Show the paginator. Pass only when hasLazyLoad is false |
showCardWrapper | boolean | false | Wrap the table in a card container |
navigateOnRowClick | boolean | false | Clicking a row navigates to details (using idField). Interactive cells — checkboxes, action icons, editable and onCellClick cells — keep their click and never navigate |
additionalFilterIdLong | number | — | Extra filter ID appended to every request |
defaultSortField | string | — | Sort applied while the user has no sort of their own — shows as a normal header arrow; persisted user sort wins; un-sorting (tri-state header click, the sort menu's reset row) returns to it, and views without a sort of their own fall back to it |
defaultSortOrder | 1 | -1 | 1 | Direction for defaultSortField |
Key Outputs
| Output | Payload | Description |
|---|---|---|
onLazyLoad | Filter | Fires on every table load (sort, filter, page change) |
onTotalRecordsChange | number | Fires when the total record count changes |
onRowSelect | RowClickEvent | Fires when a row is selected |
onRowUnselect | RowClickEvent | Fires when a row is deselected |
onIsAllSelectedChange | AllClickEvent | Fires when the "select all" checkbox changes |
Example
import { Component, OnInit } from '@angular/core';
import { TranslocoService } from '@jsverse/transloco';
import {
Column,
createFilterStore,
dateFilter,
numberFilter,
SpiderlyDataTableComponent,
textFilter,
} from 'spiderly';
import { ApiService } from 'src/app/business/services/api/api.service';
function createProductFilters(t: (key: string) => string) {
return createFilterStore({
name: textFilter({ label: t('Name') }),
price: numberFilter({ label: t('Price') }),
createdAt: dateFilter({ label: t('CreatedAt') }),
});
}
@Component({
selector: 'product-list',
templateUrl: './product-list.component.html',
imports: [SpiderlyDataTableComponent],
})
export class ProductListComponent implements OnInit {
cols: Column[];
filters: ReturnType<typeof createProductFilters>;
getProductTableDataObservableMethod = this.apiService.getProductPaginatedList;
exportProductListToExcelObservableMethod = this.apiService.exportProductListToExcel;
deleteProductObservableMethod = this.apiService.deleteProduct;
deleteProductListObservableMethod = this.apiService.deleteProductList;
constructor(
private apiService: ApiService,
private translocoService: TranslocoService,
) {}
ngOnInit() {
this.filters = createProductFilters((key) => this.translocoService.translate(key));
this.cols = [
{ name: this.translocoService.translate('Name'), field: 'name', filterType: 'text' },
{ name: this.translocoService.translate('Price'), field: 'price', filterType: 'numeric' },
{
name: this.translocoService.translate('CreatedAt'),
field: 'createdAt',
filterType: 'date',
},
];
}
}<spiderly-data-table
[cols]="cols"
[filters]="filters"
[getPaginatedListObservableMethod]="getProductTableDataObservableMethod"
[exportListToExcelObservableMethod]="exportProductListToExcelObservableMethod"
[deleteItemFromTableObservableMethod]="deleteProductObservableMethod"
[deleteListFromTableObservableMethod]="deleteProductListObservableMethod"
></spiderly-data-table>Column Configuration
Columns are defined using the Column class. Each column maps to a data field and can have filtering, sorting, and custom actions.
The table lays its columns out from these declarations rather than from the rows on screen (table-layout: fixed), so a column keeps its width across paging, searching and filtering. Declared widths behave as ratios: the table shares surplus space in proportion to them, and a table whose columns need more room than its container gets scrolls horizontally instead of crushing them. Because a column no longer widens to fit its text, the default cell clamps to one line with an ellipsis; a column rendering its own cell template clamps its own lines.
| Property | Type | Description |
|---|---|---|
name | string | Column header label |
field | string | Data field name (key from the DTO) |
filterType | 'text' | 'date' | 'multiselect' | 'boolean' | 'numeric' | 'blob' | The column's value shape — drives cell rendering, alignment and the default width |
filterId | string | Id of the filter in the table's store this column stands for; needed only when it differs from field (see Filtering) |
width | string | CSS length fixing the column's width, overriding the per-filterType default (e.g. '8rem') |
dropdownOrMultiselectValues | PrimengOption[] | Options a multiselect column maps its cell values through (id → label) |
editable | boolean | Enable inline editing for this column |
showTime | boolean | For date columns: show time portion |
decimalPlaces | number | For numeric columns: decimal formatting |
actions | Action[] | Row action buttons (see Actions) |
visible | boolean | Whether the column is initially rendered (default true; see Column chooser) |
lockVisible | boolean | Pin the column — always rendered, shown checked and disabled in the chooser |
onCellClick | (e: CellClickEvent) => void | Per-cell click handler (see Cell click) |
Filtering — the filter store and chip bar
The table's whole filter surface is a store the page owns and hands to [filters]; a table given none has no filtering. Declare it with createFilterStore and one factory per filter — textFilter, numberFilter (accepts options for a pick-list), booleanFilter, dateFilter — as in the example above. The table renders it as a chip bar above the header: one chip per applied filter, a sort menu (the "Sorted by" chip opens a popover of every sortable column — hidden ones included — with a reset-to-default row while the ordering differs from the default), the result count, a searchable + Filter menu of every declared filter, and Clear filters, which clears only the filters — the sort is untouched.
- Filter ids are backend property names — they go straight into the paginated request — so they normally repeat the column's
field. A column whose filter id differs links to it withColumn.filterId; columns never declare filters themselves. - Filters live independently of columns. A filter with no column, or a hidden one, is still offered and still narrows the grid — hiding a column keeps its filter and sort, because the chips are where both stay visible.
operatorson a factory narrows what the editor offers (the first entry becomes the default; each kind's full list mirrors what the generated backend implements).offered: falsekeeps a filter out of + Filter when a dedicated control on the page drives it throughfilters.get(id).- Late-arriving option lists (an async lookup) go in through
store.setOptions(id, options); programmatic writes throughstore.setAndCommit(id, { operator, value }). - Applied filters persist per table under
`${stateKey}:filters`in thestateStoragestorage; a custom editor control for one filter is a projected<ng-template spiderlyFilterTemplate="filterId" let-f>.
Views ([views]) are saved filter presets rendered as tabs: each view's apply receives the cleared store and writes with setAndCommit; a view that depends on now (e.g. "received today") declares transient: true so selecting it re-derives instead of restoring a stale answer. A view whose question includes an ordering (e.g. "oldest shipped first") declares it as sort: [{ field: 'statusChangedAt', order: 1 }] — selecting the view applies it, and un-sorting returns to it instead of the table default. A view is a complete state, sort included: switching tabs never carries the previous tab's ordering along — a view with no declared sort falls back to the table's defaultSortField, or to unsorted when there is none. Column layout and the user's own sort persist per view.
Column chooser
Lazy-loaded tables render a Columns toolbar button that opens a checkbox list of all data columns, letting the user show or hide each one. Declare rarely-needed columns with visible: false so they are available in the chooser without cluttering the default view:
{ name: this.translocoService.translate('CreatedAt'), field: 'createdAt', filterType: 'date', visible: false },Behavior notes:
- Hiding a column keeps its filter and sort — both stay visible on the chip bar, so nothing narrows the data invisibly.
- Choices persist per table in
localStorageunder`${stateKey}:columns`— deliberately durable even whenstateStorageis'session', since a column layout is a preference while filters are a transient working set. Only explicit user toggles are stored, so changing a column's declared default later flows through to users who never touched it. - Guards:
lockVisiblecolumns can't be toggled, and the last visible data column can't be hidden. Reset to default in the chooser restores the declared configuration.
Actions
Use the Action class to add buttons to each row:
{
actions: [
{ field: 'Details', icon: 'pi pi-pencil' },
{ field: 'Delete', icon: 'pi pi-trash' },
{ field: 'custom', name: 'Preview', icon: 'pi pi-eye', onClick: (e) => this.preview(e.id) },
],
}Built-in field values:
'Details'— navigates to the detail page'Delete'— opens a confirmation dialog and callsdeleteItemFromTableObservableMethod- Any other value — triggers the
onClickcallback
Action click payload
The onClick callback receives an ActionClickEvent carrying everything you need to react to the click — including the clicked element, so you can anchor an overlay or popover to it:
| Property | Type | Description |
|---|---|---|
id | number | The clicked row's id |
row | any | The full row object the action belongs to |
element | HTMLElement | The clicked action element — use as the popover/overlay anchor |
originalEvent | MouseEvent | The original DOM click event |
For example, to open a PrimeNG popover anchored to the clicked action:
{
field: 'preview', name: 'Preview', icon: 'pi pi-eye',
onClick: (e) => {
this.selectedRow = e.row;
this.previewPopover.show(e.originalEvent, e.element);
},
}Cell click
Set a column's onCellClick to react to clicks on the cell value (as opposed to an action icon) — the mirror of Action.onClick, but for plain value cells. Only columns that define it become clickable (they get a cursor + hover affordance), and the click stops propagation so it does not also trigger navigateOnRowClick. It is not applied to editable cells.
{
field: 'total', name: 'Total', filterType: 'numeric',
onCellClick: (e) => {
this.selectedRow = e.row;
this.itemsPopover.show(e.originalEvent, e.element);
},
}Cell click payload
The onCellClick callback receives a CellClickEvent:
| Property | Type | Description |
|---|---|---|
id | number | The clicked row's id (row[idField]) |
field | string | The clicked column's field |
row | any | The full row object |
value | any | The cell's raw value (row[field]) |
displayValue | string | The formatted text shown in the cell |
element | HTMLElement | The clicked <td> — use as the popover/overlay anchor |
originalEvent | MouseEvent | The original DOM click event |
element is captured at click time on purpose: anchor overlays with it rather than originalEvent.currentTarget, which the DOM resets to null once dispatch ends — so it would already be null inside an async handler that opens the popover after a fetch.
Row Selection
Set selectionMode="multiple" (or provide deleteListFromTableObservableMethod, which enables it automatically) to render a checkbox column. Clicking a checkbox only toggles the selection: on a navigateOnRowClick table it never navigates, and neither do action icons, editable cells or onCellClick cells.
Shift+click range selection
Click one checkbox, hold Shift, and click another: every row between them receives the state of the checkbox you clicked. Shift+checking selects the whole range; shift+unchecking clears it, which undoes an overshoot without clicking row by row.
The range anchor is the last checkbox you clicked, and a range only ever spans rows visible on the current page: if the anchor is no longer among them, the shift+click falls back to a plain toggle. A client-side sort that keeps both rows on screen ranges over the new visual order, which is the order you are looking at. Rows already in the target state are skipped, and every actual change emits onRowSelect/onRowUnselect exactly as a single click would.
SpiderlyDataView
Selector: spiderly-data-view
Card-based paginated list with custom rendering and filtering. Use this when you want a visual layout instead of a table.
Key Inputs
| Input | Type | Default | Description |
|---|---|---|---|
filters | DataViewFilter[] | [] | Filter definitions displayed above the card list |
getPaginatedListObservableMethod | (filter) => Observable<PaginatedResult> | — | Server fetch method |
rows | number | 10 | Page size |
items | any[] | — | Data items (only for non-lazy mode) |
Key Outputs
| Output | Payload | Description |
|---|---|---|
onLazyLoad | Filter | Fires on every lazy load (page change, filter change) |
Content Projection
Render each card using an ng-template with the #cardBody reference:
<spiderly-data-view
[getPaginatedListObservableMethod]="getProductListObservableMethod"
[filters]="filters"
>
<ng-template #cardBody [templateType]="templateType" let-item let-index="index">
<div class="p-4 border rounded">
<h3>{{item.name}}</h3>
<p>{{item.price | currency}}</p>
</div>
</ng-template>
</spiderly-data-view>The template context provides item (the data object) and index (row index).
Example
import { Component, OnInit } from '@angular/core';
import { TranslocoService } from '@jsverse/transloco';
import { ApiService } from 'src/app/business/services/api/api.service';
import {
DataViewFilter,
DataViewCardBody,
SpiderlyControlsModule,
SpiderlyDataViewComponent,
SpiderlyTemplateTypeDirective,
} from 'spiderly';
@Component({
selector: 'product-data-view',
templateUrl: './product-data-view.component.html',
imports: [SpiderlyTemplateTypeDirective, SpiderlyDataViewComponent, SpiderlyControlsModule],
})
export class ProductDataViewComponent implements OnInit {
templateType?: DataViewCardBody<Product>;
filters: DataViewFilter<Product>[];
getProductListObservableMethod = this.apiService.getProductPaginatedList;
constructor(
private apiService: ApiService,
private translocoService: TranslocoService,
) {}
ngOnInit() {
this.filters = [
{ label: this.translocoService.translate('Name'), type: 'text', field: 'name' },
{
label: this.translocoService.translate('Price'),
type: 'numeric',
field: 'price',
showMatchModes: true,
},
{
label: this.translocoService.translate('CreatedAt'),
type: 'date',
field: 'createdAt',
showMatchModes: true,
},
];
}
}For a quick-start version of this example, see the Frontend Customization guide.
DataViewFilter
| Property | Type | Description |
|---|---|---|
label | string | Filter label displayed above the input |
field | string | Data field to filter on |
filterField | string | Alternate field for filtering |
type | 'text' | 'date' | 'multiselect' | 'boolean' | 'numeric' | Filter input type |
showMatchModes | boolean | Show match mode dropdown (e.g., equals, greater than) |
matchModes | MatchModeCodes[] | Narrows the offered match modes; first is the default |
dropdownOrMultiselectValues | PrimengOption[] | Options for multiselect/dropdown filters |
Filter Type Behavior
| Type | Match Modes | Description |
|---|---|---|
text | Contains | Text search (contains by default) |
numeric | Equals, Greater Than, Less Than | Number comparison |
date | Equals, Greater Than, Less Than | Date comparison |
boolean | Equals | True/false selection |
multiselect | In | Multi-value selection |