Table Builder for Quasar

πŸ‡ͺπŸ‡Έ VersiΓ³n en espaΓ±ol | πŸ‡§πŸ‡· VersΓ£o em portuguΓͺs

npm version license Vue Quasar TypeScript Node

Quasar extension for declarative tables in Vue 3 + Quasar apps: listings with filters (via Form Builder), server/client pagination, backend sorting, row selection, column slots, and cancellable queries.

Table of contents

In 30 seconds

Recommended flow:

  1. Define filter types F and row type R.
  2. Configure filter fields with Form Builder (defineForm or inline filters.form).
  3. Create the table with defineTable<F, R>({ ... }) from useTableBuilder().
  4. Render <TableBuilder ref="tableRef" :config="config" /> with #column-* and #toolbar slots.

The extension registers boot and CSS in the host project automatically after quasar ext add.

What it is and what it includes

Table Builder orchestrates listings: toolbar, Form Builder filters, table, pagination, and fetch. It does not replace Form Builder β€” filters are always an FB form.

Requirements and compatibility

Item Value
Node >= 20.0.0
Quasar ^2.6.0
Vue ^3.4.18
Pinia ^2.0.11 | ^3.0.0
DataTable @benjaminor-dev/datatable ^0.1.0 β€” required
Form Builder @benjaminor-dev/form-builder ^0.9.0 β€” required
moment ^2.30.1 (peer; used when formatting filter chips with dates/times)
Package format ES modules
Recommended CLI @quasar/app-vite ^2.x or ^3.x

The host Pinia boot must run before Form Builder and Table Builder.

Install in a Quasar app

Install DataTable and Form Builder before Table Builder (required peers):

quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder
quasar ext add @benjaminor-dev/table-builder

Remove:

quasar ext remove @benjaminor-dev/table-builder

What the extension adds to the host

Resource npm path
Store boot ~@benjaminor-dev/quasar-app-extension-table-builder/boot/store
Table Builder styles ~@benjaminor-dev/quasar-app-extension-table-builder/main.css
DataTable styles ~@benjaminor-dev/quasar-app-extension-datatable/main.css (peer; also registered by this extension)
Scaffold scripts bor:make-table β†’ scripts/bor/bor-make-table.mjs
Host defaults boot src/boot/bor/bor-table-builder-defaults.ts β€” created on install; Skip/Overwrite if it already exists

Install (quasar ext add / invoke): creates src/boot/bor/bor-table-builder-defaults.ts (or .js) and registers it in quasar.config β†’ boot: [] automatically. No manual editing needed in a normal install. Package boot (boot/store) and CSS are injected by the extension on every dev/build.

Remove (quasar ext remove): removes the scaffold script, defaults boot, its quasar.config entry, and bor:make-table in package.json (if it still points to the install file).

If you delete the defaults file, the app keeps the library built-ins (fetchOnMount: false, pageSize: 10, etc.).

Boot order (this extension): Pinia β†’ DataTable (CSS) β†’ Form Builder β†’ TB store boot β†’ bor/bor-table-builder-defaults β†’ rest.

Global defaults (boot)

Avoid repeating filters, pagination, fetchOnMount, persistData, and abort in every defineTable. Priority: defineTable β†’ boot (configureTableBuilder) β†’ library built-ins.

After quasar ext add, review src/boot/bor/bor-table-builder-defaults.ts β€” template with fetchAbort and commented options (pagination, persistData). Built-in defaults: JSDoc @default in types.

// src/boot/bor/bor-table-builder-defaults.ts (excerpt)
import { configureTableBuilder } from "@benjaminor-dev/quasar-app-extension-table-builder";

