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

InputTypeDefaultDescription
colsColumn[]Column definitions (see Column Configuration)
filtersFilterSourceThe page's filter store — the table's whole filter surface (see Filtering). Read once at init
viewsTableView[]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.
rowsnumberconfig defaultPage size
rowsPerPageOptionsnumber[][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
readonlybooleanfalseHides all action buttons
hasLazyLoadbooleantruetrue = server-side; false = client-side
itemsany[]Data rows (only when hasLazyLoad is false)
showAddButtonbooleantrueShow the "Add" button
showExportToExcelButtonbooleantrueShow the "Export" button
showReloadTableButtonbooleanfalseShow the reload-table button
showPaginatorbooleantrueShow the paginator. Pass only when hasLazyLoad is false
showCardWrapperbooleanfalseWrap the table in a card container
navigateOnRowClickbooleanfalseClicking a row navigates to details (using idField). Interactive cells — checkboxes, action icons, editable and onCellClick cells — keep their click and never navigate
additionalFilterIdLongnumberExtra filter ID appended to every request
defaultSortFieldstringSort 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
defaultSortOrder1 | -11Direction for defaultSortField

Key Outputs

OutputPayloadDescription
onLazyLoadFilterFires on every table load (sort, filter, page change)
onTotalRecordsChangenumberFires when the total record count changes
onRowSelectRowClickEventFires when a row is selected
onRowUnselectRowClickEventFires when a row is deselected
onIsAllSelectedChangeAllClickEventFires 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.

PropertyTypeDescription
namestringColumn header label
fieldstringData 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
filterIdstringId of the filter in the table's store this column stands for; needed only when it differs from field (see Filtering)
widthstringCSS length fixing the column's width, overriding the per-filterType default (e.g. '8rem')
dropdownOrMultiselectValuesPrimengOption[]Options a multiselect column maps its cell values through (id → label)
editablebooleanEnable inline editing for this column
showTimebooleanFor date columns: show time portion
decimalPlacesnumberFor numeric columns: decimal formatting
actionsAction[]Row action buttons (see Actions)
visiblebooleanWhether the column is initially rendered (default true; see Column chooser)
lockVisiblebooleanPin the column — always rendered, shown checked and disabled in the chooser
onCellClick(e: CellClickEvent) => voidPer-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 with Column.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.
  • operators on 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: false keeps a filter out of + Filter when a dedicated control on the page drives it through filters.get(id).
  • Late-arriving option lists (an async lookup) go in through store.setOptions(id, options); programmatic writes through store.setAndCommit(id, { operator, value }).
  • Applied filters persist per table under `${stateKey}:filters` in the stateStorage storage; 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 localStorage under `${stateKey}:columns` — deliberately durable even when stateStorage is '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: lockVisible columns 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 calls deleteItemFromTableObservableMethod
  • Any other value — triggers the onClick callback

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:

PropertyTypeDescription
idnumberThe clicked row's id
rowanyThe full row object the action belongs to
elementHTMLElementThe clicked action element — use as the popover/overlay anchor
originalEventMouseEventThe 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:

PropertyTypeDescription
idnumberThe clicked row's id (row[idField])
fieldstringThe clicked column's field
rowanyThe full row object
valueanyThe cell's raw value (row[field])
displayValuestringThe formatted text shown in the cell
elementHTMLElementThe clicked <td> — use as the popover/overlay anchor
originalEventMouseEventThe 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

InputTypeDefaultDescription
filtersDataViewFilter[][]Filter definitions displayed above the card list
getPaginatedListObservableMethod(filter) => Observable<PaginatedResult>Server fetch method
rowsnumber10Page size
itemsany[]Data items (only for non-lazy mode)

Key Outputs

OutputPayloadDescription
onLazyLoadFilterFires 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

PropertyTypeDescription
labelstringFilter label displayed above the input
fieldstringData field to filter on
filterFieldstringAlternate field for filtering
type'text' | 'date' | 'multiselect' | 'boolean' | 'numeric'Filter input type
showMatchModesbooleanShow match mode dropdown (e.g., equals, greater than)
matchModesMatchModeCodes[]Narrows the offered match modes; first is the default
dropdownOrMultiselectValuesPrimengOption[]Options for multiselect/dropdown filters

Filter Type Behavior

TypeMatch ModesDescription
textContainsText search (contains by default)
numericEquals, Greater Than, Less ThanNumber comparison
dateEquals, Greater Than, Less ThanDate comparison
booleanEqualsTrue/false selection
multiselectInMulti-value selection