[data-reveal]{opacity:1!important;transform:none!important}
Components

Tables

Render a paginated, filterable, sortable CRUD table from a DataController with TableView.

A table is two pieces. A DataController declares the data shape and its operations — list, filter, sort, search, create/edit/delete, export, archive — once, with decorators; that whole backend side (route generators, field decorators, model wiring) is its own reference, Data controller. A TableView component points a page at that controller and renders the screen: toolbar, columns, forms, and pagination are wired for you, so a standard CRUD screen needs no Vue and no hand-written routes. For a static form or one-off list endpoint, use Pages & components instead.

What it renders

TableView produces a full table screen:

  • Toolbar — the caption heading, a global search box (when @Searchable fields exist — it searches from 2 characters), a filter per filterable column, a column-visibility menu, an optional tab strip (tabs), a display switcher when more than one display is configured, an Export button (when the export routes are present), any customButtons, and an Add button (when the add action is enabled).
  • Columns — one per @Column, headed by name; a @Sortable header toggles ascending/descending. Each cell renders through its DataType (a badge, date, thumbnail, relation chip), not raw text — see Data types.
  • Rows — one per record, keyed by rowIdKey (default _id). With hasSelection on, a leading checkbox column enables multi-select and bulk delete/archive/restore.
  • Row actions — a per-row menu of the enabled actions (edit, details, duplicate, delete, copy link, archive/restore, plus custom entries), hidden or disabled per row by their rule.
  • Pagination — a footer driven by the list/count routes.

Editing happens in a form container — drawer, modal, or page — built automatically from the controller's @Column fields. See Forms.

Usage

Model the data first: declare the controller, its routes, and its field decorators as shown in Data controller — this example reuses the taskDataAPI controller built there.

Pass the controller class itself (not an instance) to TableView. Columns come from the controller's @Column decorators — you do not redeclare them on the page.

src/pages/tasks.ts
import {
  PageController,
  RegisterPage,
  pagesCategory,
} from "@antelopejs-private/cms/interfaces/cms/page";
import { TableView } from "@antelopejs-private/cms/interfaces/cms-base/table-view";
import { taskDataAPI } from "../data-api/tasks";

@RegisterPage()
export class TasksPage extends PageController("tasks", {
  displayName: "Tasks",
  icon: "i-ph-table",
  category: pagesCategory,
}) {
  static table = TableView(taskDataAPI, {
    caption: "Tasks",
    labelKey: "name", // titles the drawer/modal/page form
    archiveMode: true, // requires an @ArchiveField on the controller
    formContainer: { type: "drawer" },
    rowActions: {
      add: true,
      edit: true,
      details: true,
      hasSelection: true,
      delete: { isEnabled: true, rule: { field: "status", notEquals: "done" } },
    },
  });
}

TableView options

A frontend layer can add alternate displays (kanban, cards, calendar…) beyond the built-in grid — see TableView displays — and make table actions react to other components via Component events.

TableView(controller, options):