configureTableBuilder({
  filters: { layout: "drawer", openFiltersOnMount: false, showFilterChips: true },
  fetchOnMount: false,
  // persistData: true, // uncomment for global SPA cache
  filterUi: {
    buttonStyle: {
      light: {
        filtersColor: "teal",
        filtersVariant: "outline",
        searchColor: "primary",
        searchVariant: "outline",
        clearVariant: "flat",
        submitColor: "primary",
        submitVariant: "unelevated",
        stopColor: "negative",
        stopVariant: "unelevated",
      },
      dark: {
        filtersColor: "teal",
        filtersVariant: "unelevated",
        searchColor: "primary",
        searchVariant: "unelevated",
        clearColor: "white",
        clearVariant: "flat",
        submitColor: "primary",
        submitVariant: "unelevated",
        stopColor: "negative",
        stopVariant: "unelevated",
      },
    },
    panelHeader: {
      light: { color: "primary", textColor: "white" },
      dark: { color: "primary", textColor: "white" },
    },
    chips: {
      light: { color: "primary", textColor: "white" },
      dark: { color: "primary", textColor: "white" },
    },
    chipsPanel: {
      light: {
        borderColor: "rgba(25, 118, 210, 0.16)",
        backgroundColor: "rgba(25, 118, 210, 0.04)",
      },
      dark: {
        borderColor: "rgba(255, 255, 255, 0.12)",
        backgroundColor: "rgba(255, 255, 255, 0.04)",
      },
    },
  },
  // optional fetchAbort β€” see file generated on install
});
Boot option Description
filters Panel defaults (no form; the form stays in each table)
filterUi.buttonStyle Filters / Search / Clear / Stop colors and variants per theme (light / dark)
filterUi.panelHeader Drawer header (color, textColor Quasar per theme)
filterUi.chips Active filter chips (color, textColor per theme)
filterUi.chipsPanel Chip container border and background (borderColor, backgroundColor CSS per theme)
pagination pageSize, placement, scrollOnPageChange, pageSizeOptions
fetchOnMount Initial load on mount (library default: false)
persistData Keep Pinia session when the table unmounts
onFetchError Global fetch error hook
fetchAbort Optional global abort (useTableFetchAbortStore or custom adapter) + allowUserCancel

Use configureTableBuilderDefaults if you only need defaults without registering fetchAbort.

filterUi priority: defineTable({ filterUi }) β†’ boot β†’ built-in. Active theme follows host $q.dark.

Detailed cancellation: Β§ Cancellation.

Scaffold commands in the host

The extension registers bor:make-table in the host (scripts/bor/bor-make-table.mjs β†’ runBorMakeTable in /scaffold). Run npm run bor:help (Form Builder) to see all installed generators.

Tables (bor:make-table)

npm run bor:make-table -- <nombre-o-ruta>

Example β€” create UsuariosTable.vue:

npm run bor:make-table -- usuarios

Result:

Naming rules:

Quick start

<script setup lang="ts">
import { TableBuilder, useTableBuilder } from "@benjaminor-dev/quasar-app-extension-table-builder";

type Filters = { search: string | null; role: string | null };
type Row = { id: number; name: string; role: string };

const { defineTable, useTableRef } = useTableBuilder();
const tableRef = useTableRef();

const config = defineTable<Filters, Row>({
  tableName: "usuarios",
  title: "Users",
  columns: [
    { name: "name", label: "Name", field: "name", align: "left", sortable: true },
    { name: "role", label: "Role", field: "role", sortable: true },
  ],
  pagination: { mode: "server", pageSize: 10, placement: "auto", defaultSort: { sortBy: "name" } },
  fetchOnMount: false,
  filters: {
    layout: "drawer",
    minActiveFilters: 1,
    form: {
      fields: [
        { type: "InputSearch", model: "search", label: "Search" },
        {
          type: "InputSelect",
          model: "role",
          label: "Role",
          props: { options: [{ label: "Admin", value: "admin" }], clearable: true },
        },
      ],
    },
  },
  fetch: async ({ filters, page, pageSize, sortBy, descending, signalAbort }) => {
    const params = new URLSearchParams({
      page: String(page),
      pageSize: String(pageSize),
    });
    if (sortBy) {
      params.set("sortBy", sortBy);
      params.set("descending", String(descending));
    }
    if (filters.search) params.set("search", filters.search);
    if (filters.role) params.set("role", filters.role);

    const res = await fetch(`/api/users?${params}`, { signal: signalAbort });
    const data = await res.json();
    return {
      rows: data.items,
      pagination: { total: data.total, page, pageSize },
    };
  },
});
</script>

<template>
  <TableBuilder ref="tableRef" :config="config">
    <template #column-name="{ row, text }">
      <strong>{{ text || row.name }}</strong>
    </template>
  </TableBuilder>
