Table Builder para Quasar
🇺🇸 English | 🇧🇷 Versão em português
Extensión Quasar para tablas declarativas en apps Vue 3 + Quasar: listados con filtros (vía Form Builder), paginación servidor/cliente, ordenación en backend, selección de filas, slots de columna y consultas cancelables.
Tabla de contenidos
- En 30 segundos
- Qué es y qué incluye
- Requisitos y compatibilidad
- Instalación en app Quasar
- Qué agrega la extensión al host
- Defaults globales (boot)
- Comandos scaffold en host
- Uso rápido
- Idioma (i18n)
- TypeScript en tu IDE
- Configuración de
defineTable - Filtros y búsqueda
- Paginación
- Ordenación
- Selección de filas
- [
fetch, cancelación y hooks`](#fetch-cancelación-y-hooks) - Slots
- Ref de
TableBuilder - Tipos TypeScript exportados
- Extensiones relacionadas
- Sesión y store Pinia
- Entrypoints públicos
- Troubleshooting
En 30 segundos
Flujo base recomendado:
- Definir tipos de filtros
Fy filaR. - Configurar campos de filtro con Form Builder (
defineFormofilters.forminline). - Crear la tabla con
defineTable<F, R>({ ... })desdeuseTableBuilder(). - Renderizar
<TableBuilder ref="tableRef" :config="config" />con slots#column-*y#toolbar.
La extensión registra boot y CSS en el proyecto host automáticamente tras quasar ext add.
Qué es y qué incluye
Table Builder orquesta listados: toolbar, filtros Form Builder, tabla, paginación y fetch. No sustituye Form Builder — los filtros siempre son un formulario FB.
defineTable<F, R>+<TableBuilder />; filtrosdrawer|inlineofilters: false.- Paginación
server|client; ordenación; selección de filas; slots#column-*/#toolbar. - Ref imperativa (
reload,resetFilters,cancelFetch, …) yselectTableStore(Pinia por tabla). persistDataopcional; cancelación víasignalAborto abort global en boot.- Defaults en boot (
configureTableBuilder);filterUitematiza botones, cabecera del drawer, chips y panel porlight/dark. - Grilla y paginación vía
DataTable(@benjaminor-dev/datatable) — cards en móvil< sm. - Defaults en boot (
configureTableBuilder); scaffoldbor:make-table. - i18n integrado
en,es,pt— sigue Quasar$q.lang; overrides en boot (locale,messages).
Requisitos y compatibilidad
| Item | Valor |
|---|---|
| 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 — requerida |
| Form Builder | @benjaminor-dev/form-builder ^0.9.0 — requerida |
| moment | ^2.30.1 (peer; usado al formatear chips de filtros con fechas/horas) |
| Formato del paquete | ES modules |
| CLI recomendado | @quasar/app-vite ^2.x o ^3.x |
El boot de Pinia del host debe correr antes que Form Builder y Table Builder.
Instalación en app Quasar
Instala DataTable y Form Builder antes que Table Builder (peers requeridos):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder
quasar ext add @benjaminor-dev/table-builder
Remover:
quasar ext remove @benjaminor-dev/table-builder
Qué agrega la extensión al host
| Recurso | Ruta npm |
|---|---|
| Boot store | ~@benjaminor-dev/quasar-app-extension-table-builder/boot/store |
| Estilos Table Builder | ~@benjaminor-dev/quasar-app-extension-table-builder/main.css |
| Estilos DataTable | ~@benjaminor-dev/quasar-app-extension-datatable/main.css (peer; registrado también por esta extensión) |
| Scripts scaffold | bor:make-table → scripts/bor/bor-make-table.mjs |
| Defaults boot (host) | src/boot/bor/bor-table-builder-defaults.ts — creado en install; Skip/Overwrite si ya existe |
Install (quasar ext add / invoke): crea src/boot/bor/bor-table-builder-defaults.ts (o .js) y lo registra en quasar.config → boot: [] automáticamente. No hace falta editarlo a mano en una instalación normal. El boot de paquete (boot/store) y CSS los inyecta la extensión en cada dev/build.
Remove (quasar ext remove): elimina el script scaffold, el defaults boot, su entrada en quasar.config y bor:make-table en package.json (si sigue apuntando al archivo del install).
Si borras el archivo de defaults, la app sigue con built-in de la librería (fetchOnMount: false, pageSize: 10, etc.).
Orden de boots (esta extensión): Pinia → DataTable (CSS) → Form Builder → boot store TB → bor/bor-table-builder-defaults → resto.
Defaults globales (boot)
Evita repetir filters, pagination, fetchOnMount, persistData y abort en cada defineTable. Prioridad: defineTable → boot (configureTableBuilder) → built-in de la librería.
Tras quasar ext add, revisa src/boot/bor/bor-table-builder-defaults.ts — plantilla con fetchAbort y opciones comentadas (pagination, persistData). Defaults built-in: JSDoc @default en tipos.
// src/boot/bor/bor-table-builder-defaults.ts (extracto)
import { configureTableBuilder } from "@benjaminor-dev/quasar-app-extension-table-builder";
configureTableBuilder({
filters: { layout: "drawer", openFiltersOnMount: false, showFilterChips: true },
fetchOnMount: false,
// persistData: true, // descomenta para caché SPA global
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)",
},
},
},
// fetchAbort opcional — ver archivo generado en install
});
| Opción boot | Descripción |
|---|---|
filters |
Defaults del panel (sin form; el formulario sigue en cada tabla) |
filterUi.buttonStyle |
Colores y variantes de Filtros / Buscar / Limpiar / Stop por tema (light / dark) |
filterUi.panelHeader |
Cabecera del drawer (color, textColor Quasar por tema) |
filterUi.chips |
Chips de filtros activos (color, textColor por tema) |
filterUi.chipsPanel |
Borde y fondo del contenedor de chips (borderColor, backgroundColor CSS por tema) |
pagination |
pageSize, placement, scrollOnPageChange, pageSizeOptions |
fetchOnMount |
Carga inicial al montar (default librería: false) |
persistData |
Conservar sesión Pinia al desmontar la tabla |
onFetchError |
Hook global de errores de fetch |
fetchAbort |
Abort global opcional (useTableFetchAbortStore o adapter propio) + allowUserCancel |
Usa configureTableBuilderDefaults si solo necesitas defaults sin registrar fetchAbort.
Prioridad de filterUi: defineTable({ filterUi }) → boot → built-in. El tema activo sigue $q.dark del host.
Cancelación detallada: § Cancelación.
Comandos scaffold en host
La extensión registra bor:make-table en el host (scripts/bor/bor-make-table.mjs → runBorMakeTable en /scaffold). Ejecuta npm run bor:help (Form Builder) para ver todos los generadores instalados.
Tablas (bor:make-table)
npm run bor:make-table -- <nombre-o-ruta>
Ejemplo — crear UsuariosTable.vue:
npm run bor:make-table -- usuarios
Resultado:
src/components/TableBuilder/UsuariosTable.vuetableName: "UsuariosTable"(PascalCase, alineado al archivo)- Tipos
UsuariosFilters/UsuarioRowy variableusuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable")
Reglas de naming:
- El nombre del archivo se normaliza a PascalCase y termina en
Table.vue. - Si envías
UsuariosTable, no duplica el sufijo. usuarios→UsuariosTable.vue;personas/equipo→personas/EquipoTable.vue.- Puedes forzar overwrite con
--force(o-f). - El borrador incluye filtros drawer, columnas con slots (
icon,actions),selectTableStoreydeleteRowvíapatchItems. - Plantilla override en el host:
scripts/bor/stubs/bor-make-table.stub.
Uso rápido
<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: "Usuarios",
columns: [
{ name: "name", label: "Nombre", field: "name", align: "left", sortable: true },
{ name: "role", label: "Rol", 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: "Buscar" },
{
type: "InputSelect",
model: "role",
label: "Rol",
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>
Idioma (i18n)
Integrado en, es y pt. Por defecto los textos de toolbar, drawer, estados vacíos y skeleton siguen el idioma de Quasar ($q.lang.isoName); si Quasar no tiene idioma, el fallback es en.
Resolución de locale: boot locale → prefijo de $q.lang.isoName (es* → es, pt* → pt) → en.
| Opción | Cuándo usarla |
|---|---|
framework.lang en quasar.config.ts |
Recomendado — alinea Table Builder con el resto de Quasar |
locale: 'es' en boot |
Fijar idioma (en | es | pt); ignora cambios de $q.lang |
messages: { search: '…' } en boot |
Cambiar un texto sin fijar locale |
Ejemplo en quasar.config.ts (español para toda la app):
// quasar.config.ts
framework: {
lang: "es",
},
Plantilla boot (tras install):
configureTableBuilder({
// locale: "es", // fija solo el idioma de Table Builder
// messages: { search: "Consultar" },
});
Claves i18n: botones (filters, search, refresh, stopQuery, clear), drawer (filtersPanelTitle), estados vacíos (emptyNotQueried*, noResults*, fetchCancelled*), tooltip de búsqueda (searchGateDefault), skeleton (loading*Title, loading*Hint) y aria (filtersAppliedAria, removeFilterAria con {label}).
Prioridad de mensajes: boot messages fusiona sobre el catálogo built-in del locale activo.
TypeScript en tu IDE
La config de la tabla está tipada de punta a punta: deja que el IDE te guíe.
- Importa desde
@benjaminor-dev/quasar-app-extension-table-builder/types(TableBuilderConfig,TableColumnDef,TableFetchContext, …). defineTable<MisFiltros, MiFila>({ ... })— los genéricos tipanfilters, el contexto defetchy las filas en slots.- Ctrl+Space (Windows/Linux) o Cmd+Space (macOS) dentro de
defineTablemuestra columnas,pagination,filters, hooks, etc. - En el callback
fetch, el parámetro incluyefilters,page,pageSize,sortBy,descendingy, si lo necesitas,signalAbort— todo autocompletado según tus genéricos. - Filtros: los
fieldsdel formulario son los mismos tipos de Form Builder; combina con@benjaminor-dev/quasar-app-extension-form-builder/typessi compartes modelos. - En
configureTableBuilder({ filterUi: { ... } }), Ctrl+Space / Cmd+Space listabuttonStyle,panelHeader,chips,chipsPanely campos por tema (light/dark). - En boot,
localeacepta"en" \| "es" \| "pt";messagesaceptaPartial<TableBuilderMessages>(toolbar, drawer, estados vacíos, skeleton). - Override puntual en
defineTable({ filterUi })para una sola tabla.
Si una opción no sale en autocompletado, revisa el import desde /types antes de buscar en el README.
Configuración de defineTable
Campos principales de defineTable<F, R>():
| Campo | Descripción |
|---|---|
tableName |
Identificador único (nombre del form de filtros derivado si no pasas formName). |
title, hint, prependIcon, helpTooltip |
Presentación en toolbar. |
columns |
Definición de columnas (name, label, field?, format, sortable, align, …). sortable por defecto false. |
selection |
single | multiple para checkboxes; omitir para desactivar. Ref: selectedRows, clearSelection(). |
pagination |
Modo, pageSize, placement y defaultSort (server). |
filters |
Panel Form Builder o false. |
fetch |
Función async que devuelve filas (y metadatos en modo server). |
fetchOnMount |
Carga inicial al montar (respeta reglas de búsqueda). |
persistData |
true conserva la sesión Pinia al desmontar la tabla (navegar y volver en la SPA). |
persistDataConfig |
Ajuste fino (reuseOnMount). Gana sobre persistData si defines ambos. |
onBeforeFetch, onAfterFetch, onFetchError |
Hooks del ciclo de consulta. |
allowUserCancel |
Stop / cancelFetch() para el usuario (override del boot). Default: false. Boot global: solo vía fetchAbort.allowUserCancel. |
filterUi |
Override de botones, cabecera drawer, chips y panel de chips por tema (light / dark). |
Columnas (columns)
defineTable no exige un orden fijo: el runtime normaliza la lista al resolver la config.
| Comportamiento | Detalle |
|---|---|
Columna # |
Si no defines name: "index", se inyecta al inicio con label: "#" y field: "index". Ancho compacto por defecto. El fetch no debe devolverla: se calcula en cada página (1, 2, … según offset). |
field |
Opcional en columnas solo-slot (#column-{name}): omítelo si el valor viene del slot. Si lo declaras, se usa para value/text y ordenación en client. |
label |
Opcional en columnas solo-slot; si falta, el encabezado queda vacío. |
align |
Por defecto "left". Usa "center" o "right" donde aplique (p. ej. iconos o acciones). |
| Orden | Las demás columnas (incluida actions) se muestran en el mismo orden que en columns. |
sortable |
Por defecto false. Marca sortable: true en las columnas que ordenes; en server usa sortBy / descending en fetch. |
Para personalizar la columna índice (ancho, alineación, ocultarla), declárala explícitamente con name: "index".
Filtros y búsqueda
Layout
| Valor | Comportamiento |
|---|---|
drawer (default) |
Formulario en panel lateral; toolbar con grupo Filtros + lupa/refresh. |
inline |
Panel sobre la tabla con cabecera Filtros, formulario, Buscar / Limpiar. Los chips (si están activos) van debajo del formulario. |
Reglas para habilitar búsqueda (AND)
filters: {
minActiveFilters: 1,
canSearch: (filters) => Boolean(filters.search?.trim()) || filters.role != null,
searchDisabledMessage: "Indica búsqueda o rol para consultar.",
showFilterChips: true,
form: { fields: [...] },
}
| Opción | Default | Descripción |
|---|---|---|
minActiveFilters |
0 |
Mínimo de filtros con valor activo. |
canSearch |
true |
boolean o (filters: F) => boolean. |
searchDisabledMessage |
"Ajusta los filtros para consultar." |
Tooltip cuando falla canSearch o minActiveFilters. |
showFilterChips |
true |
Chips de la última búsqueda exitosa. |
resetFiltersOnMount |
false |
Restaura filtros al montar. |
openFiltersOnMount |
false |
Abre drawer al montar (true | "desktop"). |
Los chips reflejan el snapshot de la última búsqueda exitosa, no el formulario en vivo.
Formulario de filtros (filters.form)
Misma forma que defineForm de Form Builder:
fields— obligatorio.defaultValues— opcional: solo overrides o precarga; no hace falta listar todo el modelo.formName— opcional; por defectotable:{tableName}:filters.
filters: {
form: {
fields: [{ type: "InputSearch", model: "search", label: "Buscar" }],
defaultValues: { role: "admin" },
},
}
También puedes pasar el resultado de defineForm(...) si prefieres definir el formulario fuera de defineTable.
Toolbar con drawer (grupo fusionado)
Un QBtnGroup outline agrupa los botones del toolbar solo cuando Filtros y Buscar/Stop comparten variante outline. Si mezclas variantes (p. ej. outline + flat), se renderizan como botones separados.
- Filtros: abre el drawer.
- Lupa / refresh (mismo botón): primera búsqueda si aún no hay filas; refresh si ya hay resultados (recarga sin resetear página).
- Stop: durante la carga, cancela la consulta — solo si
allowUserCancelestá activo (boot o tabla).
UI de filtros por tema (filterUi)
| Bloque boot | Qué controla |
|---|---|
buttonStyle |
Botones Filtros / Buscar / Limpiar / Stop (*Color, *Variant por tema) |
panelHeader |
Cabecera del drawer (color, textColor Quasar) |
chips |
QChip de filtros activos (color, textColor) |
chipsPanel |
Borde y fondo del contenedor de chips (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" } },
},
Paginación
pagination: {
mode: "server",
pageSize: 25,
placement: "auto",
scrollOnPageChange: "auto",
defaultSort: { sortBy: "name" },
}
placement |
Comportamiento |
|---|---|
bottom |
Solo debajo (default). |
top |
Solo arriba. |
both |
Arriba y abajo. |
auto |
Abajo siempre; arriba solo si la página actual muestra ≥15 filas visibles (cuenta filas renderizadas, no el valor del selector «filas por página»). |
scrollOnPageChange |
Comportamiento |
|---|---|
auto (default) |
Scroll al título en móvil o cuando la paginación solo está abajo. |
true |
Siempre tras cambiar de página. |
false |
Nunca. |
| defaultSort | Orden inicial en modo server (sortBy, descending?). Ignorado en client salvo que QTable reciba el estado inicial vía paginación interna. |
Modo server: fetch debe devolver { rows, pagination } con al menos total, page y pageSize. Los campos from, to y lastPage son opcionales (el runtime los calcula si faltan). Al pulsar un encabezado sortable, se reconsulta con sortBy y descending (página 1).
Modo client: fetch puede devolver R[] o { rows }; pagina y ordena QTable en memoria.
Ordenación
| Modo | Comportamiento |
|---|---|
client |
Marca columnas sortable: true donde quieras. QTable ordena las filas cargadas sin nuevo fetch. |
server |
Marca sortable: true en las columnas que tu API soporta. Al pulsar el encabezado se ejecuta fetch en página 1 con sortBy (nombre de columna) y descending. |
Orden inicial en servidor:
pagination: {
mode: "server",
defaultSort: { sortBy: "name", descending: false },
}
sortBy y descending también llegan a onBeforeFetch con la misma forma que en fetch.
Selección de filas
const config = defineTable<Filters, Row>({
// ...
selection: "multiple", // o "single"
});
| Valor | UI |
|---|---|
"multiple" |
Checkbox por fila |
"single" |
Radio por fila |
Omitir / false |
Sin selección |
La selección se limpia tras cada fetch exitoso (búsqueda, paginación, ordenación o reload).
En script, lee las filas desde la ref:
const tableRef = useTableRef();
const selectedCount = computed(() => tableRef.value?.selectedRows?.length ?? 0);
En plantilla, usa un computed (no accedas a tableRef.selectedRows directamente: vue-tsc no desenvuelve el ShallowRef en el template).
fetch, cancelación y hooks
Contexto de fetch
type TableFetchContext<F> = {
filters: F;
page: number;
pageSize: number;
sortBy: string | null;
descending: boolean;
signalAbort?: AbortSignal;
};
Devuelve filas (y metadatos en modo server):
// client
fetch: async ({ filters }) => ({ rows: await api.list(filters) }),
// server
fetch: async ({ filters, page, pageSize, sortBy, descending }) => ({
rows: items,
pagination: { total, page, pageSize },
}),
Cancelación — elige un camino
La cancelación no pasa por onFetchError.
¿Quién puede detener la consulta?
Por defecto nadie (sin botón Stop). Actívalo en boot (fetchAbort.allowUserCancel) o por tabla (defineTable.allowUserCancel + signalAbort en fetch).
Prioridad: defineTable.allowUserCancel → fetchAbort.allowUserCancel (boot) → false.
Boot fetchAbort.allowUserCancel |
Tabla | Stop / cancelFetch() |
|---|---|---|
true |
(sin definir) | ✅ todas |
true |
false |
❌ solo esa tabla |
false / sin boot |
true |
✅ solo esa tabla |
false |
(sin definir) | ❌ ninguna |
// Boot — todas las tablas con Stop (abort global del host)
configureTableBuilder({
fetchAbort: {
allowUserCancel: true,
getSignal: () => useAbortStore().getSignal(),
abort: () => useAbortStore().abort(),
},
});
// Una tabla sin Stop aunque el boot lo tenga activo
defineTable({ tableName: "Logs", allowUserCancel: false, ... });
// Solo esta tabla con Stop (Camino A portable)
defineTable({
tableName: "Usuarios",
allowUserCancel: true,
fetch: ({ signalAbort, ...ctx }) => UsersService.list(ctx.filters, signalAbort),
});
Una nueva búsqueda o cambio de página sigue abortando la petición anterior aunque Stop esté desactivado (evita solapamientos).
Camino A — portable (default)
Table Builder crea un AbortController por consulta. Reenvía signalAbort al cliente HTTP en 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 };
},
Si el service vive fuera del componente, pásalo como argumento:
fetch: ({ filters, signalAbort }) => UsersService.list(filters, signalAbort),
Recomendado con varias tablas en paralelo (cada consulta tiene su propia señal).
Camino B — abort global del host (sin signalAbort en cada tabla)
Si tu app ya centraliza la cancelación (Pinia + axios que auto-inyecta signal en cada petición), registra el adaptador una vez en el boot del host.
Opción incluida — store Pinia de la extensión (useTableFetchAbortStore):
configureTableBuilder({
fetchAbort: {
useStore: true,
allowUserCancel: false, // recomendado: false
},
});
Inyecta useTableFetchAbortStore().getSignal() en tu capa HTTP (axios, etc.).
Opción custom — store propio del host:
// 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"),
},
});
});
Tu capa HTTP debe seguir inyectando el mismo signal del store cuando la petición no trae uno explícito (como en ApiUtils.makeNewConfig).
Entonces el fetch de la tabla no necesita usar signalAbort:
fetch: ({ filters, page, pageSize }) =>
UsersService.index(filters, { page, pageSize }),
Table Builder llamará abort() del store al cancelar o al iniciar otra consulta; axios lo captará igual que antes.
Nota: un abort store global implica una consulta abortable activa en la app. Con varias tablas cargando a la vez, usa el camino A.
Hooks (control opcional)
| Hook | Cuándo | Comportamiento |
|---|---|---|
onBeforeFetch |
Antes de arrancar | Devuelve false para no consultar (validación previa). |
onAfterFetch |
Tras éxito | Filas ya normalizadas (index incluido). |
onFetchError |
Error real (red, HTTP, excepción) | Si lo defines, no hay notify por defecto. |
Cancelación (AbortError, axios canceled, …) no dispara onFetchError.
Ejemplo — errores de validación en filtros (adapta al contrato de tu API):
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 al consultar." });
return;
}
selectFormStore(`table:${tableName}:filters`).setErrors(
Object.fromEntries(
Object.entries(raw).map(([key, value]) => [
key,
Array.isArray(value) ? value[0] : String(value),
]),
),
);
},
Estados de UI tras la consulta
- Sin filas tras búsqueda → mensaje contextual (con/sin filtros aplicados).
- Búsqueda cancelada manualmente → «Consulta cancelada» (según origen de la carga).
- Carga → skeleton con mensaje según motivo (
search,pagination,sort,refresh, …).
Slots
<TableBuilder :config="config">
<template #toolbar>
<q-chip v-if="selectedCount > 0" dense color="primary">
{{ selectedCount }} seleccionados
</q-chip>
<q-btn outline label="Exportar" />
</template>
<template #column-actions="{ row, text, column, value }">
<q-btn flat icon="edit" @click="edit(row)" />
</template>
</TableBuilder>
| Slot | Props |
|---|---|
toolbar |
— (acciones extra a la derecha del grupo Filtros) |
column-{name} |
row, column, value, text, index |
Helper tipado: tableColumnSlotName("actions") → "column-actions".
Ref de TableBuilder
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 ?? [];
| Método / ref | Descripción |
|---|---|
reload(options?) |
Vuelve a ejecutar fetch. resetFilters: true restaura filtros y va a página 1 (salvo resetPage: false explícito). |
resetFilters() |
Restaura filtros a defaults (sin recargar). |
openFilters() / closeFilters() |
Drawer de filtros. |
cancelFetch() |
Cancela el fetch en curso (abort del signalAbort activo). |
clearSelection() |
Deselecciona todas las filas. |
invalidate() |
Vacía filas y metadatos en Pinia. Útil antes de reload() tras CRUD externo si no usas patchItems. |
loading |
true mientras hay un fetch en curso. |
selectedRows |
Filas seleccionadas cuando selection está activo (se limpia tras cada fetch exitoso). |
Tipos TypeScript exportados
Desde el entrypoint principal y /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";
Extensiones relacionadas
| Extensión | Relación |
|---|---|
| @benjaminor-dev/datatable | Requerida. Provee DataTable, barra de paginación y estilos de grilla usados por TableBuilder. |
| @benjaminor-dev/form-builder | Requerida. Filtros, store y validación vía defineForm / FormBuilder. |
Table Builder no incluye generador de formularios propio: reutiliza Form Builder para no duplicar inputs ni reglas.
Orden de boots (stack completo):
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)
En quasar.config verás el defaults boot de cada extensión (p. ej. bor/bor-table-builder-defaults); los boots del paquete los inyecta la extensión al compilar — no aparecen como entradas cortas en tu config.
Sesión y store Pinia
Una sesión por tableName en Pinia (mismo patrón que el store de Form Builder para filtros).
Acceso: useTableBuilder().selectTableStore(tableName) o selectTableStore(tableName) importado del paquete.
selectTableStore<R>(tableName)
Vista tipada de la sesión. El mismo tableName en otro .vue o composable comparte filas y metadatos.
const { defineTable, selectTableStore } = useTableBuilder();
const usuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable");
// Tras borrar en API, quitar fila sin refetch
await api.delete(id);
usuariosTableStore.patchItems((rows) => rows.filter((r) => r.id !== id));
// Lectura reactiva
if (usuariosTableStore.hasItems.value) {
const rows = usuariosTableStore.items.value;
}
| Miembro | Descripción |
|---|---|
tableName |
Identificador (defineTable.tableName) |
items |
Filas visibles (reactivo) |
hasItems |
true si hay al menos una fila |
pagination |
Metadatos en modo server |
appliedFilters |
Snapshot de la última búsqueda exitosa |
hasSearched |
true tras al menos un fetch exitoso |
getItems() |
Snapshot no reactivo |
setItems / patchItems |
Mutación local sin refetch |
clearItems() |
Vacía filas y metadatos |
remove() |
Elimina la sesión Pinia |
Tipo exportado: TableSessionHandle<R> en /types.
Filtros siguen en Form Builder (table:{tableName}:filters por defecto).
persistData (opcional)
Conserva la sesión Pinia al desmontar <TableBuilder /> (navegar a otra ruta y volver).
const config = defineTable<Filters, Row>({
tableName: "usuarios",
columns: [...],
fetch: async ({ filters, page, pageSize, signalAbort }) => ({ rows, pagination: { ... } }),
persistData: true,
fetchOnMount: false, // vacío hasta la primera búsqueda
});
| Comportamiento | Detalle |
|---|---|
persistData: true |
Al salir de la ruta, el store conserva filas, página y snapshot de filtros aplicados. |
persistData: false (default) |
Al desmontar, se elimina la sesión de esa tableName. |
| Navegar y volver | Con persistData: true, hidrata desde Pinia (reuseOnMount: true por defecto). |
| Recargar pestaña (F5) | Se pierde (solo memoria de la sesión SPA). |
reload() / buscar |
Siempre van a red. |
tableRef.invalidate() |
Vacía filas y metadatos en el store (útil antes de reload() tras CRUD externo). |
Ajuste fino: persistDataConfig: { reuseOnMount: false } fuerza fetch al montar aunque haya datos en Pinia.
Entrypoints públicos
| Entrypoint | Propósito |
|---|---|
@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 |
Tipos de config, columnas, fetch, boot y sesión (TableBuilderConfig, TableFetchContext, TableSessionHandle, ConfigureTableBuilderOptions, …) — autocompletado en IDE |
@benjaminor-dev/quasar-app-extension-table-builder/scaffold |
runBorMakeTable (bor:make-table) |
@benjaminor-dev/quasar-app-extension-table-builder/boot/store |
Boot Quasar (registrado por la extensión) |
@benjaminor-dev/quasar-app-extension-table-builder/main.css |
Estilos de tabla, drawer, paginación y skeleton |
Troubleshooting
Los textos de la UI siguen en inglés
- Configura
framework.langenquasar.configo fija la extensión:configureTableBuilder({ locale: "es" })ensrc/boot/bor/bor-table-builder-defaults.ts.
El defaults boot no aparece en quasar.config
En condiciones normales el install lo registra solo. Si falta (proyecto antiguo o edición manual):
npx quasar ext invoke @benjaminor-dev/table-builder
Error de Pinia / store de filtros o tablas
- Pinia del host debe ir antes que Form Builder y Table Builder.
- Instala y bootéa Form Builder y DataTable antes que Table Builder.
- Si ves errores de sesión no inicializada, comprueba que
<TableBuilder />esté montado y quetableNamesea único por tabla.
fetch en modo server sin metadatos
En pagination.mode: "server", devuelve { rows, pagination } con al menos total, page y pageSize. No hace falta calcular from/to/lastPage manualmente.
return { rows: items, pagination: { total, page, pageSize } };
Botón buscar deshabilitado
Revisa minActiveFilters y canSearch. El tooltip indica el motivo. Los valores se leen del formulario en vivo, no del último snapshot de chips.
Los chips no coinciden con lo que veo en el drawer
Es esperado: chips = última búsqueda exitosa. Abre filtros, cambia valores y pulsa buscar para actualizar.
Cancelar durante paginación vs búsqueda
- Búsqueda cancelada sin filas previas → mensaje «Consulta cancelada».
- Paginación, ordenación o quitar chip cancelada → revierte página/orden/filtro y mantiene filas actuales.
Ordenar en server no hace nada
Comprueba que la columna tenga sortable: true y que tu fetch use sortBy / descending al llamar a la API.
selectedRows en plantilla da error de tipos
Usa tableRef.value?.selectedRows en script (p. ej. con computed), no tableRef.selectedRows en el template.
Stop no cancela la petición HTTP
- Camino A (default): tu
fetchdebe reenviarsignalAbortal cliente (fetch, axios como{ signal: signalAbort }, etc.). - Camino B (adapter): registra
configureTableBuilder({ fetchAbort: … })en el boot (ofetchAbort: { useStore: true }) y comprueba que tu capa HTTP inyecte el mismoAbortSignaldel store.configureTableFetchAbortes la API de bajo nivel equivalente. Si axios lanzamessage: "canceled", incluyeisAbortErroren el adapter (ver README § Cancelación).
Errores del endpoint no llegan al formulario de filtros
Implementa onFetchError y mapea el cuerpo del error con selectFormStore(formName).setErrors(...) según el contrato de tu API. Comprueba que formName coincida con el del form de filtros.
MIT © Benjamín Olvera R.