Table Builder para Quasar

🇺🇸 English | 🇧🇷 Versão em português

npm version license Vue Quasar TypeScript Node

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

Flujo base recomendado:

  1. Definir tipos de filtros F y fila R.
  2. Configurar campos de filtro con Form Builder (defineForm o filters.form inline).
  3. Crear la tabla con defineTable<F, R>({ ... }) desde useTableBuilder().
  4. 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.

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-tablescripts/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.configboot: [] 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.mjsrunBorMakeTable 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:

Reglas de naming:

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.

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:

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.

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.allowUserCancelfetchAbort.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

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

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

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

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

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.