</template>

Language (i18n)

Built-in en, es, and pt. By default toolbar, drawer, empty states, and skeleton texts follow Quasar language ($q.lang.isoName); if Quasar has no language, fallback is en.

Locale resolution: boot locale β†’ $q.lang.isoName prefix (es* β†’ es, pt* β†’ pt) β†’ en.

Option When to use
framework.lang in quasar.config.ts Recommended β€” aligns Table Builder with the rest of Quasar
locale: 'es' in boot Pin a language (en | es | pt); ignores $q.lang changes
messages: { search: '…' } in boot Change one string without pinning locale

Quasar config example (Spanish for the whole app):

// quasar.config.ts
framework: {
  lang: "es",
},

Boot template (after install):

configureTableBuilder({
  // locale: "es", // pins only Table Builder language
  // messages: { search: "Query" },
});

i18n keys: buttons (filters, search, refresh, stopQuery, clear), drawer (filtersPanelTitle), empty states (emptyNotQueried*, noResults*, fetchCancelled*), search tooltip (searchGateDefault), skeleton (loading*Title, loading*Hint), and aria (filtersAppliedAria, removeFilterAria with {label}).

Message priority: boot messages merges over the active locale built-in catalog.

TypeScript in your IDE

Table config is fully typed end to end β€” let your IDE guide you.

If an option does not appear in autocomplete, check the import from /types before searching this README.

defineTable configuration

Main fields of defineTable<F, R>():

Field Description
tableName Unique identifier (filter form name derived if you omit formName).
title, hint, prependIcon, helpTooltip Toolbar presentation.
columns Column definitions (name, label, field?, format, sortable, align, …). sortable defaults to false.
selection single | multiple for checkboxes; omit to disable. Ref: selectedRows, clearSelection().
pagination Mode, pageSize, placement, and defaultSort (server).
filters Form Builder panel or false.
fetch Async function returning rows (and metadata in server mode).
fetchOnMount Initial load on mount (respects search rules).
persistData true keeps the Pinia session when the table unmounts (navigate away and back in the SPA).
persistDataConfig Fine tuning (reuseOnMount). Wins over persistData if you set both.
onBeforeFetch, onAfterFetch, onFetchError Query lifecycle hooks.
allowUserCancel Stop / cancelFetch() for the user (boot override). Default: false. Global boot: only via fetchAbort.allowUserCancel.
filterUi Override buttons, drawer header, chips, and chip panel per theme (light / dark).

Columns (columns)

defineTable does not require a fixed order: the runtime normalizes the list when resolving config.