OptionTypeDefaultPurpose
captionstringHeading above the table.
rowIdKeystring"_id"Field used as row identity for actions and realtime.
labelKeystringField that titles the form container ("Edit Buy milk").
rowActionsTableViewRowActionOptionsWhich per-row actions to show and their rules (see Row actions).
customButtonsCustomButton[]Extra toolbar buttons with an ActionTarget.
formContainer{ type: "drawer" | "modal" | "page" }pageWhere the add/edit/view form opens (see Form containers).
archiveModebooleanfalseReplace delete with archive/restore; requires an @ArchiveField.
defaultFiltersArray<{ accessorKey; value?; mode }>Filters pre-applied and visible when the table opens.
defaultSort{ field; desc? }Initial sort when the user has no saved preference.
queryParamFiltersRecord<string, { field; mode? }>Map URL query params to hidden filters (not shown, not persisted).
routeParamFiltersRecord<string, { field; mode? }>Map CMS route params (page-slug tokens like :id) to hidden filters — for detail pages scoped by a path param.
strictRuleValidationbooleanfalseWhen true, reject the whole request if any row fails a rule; when false, process only eligible rows.
tabsTableViewTab[]Tab strip above the table; each tab applies hidden filters. An implicit "all" tab is added automatically.
realtimebooleantrueBroadcast row mutations and presence to other viewers (opt-out).
kanbanKanbanOptionsEnable a kanban display; groupByField must be a non-multiple SelectType, BooleanType, or StatusType column.
displaysTableViewDisplayOption[]Additional custom displays beyond the built-in table (and kanban).
defaultDisplaystring"table"Display shown when the user has no preference. Supersedes the deprecated defaultViewMode.
guardsTableViewGuardsServer-side checks before mutating actions (see Guards).
bypassTenantAccessGatebooleanfalseKeep the table readable while a tenant access gate denies the tenant — for tables on recovery surfaces (invoices on a suspended tenant's billing page). Covers view, list and select only: every write, and export (which queues a job and delivers a file), still asserts the gate. Action permission checks apply on top. Latched controller-wide: once one TableView() registration of a controller declares it, a later registration without the flag cannot silently re-gate (or un-gate) the shared routes — so a table mounted on several pages shares the opening, and mounting it both ways is warned about.
queryParamFilters and routeParamFilters also seed form submit defaults: a form opened from a filtered view inherits the parent token (e.g. a :projectId route param flows into the new row's projectId), resolved at submit time and dropped if absent.

Features

Filtering, search, and sorting

Filtering, search, and sorting need no page-side configuration — they come from the controller's decorators (Data controller): a filterable column gets a filter offering its DataType's operators, @Searchable fields feed the search box, and @Sortable columns get sortable headers.

What the page declares are pre-applied filters, in two forms. defaultFilters applies filters when the table opens, visible in the toolbar. tabs adds a tab strip where each tab applies its own filters; an implicit "all" tab is added automatically.

A tab pairs an id and a label (plus optional icon, iconColor, textColor) with its filters; each filter names an accessorKey, a mode, and usually a value. The tabs array below sits inside the TableView(controller, { ... }) options object:

src/pages/tasks.ts
tabs: [
  { id: "pending", label: "Pending", icon: "i-ph-clock",
    filters: [{ accessorKey: "status", value: "pending", mode: "is" }] },
  { id: "high", label: "High priority", icon: "i-ph-fire", iconColor: "error",
    filters: [{ accessorKey: "priority", value: "high", mode: "is" }] },
],

Row actions

rowActions enables the per-row menu. Each built-in action takes a boolean or a RowActionConfig with optional isEnabled, isVisible, and rule:

ActionNeeds routeEffect
addnewToolbar Add button (opens the new form).
editeditOpen the edit form.
detailsgetOpen the read-only view form.
duplicatenewOpen the new form pre-filled from the row (?duplicate=<id>).
deletedeleteDelete the row.
copyLinkgetCopy a deep link to the row.
archive / restorearchive / restoreSoft-archive / restore (archive mode).
hasSelectionEnable the selection checkbox column and bulk actions.
customArray of custom actions (below).

A rule hides or disables the action per row by evaluating against the row's fields. Field operators are equals, notEquals, in, notIn; combine them with and, or, not:

src/pages/tasks.ts
rowActions: {
  add: true,
  edit: { isEnabled: true, rule: { field: "status", notEquals: "completed" } },
  delete: {
    isEnabled: true,
    rule: {
      and: [
        { field: "status", notEquals: "completed" },
        { field: "priority", notEquals: "high" },
      ],
    },
  },
  hasSelection: true,
},

Rules are enforced server-side as well as in the UI:

  • A delete/edit/archive/restore request for an ineligible row is filtered out.
  • A non-strict edit whose single row is ineligible returns 403.
  • With strictRuleValidation: true, the whole request is rejected 400.
  • A rule declared on a row action is registered once per controller and applies to every table view on it.

Custom row actions add menu entries that fire an ActionTarget rather than a built-in CRUD operation — the custom array below sits inside the TableView's rowActions:

src/pages/tasks.ts
custom: [
  {
    label: "Mark complete",
    icon: "i-ph-check-circle",
    target: { type: "api", url: "/api/task/complete", method: "POST",
              successMessage: "Task completed" },
    rule: { field: "done", equals: false },
  },
],

A target can open a component in a drawer or a modal, navigate to an internal page or an external URL, call an API endpoint (with an optional confirmation dialog), or run an async export job — each ActionTarget type and its fields are in Actions & reactivity. Custom toolbar buttons (customButtons) take the same target, plus a variant and a color.

A custom toolbar button can also declare a permission: either the id of one of the table's own actions (e.g. "add") or an Action from another component (otherTable.getAction("add")). The button is stripped from the serialized options when the caller lacks the permission — same mechanism as the built-in actions. The route the button calls still needs its own guard (@AuthUserWithPermission(...)).

Archive mode

Set archiveMode: true and decorate a boolean field with @ArchiveField(); archive mode swaps delete for archive/restore.

  • TableView throws at registration if archiveMode is set without an @ArchiveField on the controller.
  • An @ArchiveField without archiveMode is accepted: it enables the archive filter on list without swapping the actions.
  • Archive mode registers three permissions — archive, restore, viewArchived.

It also injects default rules so the right action shows per row: edit/duplicate/archive apply only when the archive field is not true; restore/delete apply only when it is true. Your own rule on any of these wins.

The viewArchived permission is enforced server-side: a client sending showArchived=true without it gets a hard 403, not a silent empty list. Archive and restore are bulk PUT operations over selected ids.

Kanban display

Setting kanban offers the built-in kanban board as an alternate display — cards grouped into columns by a field, with drag-and-drop between columns writing the group field back:

src/pages/tasks.ts
static board = TableView(taskDataAPI, {
  caption: "Tasks",
  labelKey: "name",
  rowActions: { add: true, edit: true, delete: true },
  kanban: {
    groupByField: "status", // default grouping; users can switch to any eligible field
    cardFields: ["priority", "assignee"],
  },
  defaultDisplay: "kanban",
});

KanbanOptions:

OptionTypeDefaultPurpose
groupByFieldstringrequiredThe column cards group by. Must be a non-multiple SelectType, a BooleanType, or a StatusType column — anything else throws at registration.
cardFieldsstring[]Fields shown on the default card under the title (labelKey), each rendered by its column DataType.
cardComponentComponentReplace the default card with your own component (resolved by name from the global registry). It receives item, columns, and groupValue, and can emit edit / delete.
draggablebooleantrueAllow dragging cards between columns to update the group field.
columnMaxHeightstring"60vh"Column height before the card list scrolls.

Kanban manages its own data (one query per column) and hides the filter tabs while active. Beyond kanban, a frontend layer can register entirely new displays — cards, calendar, gallery — that inherit the whole toolbar: see TableView displays.

Form containers

formContainer.type controls where the add/edit/view form opens:

  • page (default) — TableView auto-generates hidden sub-pages at <slug>/new, <slug>/:id/edit, and <slug>/:id/view, each gated by the matching permission. Override slugs/titles via formContainer.pages (new/edit/view); custom edit/view slugs must contain :id. Set customPage: true to skip auto-generation and register the page yourself.
  • drawer — slides in from the side.
  • modal — opens centered; accepts a size.

Forms are built from the controller's @Column fields, honoring @Access, @Mandatory, readonlyBehavior, and @ColumnGroup. See Forms.

Export jobs

When the ExportRoutes are present, the Export button runs an asynchronous job, not a synchronous download: it exports the @Exported fields for the current filter/sort in the background, shows progress, and downloads the file on completion. Exports are CSV with download delivery — the only exporter and deliverer shipped.

  • A job that exceeds its execution limit (10 minutes by default) is marked failed.
  • Artifacts older than 24 hours are swept by a built-in cron.
  • Jobs are private to the requesting user and tenant.

The engine behind this is reusable for your own exports — see Backend services.

Events & realtime

With realtime on (the default), the table broadcasts row mutations (created / updated / deleted) to other viewers — their tables refresh live — and tracks edit presence: opening a row's edit form acquires a row lock, and other viewers see who is editing (with a warning dialog before a concurrent edit). Set realtime: false to opt a table out.

The rendered table also emits component events — TableViewEvents: ROW_CLICK, ROW_SELECT, ROW_ADD, ROW_EDIT, ROW_DELETE, FILTER_CHANGE, SORT_CHANGE, EXPORT — that other components can react to through watch-actions (Actions & reactivity). On the function side, TableViewFunctions ships one watchable id, CUSTOM_PAGE_FORM_SUCCESS (a url, optional preserveQuery): after a custom-page form succeeds, it navigates back to the table, restoring filters.

Guards

guards runs a server-side check before a mutating action commits. this inside a guard is the data controller instance, so @Model-bound models are reachable. Throw with assert(...) from @antelopejs/interface-api-util to abort with an HTTP status and an i18n message key:

src/pages/tasks.ts
import { assert } from "@antelopejs/interface-api-util";

guards: {
  edit: (ctx, { id, body, current }) => {
    assert(current.status !== "locked", 403, "$tasks.locked");
  },
  delete: (ctx, { ids }) => { /* … */ },
},

Guards exist for edit, delete, archive, restore, and new, each receiving the relevant ids/body (and, for edit, the current row).

Permissions

Every table action registers its own permission, named <componentPermissionId>.<actionId>. The component permission id is the page's full id followed by every field/child id in the table's position; each action appends its id — list, select (granted by default), add, edit, view, delete, export, plus archive/restore/viewArchived in archive mode. These same per-action permissions gate the row actions.

The table is filtered per user: it strips the forms and row actions the caller lacks before the page renders, and the matching routes reject unauthorized calls server-side. As a special case, if the edit action has no rule, the table force-disables details (the edit form already covers viewing). See Auth & permissions.