Table Builder for Quasar
πͺπΈ VersiΓ³n en espaΓ±ol | π§π· VersΓ£o em portuguΓͺs
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
- What it is and what it includes
- Requirements and compatibility
- Install in a Quasar app
- What the extension adds to the host
- Global defaults (boot)
- Scaffold commands in the host
- Quick start
- Language (i18n)
- TypeScript in your IDE
defineTableconfiguration- Filters and search
- Pagination
- Sorting
- Row selection
- [
fetch, cancellation, and hooks`](#fetch-cancellation-and-hooks) - Slots
TableBuilderref- Exported TypeScript types
- Related extensions
- Session and Pinia store
- Public entrypoints
- Troubleshooting
In 30 seconds
Recommended flow:
- Define filter types
Fand row typeR. - Configure filter fields with Form Builder (
defineFormor inlinefilters.form). - Create the table with
defineTable<F, R>({ ... })fromuseTableBuilder(). - Render
<TableBuilder ref="tableRef" :config="config" />with#column-*and#toolbarslots.
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.
defineTable<F, R>+<TableBuilder />; filtersdrawer|inlineorfilters: false.- Pagination
server|client; sorting; row selection;#column-*/#toolbarslots. - Imperative ref (
reload,resetFilters,cancelFetch, β¦) andselectTableStore(Pinia per table). - Optional
persistData; cancellation viasignalAbortor global abort in boot. - Boot defaults (
configureTableBuilder);filterUithemes buttons, drawer header, chips, and panel bylight/dark. - Grid and pagination via
DataTable(@benjaminor-dev/datatable) β cards on mobile< sm. - Boot defaults (
configureTableBuilder); scaffoldbor:make-table. - Built-in i18n
en,es,ptβ follows Quasar$q.lang; boot overrides (locale,messages).
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:
src/components/TableBuilder/UsuariosTable.vuetableName: "UsuariosTable"(PascalCase, aligned with the file)- Types
UsuariosFilters/UsuarioRowand variableusuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable")
Naming rules:
- The file name is normalized to PascalCase and ends in
Table.vue. - If you pass
UsuariosTable, it does not duplicate the suffix. usuariosβUsuariosTable.vue;personas/equipoβpersonas/EquipoTable.vue.- You can force overwrite with
--force(or-f). - The stub includes drawer filters, columns with slots (
icon,actions),selectTableStore, anddeleteRowviapatchItems. - Host override template:
scripts/bor/stubs/bor-make-table.stub.
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.
- Import from
@benjaminor-dev/quasar-app-extension-table-builder/types(TableBuilderConfig,TableColumnDef,TableFetchContext, β¦). defineTable<MyFilters, MyRow>({ ... })β generics typefilters,fetchcontext, and rows in slots.- Ctrl+Space (Windows/Linux) or Cmd+Space (macOS) inside
defineTableshows columns,pagination,filters, hooks, etc. - In the
fetchcallback, the parameter includesfilters,page,pageSize,sortBy,descending, and, if needed,signalAbortβ all autocompleted from your generics. - Filters: form
fieldsuse the same Form Builder types; combine with@benjaminor-dev/quasar-app-extension-form-builder/typesif you share models. - In
configureTableBuilder({ filterUi: { ... } }), Ctrl+Space / Cmd+Space listsbuttonStyle,panelHeader,chips,chipsPanel, and per-theme fields (light/dark). - In boot,
localeaccepts"en" \| "es" \| "pt";messagesacceptsPartial<TableBuilderMessages>(toolbar, drawer, empty states, skeleton). - One-off override in
defineTable({ filterUi })for a single table.
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".
Filters and search
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:
fieldsβ required.defaultValuesβ optional: overrides or preload only; no need to list the entire model.formNameβ optional; defaults totable:{tableName}:filters.
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.
- Filters: opens the drawer.
- Search / refresh (same button): first search when there are no rows yet; refresh when results exist (reload without resetting page).
- Stop: during loading, cancels the query β only if
allowUserCancelis enabled (boot or table).
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
- No rows after search β contextual message (with/without applied filters).
- Manually cancelled search β "Query cancelled" (depending on load origin).
- Loading β skeleton with message by reason (
search,pagination,sort,refresh, β¦).
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";
Related extensions
| 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
- Set
framework.langinquasar.configor pin the extension:configureTableBuilder({ locale: "es" })insrc/boot/bor/bor-table-builder-defaults.ts.
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
- Host Pinia must run before Form Builder and Table Builder.
- Install and boot Form Builder and DataTable before Table Builder.
- If you see uninitialized session errors, check that
<TableBuilder />is mounted andtableNameis unique per table.
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.
Cancel during pagination vs search
- Search cancelled with no prior rows β "Query cancelled" message.
- Pagination, sorting, or chip removal cancelled β reverts page/order/filter and keeps current rows.
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
- Path A (default): your
fetchmust forwardsignalAbortto the client (fetch, axios as{ signal: signalAbort }, etc.). - Path B (adapter): register
configureTableBuilder({ fetchAbort: β¦ })in boot (orfetchAbort: { useStore: true }) and verify your HTTP layer injects the same storeAbortSignal.configureTableFetchAbortis the equivalent low-level API. If axios throwsmessage: "canceled", includeisAbortErrorin the adapter (see README Β§ Cancellation).
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.