Behavior Detail
# column If you do not define name: "index", it is injected at the start with label: "#" and field: "index". Compact width by default. fetch must not return it: computed per page (1, 2, … by offset).
field Optional on slot-only columns (#column-{name}): omit if the value comes from the slot. If declared, used for value/text and sorting in client.
label Optional on slot-only columns; if missing, the header is empty.
align Defaults to "left". Use "center" or "right" where appropriate (e.g. icons or actions).
Order Other columns (including actions) render in the same order as in columns.
sortable Defaults to false. Set sortable: true on columns you sort; in server use sortBy / descending in fetch.

To customize the index column (width, alignment, hide it), declare it explicitly with name: "index".

Layout

Value Behavior
drawer (default) Form in side panel; toolbar with Filters + search/refresh group.
inline Panel above the table with Filters header, form, Search / Clear. Chips (if enabled) go below the form.

Rules to enable search (AND)

filters: {
  minActiveFilters: 1,
  canSearch: (filters) => Boolean(filters.search?.trim()) || filters.role != null,
  searchDisabledMessage: "Enter search or role to query.",
  showFilterChips: true,
  form: { fields: [...] },
}
Option Default Description
minActiveFilters 0 Minimum filters with an active value.
canSearch true boolean or (filters: F) => boolean.
searchDisabledMessage "Adjust filters to query." Tooltip when canSearch or minActiveFilters fails.
showFilterChips true Chips from the last successful search.
resetFiltersOnMount false Restore filters on mount.
openFiltersOnMount false Open drawer on mount (true | "desktop").

Chips reflect the last successful search snapshot, not the live form.

Filter form (filters.form)

Same shape as Form Builder defineForm:

filters: {
  form: {
    fields: [{ type: "InputSearch", model: "search", label: "Search" }],
    defaultValues: { role: "admin" },
  },
}

You can also pass the result of defineForm(...) if you prefer defining the form outside defineTable.

Toolbar with drawer (merged group)

An outline QBtnGroup groups toolbar buttons only when Filters and Search/Stop share the outline variant. If you mix variants (e.g. outline + flat), they render as separate buttons.

Filter UI per theme (filterUi)

Boot block What it controls
buttonStyle Filters / Search / Clear / Stop buttons (*Color, *Variant per theme)
panelHeader Drawer header (color, textColor Quasar)
chips Active filter QChip (color, textColor)
chipsPanel Chip container border and background (borderColor, backgroundColor CSS)
filterUi: {
  buttonStyle: {
    light: { filtersColor: "teal", filtersVariant: "outline", searchVariant: "outline" },
    dark: { filtersColor: "teal", filtersVariant: "unelevated", searchVariant: "unelevated" },
  },
  panelHeader: { light: { color: "primary", textColor: "white" }, dark: { color: "primary", textColor: "white" } },
  chips: { light: { color: "primary", textColor: "white" }, dark: { color: "primary", textColor: "white" } },
},

Pagination

pagination: {
  mode: "server",
  pageSize: 25,
  placement: "auto",
  scrollOnPageChange: "auto",
  defaultSort: { sortBy: "name" },
}
placement Behavior
bottom Bottom only (default).
top Top only.
both Top and bottom.
auto Bottom always; top only when the current page shows β‰₯15 visible rows (counts rendered rows, not the rows-per-page selector value).
scrollOnPageChange Behavior
auto (default) Scroll to title on mobile or when pagination is bottom-only.
true Always after changing page.
false Never.

| defaultSort | Initial order in server mode (sortBy, descending?). Ignored in client unless QTable receives initial state via internal pagination. |

server mode: fetch must return { rows, pagination } with at least total, page, and pageSize. from, to, and lastPage are optional (the runtime computes them if missing). Clicking a sortable header re-queries with sortBy and descending (page 1).

client mode: fetch can return R[] or { rows }; QTable paginates and sorts in memory.

Sorting

Mode Behavior
client Set sortable: true on columns you want. QTable sorts loaded rows without a new fetch.
server Set sortable: true on columns your API supports. Clicking the header runs fetch on page 1 with sortBy (column name) and descending.

Initial server order:

pagination: {
  mode: "server",
  defaultSort: { sortBy: "name", descending: false },
}

sortBy and descending also reach onBeforeFetch in the same shape as in fetch.

Row selection

const config = defineTable<Filters, Row>({
  // ...
  selection: "multiple", // or "single"
});
Value UI
"multiple" Checkbox per row
"single" Radio per row
Omit / false No selection

Selection is cleared after each successful fetch (search, pagination, sorting, or reload).

In script, read rows from the ref:

const tableRef = useTableRef();
const selectedCount = computed(() => tableRef.value?.selectedRows?.length ?? 0);

In template, use a computed (do not access tableRef.selectedRows directly: vue-tsc does not unwrap the ShallowRef in the template).

fetch, cancellation, and hooks

fetch context

type TableFetchContext<F> = {
  filters: F;
  page: number;
  pageSize: number;
  sortBy: string | null;
  descending: boolean;
  signalAbort?: AbortSignal;
};

Returns rows (and metadata in server mode):

// client
fetch: async ({ filters }) => ({ rows: await api.list(filters) }),

// server
fetch: async ({ filters, page, pageSize, sortBy, descending }) => ({
  rows: items,
  pagination: { total, page, pageSize },
}),

Cancellation β€” pick a path

Cancellation does not go through onFetchError.

Who can stop the query?

By default nobody (no Stop button). Enable it in boot (fetchAbort.allowUserCancel) or per table (defineTable.allowUserCancel + signalAbort in fetch).

Priority: defineTable.allowUserCancel β†’ fetchAbort.allowUserCancel (boot) β†’ false.

Boot fetchAbort.allowUserCancel Table Stop / cancelFetch()
true (not set) βœ… all
true false ❌ that table only
false / no boot true βœ… that table only
false (not set) ❌ none
// Boot β€” all tables with Stop (host global abort)
configureTableBuilder({
  fetchAbort: {
    allowUserCancel: true,
    getSignal: () => useAbortStore().getSignal(),
    abort: () => useAbortStore().abort(),
  },
});

// One table without Stop even if boot has it enabled
defineTable({ tableName: "Logs", allowUserCancel: false, ... });

// Only this table with Stop (portable Path A)
defineTable({
  tableName: "Usuarios",
  allowUserCancel: true,
  fetch: ({ signalAbort, ...ctx }) => UsersService.list(ctx.filters, signalAbort),
});

A new search or page change still aborts the previous request even when Stop is disabled (avoids overlaps).

Path A β€” portable (default)

Table Builder creates an AbortController per query. Forward signalAbort to the HTTP client in fetch:

fetch: async ({ filters, page, pageSize, signalAbort }) => {
  const res = await api.get("/users", { params: { filters, page, pageSize }, signal: signalAbort });
  return { rows: res.data.items, pagination: res.data.meta };
},

If the service lives outside the component, pass it as an argument:

fetch: ({ filters, signalAbort }) => UsersService.list(filters, signalAbort),

Recommended with multiple tables in parallel (each query has its own signal).

Path B β€” host global abort (no signalAbort in each table)

If your app already centralizes cancellation (Pinia + axios that auto-injects signal on each request), register the adapter once in the host boot.

Built-in option β€” extension Pinia store (useTableFetchAbortStore):

configureTableBuilder({
  fetchAbort: {
    useStore: true,
    allowUserCancel: false, // recommended: false
  },
});

Inject useTableFetchAbortStore().getSignal() in your HTTP layer (axios, etc.).

Custom option β€” host-owned store:

// src/boot/table-builder.ts
import { boot } from "quasar/wrappers";
import {
  configureTableBuilder,
  defaultIsTableFetchAbortError,
} from "@benjaminor-dev/quasar-app-extension-table-builder";
import useAbortStore from "@/stores/config/useAbortStore";

export default boot(() => {
  configureTableBuilder({
    fetchAbort: {
      allowUserCancel: false,
      getSignal: () => useAbortStore().getSignal(),
      abort: () => useAbortStore().abort(),
      isAbortError: (error) =>
        defaultIsTableFetchAbortError(error) ||
        (error instanceof Error && error.message === "canceled"),
    },
  });
});

Your HTTP layer must still inject the same store signal when the request has no explicit one (as in ApiUtils.makeNewConfig).

Then the table fetch does not need to use signalAbort:

fetch: ({ filters, page, pageSize }) =>
  UsersService.index(filters, { page, pageSize }),

Table Builder will call the store abort() when cancelling or starting another query; axios will catch it as before.

Note: a global abort store means one active abortable query in the app. With multiple tables loading at once, use Path A.

Hooks (optional control)

Hook When Behavior
onBeforeFetch Before starting Return false to skip the query (pre-validation).
onAfterFetch After success Rows already normalized (index included).
onFetchError Real error (network, HTTP, exception) If defined, there is no default notify.

Cancellation (AbortError, axios canceled, …) does not trigger onFetchError.

Example β€” filter validation errors (adapt to your API contract):

import { selectFormStore } from "@benjaminor-dev/quasar-app-extension-form-builder";

onFetchError: (error, ctx) => {
  const raw = (error as { response?: { data?: { errors?: Record<string, string | string[]> } } })
    .response?.data?.errors;
  if (!raw) {
    $q.notify({ type: "negative", message: "Error fetching data." });
    return;
  }

  selectFormStore(`table:${tableName}:filters`).setErrors(
    Object.fromEntries(
      Object.entries(raw).map(([key, value]) => [
        key,
        Array.isArray(value) ? value[0] : String(value),
      ]),
    ),
  );
},

UI states after query

Slots

<TableBuilder :config="config">
  <template #toolbar>
    <q-chip v-if="selectedCount > 0" dense color="primary">
      {{ selectedCount }} selected
    </q-chip>
    <q-btn outline label="Export" />
  </template>

  <template #column-actions="{ row, text, column, value }">
    <q-btn flat icon="edit" @click="edit(row)" />
  </template>
</TableBuilder>
Slot Props
toolbar β€” (extra actions to the right of the Filters group)
column-{name} row, column, value, text, index

Typed helper: tableColumnSlotName("actions") β†’ "column-actions".

TableBuilder ref

const tableRef = useTableRef();

await tableRef.value?.reload();
await tableRef.value?.reload({ resetPage: true });
await tableRef.value?.reload({ resetFilters: true });

tableRef.value?.openFilters();
tableRef.value?.resetFilters();
tableRef.value?.cancelFetch();
tableRef.value?.clearSelection();
await tableRef.value?.invalidate();

if (tableRef.value?.loading) { /* ... */ }
const selected = tableRef.value?.selectedRows ?? [];
Method / ref Description
reload(options?) Re-runs fetch. resetFilters: true restores filters and goes to page 1 (unless explicit resetPage: false).
resetFilters() Restores filters to defaults (without reloading).
openFilters() / closeFilters() Filter drawer.
cancelFetch() Cancels the in-flight fetch (abort of active signalAbort).
clearSelection() Deselects all rows.
invalidate() Clears rows and metadata in Pinia. Useful before reload() after external CRUD if you do not use patchItems.
loading true while a fetch is in progress.
selectedRows Selected rows when selection is enabled (cleared after each successful fetch).

Exported TypeScript types

From the main entrypoint and /types:

import type {
  ConfigureTableBuilderOptions,
  TableFilterUiDefaults,
  TableFilterChipStyleConfig,
  TableFilterChipsPanelStyleConfig,
  TableFilterPanelHeaderStyleConfig,
  TableBuilderConfig,
  TableBuilderResolvedConfig,
  TableFiltersConfigInput,
  TableSearchGateFn,
  TableColumnDef,
  TableColumnSlotProps,
  TablePaginationConfig,
  TablePaginationPlacement,
  TableDefaultSort,
  TableBuilderSlots,
} from "@benjaminor-dev/quasar-app-extension-table-builder/types";

import type {
  TableRow,
  TableFilters,
  TableFetchContext,
  TableFetchResult,
  TablePaginationMeta,
  TablePaginationMetaInput,
  TablePaginationMode,
  TableRowSelectionMode,
} from "@benjaminor-dev/quasar-app-extension-table-builder/types";

import type {
  PersistDataConfig,
  PersistDataConfigOption,
} from "@benjaminor-dev/quasar-app-extension-table-builder/types";

import type {
  TableBuilderExpose,
  TableBuilderReloadOptions,
  RefTableBuilder,
} from "@benjaminor-dev/quasar-app-extension-table-builder";

import { tableColumnSlotName } from "@benjaminor-dev/quasar-app-extension-table-builder";
Extension Relationship
@benjaminor-dev/datatable Required. Provides DataTable, pagination bar, and grid styles used by TableBuilder.
@benjaminor-dev/form-builder Required. Filters, store, and validation via defineForm / FormBuilder.

Table Builder does not include its own form generator: it reuses Form Builder to avoid duplicating inputs and rules.

Boot order (full stack):

1. Pinia (host)
2. DataTable β€” CSS
3. Form Builder β€” boot/store
4. bor/bor-form-builder-defaults (host)
5. Table Builder β€” boot/store
6. bor/bor-table-builder-defaults (host)
7. Dialog File Preview β€” boot/dialog
8. bor/bor-dialog-file-preview-defaults (host)
9. Dialog Loader β€” boot/dialog
10. bor/bor-dialog-loader-defaults (host)
11. Dialog Message β€” boot/dialog
12. bor/bor-dialog-message-defaults (host)

In quasar.config you will see each extension defaults boot (e.g. bor/bor-table-builder-defaults); package boots are injected by the extension at compile time β€” they do not appear as short entries in your config.

Session and Pinia store

One session per tableName in Pinia (same pattern as Form Builder store for filters).

Access: useTableBuilder().selectTableStore(tableName) or selectTableStore(tableName) imported from the package.

selectTableStore<R>(tableName)

Typed view of the session. The same tableName in another .vue or composable shares rows and metadata.

const { defineTable, selectTableStore } = useTableBuilder();

const usuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable");

// After API delete, remove row without refetch
await api.delete(id);
usuariosTableStore.patchItems((rows) => rows.filter((r) => r.id !== id));

// Reactive read
if (usuariosTableStore.hasItems.value) {
  const rows = usuariosTableStore.items.value;
}
Member Description
tableName Identifier (defineTable.tableName)
items Visible rows (reactive)
hasItems true if there is at least one row
pagination Metadata in server mode
appliedFilters Last successful search snapshot
hasSearched true after at least one successful fetch
getItems() Non-reactive snapshot
setItems / patchItems Local mutation without refetch
clearItems() Clears rows and metadata
remove() Removes the Pinia session

Exported type: TableSessionHandle<R> in /types.

Filters remain in Form Builder (table:{tableName}:filters by default).

persistData (optional)

Keeps the Pinia session when unmounting <TableBuilder /> (navigate away and back).

const config = defineTable<Filters, Row>({
  tableName: "usuarios",
  columns: [...],
  fetch: async ({ filters, page, pageSize, signalAbort }) => ({ rows, pagination: { ... } }),
  persistData: true,
  fetchOnMount: false, // empty until first search
});
Behavior Detail
persistData: true When leaving the route, the store keeps rows, page, and applied filter snapshot.
persistData: false (default) On unmount, the session for that tableName is removed.
Navigate away and back With persistData: true, hydrates from Pinia (reuseOnMount: true by default).
Reload tab (F5) Lost (SPA session memory only).
reload() / search Always hit the network.
tableRef.invalidate() Clears rows and metadata in the store (useful before reload() after external CRUD).

Fine tuning: persistDataConfig: { reuseOnMount: false } forces fetch on mount even if Pinia has data.

Public entrypoints

Entrypoint Purpose
@benjaminor-dev/quasar-app-extension-table-builder TableBuilder, useTableBuilder, configureTableBuilder, configureTableBuilderDefaults, configureTableFetchAbort, registerTableFetchAbortStore, createTableFetchAbortStoreAdapter, useTableFetchAbortStore, defaultIsTableFetchAbortError, tableFetchAbortIsAbortErrorIncludingAxios, selectTableStore, useTableRef, useTableBuilderBuilderRef, tableColumnSlotName
@benjaminor-dev/quasar-app-extension-table-builder/types Config, column, fetch, boot, and session types (TableBuilderConfig, TableFetchContext, TableSessionHandle, ConfigureTableBuilderOptions, …) β€” IDE autocomplete
@benjaminor-dev/quasar-app-extension-table-builder/scaffold runBorMakeTable (bor:make-table)
@benjaminor-dev/quasar-app-extension-table-builder/boot/store Quasar boot (registered by the extension)
@benjaminor-dev/quasar-app-extension-table-builder/main.css Table, drawer, pagination, and skeleton styles

Troubleshooting

UI texts still in English

Defaults boot missing from quasar.config

Under normal conditions install registers it automatically. If missing (old project or manual edit):

npx quasar ext invoke @benjaminor-dev/table-builder

Pinia / filter or table store error

fetch in server mode without metadata

In pagination.mode: "server", return { rows, pagination } with at least total, page, and pageSize. No need to compute from/to/lastPage manually.

return { rows: items, pagination: { total, page, pageSize } };

Search button disabled

Check minActiveFilters and canSearch. The tooltip shows the reason. Values are read from the live form, not the last chip snapshot.

Chips do not match what I see in the drawer

Expected: chips = last successful search. Open filters, change values, and click search to update.

Server sorting does nothing

Check that the column has sortable: true and your fetch uses sortBy / descending when calling the API.

selectedRows in template causes type error

Use tableRef.value?.selectedRows in script (e.g. with computed), not tableRef.selectedRows in the template.

Stop does not cancel the HTTP request

Endpoint errors do not reach the filter form

Implement onFetchError and map the error body with selectFormStore(formName).setErrors(...) per your API contract. Verify formName matches the filter form.


MIT Β© BenjamΓ­n Olvera R.