@benjaminor-dev/quasar-app-extension-table-builder
v0.2.18
Published
Declarative data tables for Quasar — filters via Form Builder, server pagination, column slots
Downloads
4,197
Maintainers
Readme
Table Builder para Quasar
🇺🇸 English | 🇪🇸 Versión en español
Extensão Quasar para tabelas declarativas em apps Vue 3 + Quasar: listagens com filtros (via Form Builder), paginação servidor/cliente, ordenação no backend, seleção de linhas, slots de coluna e consultas canceláveis.
Índice
- Em 30 segundos
- O que é e o que inclui
- Requisitos e compatibilidade
- Instalação em app Quasar
- O que a extensão adiciona ao host
- Defaults globais (boot)
- Comandos scaffold no host
- Uso rápido
- Idioma (i18n)
- TypeScript no seu IDE
- Configuração de
defineTable - Filtros e busca
- Paginação
- Ordenação
- Seleção de linhas
fetch, cancelamento e hooks- Slots
- Ref de
TableBuilder - Tipos TypeScript exportados
- Extensões relacionadas
- Sessão e store Pinia
- Entrypoints públicos
- Troubleshooting
Em 30 segundos
Fluxo base recomendado:
- Definir tipos de filtros
Fe linhaR. - Configurar campos de filtro com Form Builder (
defineFormoufilters.forminline). - Criar a tabela com
defineTable<F, R>({ ... })a partir deuseTableBuilder(). - Renderizar
<TableBuilder ref="tableRef" :config="config" />com slots#column-*e#toolbar.
A extensão registra boot e CSS no projeto host automaticamente após quasar ext add.
O que é e o que inclui
Table Builder orquestra listagens: toolbar, filtros Form Builder, tabela, paginação e fetch. Não substitui Form Builder — os filtros são sempre um formulário FB.
defineTable<F, R>+<TableBuilder />; filtrosdrawer|inlineoufilters: false.- Paginação
server|client; ordenação; seleção de linhas; slots#column-*/#toolbar. - Ref imperativa (
reload,resetFilters,cancelFetch, …) eselectTableStore(Pinia por tabela). persistDataopcional; cancelamento viasignalAbortou abort global no boot.- Defaults no boot (
configureTableBuilder);filterUitematiza botões, cabeçalho do drawer, chips e painel porlight/dark. - Grade e paginação via
DataTable(@benjaminor-dev/datatable) — cards no mobile< sm. - Defaults no boot (
configureTableBuilder); scaffoldbor:make-table. - i18n integrado
en,es,pt— segue Quasar$q.lang; overrides no boot (locale,messages).
Requisitos e compatibilidade
| 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 — obrigatória | | Form Builder | @benjaminor-dev/form-builder ^0.9.0 — obrigatório | | moment | ^2.30.1 (peer; usado ao formatar chips de filtros com datas/horas) | | Formato do pacote | ES modules | | CLI recomendado | @quasar/app-vite ^2.x ou ^3.x |
O boot de Pinia do host deve rodar antes de Form Builder e Table Builder.
Instalação em app Quasar
Instale DataTable e Form Builder antes de Table Builder (peers obrigatórios):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder
quasar ext add @benjaminor-dev/table-builderRemover:
quasar ext remove @benjaminor-dev/table-builderO que a extensão adiciona ao host
| Recurso | Caminho 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 também por esta extensão) |
| Scripts scaffold | bor:make-table → scripts/bor/bor-make-table.mjs |
| Defaults boot (host) | src/boot/bor/bor-table-builder-defaults.ts — criado no install; Skip/Overwrite se já existir |
Install (quasar ext add / invoke): cria src/boot/bor/bor-table-builder-defaults.ts (ou .js) e registra em quasar.config → boot: [] automaticamente. Não é preciso editar manualmente numa instalação normal. O boot do pacote (boot/store) e CSS são injetados pela extensão em cada dev/build.
Remove (quasar ext remove): remove o script scaffold, o defaults boot, sua entrada em quasar.config e bor:make-table em package.json (se ainda apontar para o arquivo do install).
Se você apagar o arquivo de defaults, a app continua com os built-ins da biblioteca (fetchOnMount: false, pageSize: 10, etc.).
Ordem de boots (esta extensão): Pinia → DataTable (CSS) → Form Builder → boot store TB → bor/bor-table-builder-defaults → resto.
Defaults globais (boot)
Evite repetir filters, pagination, fetchOnMount, persistData e abort em cada defineTable. Prioridade: defineTable → boot (configureTableBuilder) → built-in da biblioteca.
Após quasar ext add, revise src/boot/bor/bor-table-builder-defaults.ts — template com fetchAbort e opções comentadas (pagination, persistData). Defaults built-in: JSDoc @default nos tipos.
// src/boot/bor/bor-table-builder-defaults.ts (trecho)
import { configureTableBuilder } from "@benjaminor-dev/quasar-app-extension-table-builder";
configureTableBuilder({
filters: { layout: "drawer", openFiltersOnMount: false, showFilterChips: true },
fetchOnMount: false,
// persistData: true, // descomente para cache 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 — veja arquivo gerado no install
});| Opção boot | Descrição |
| --- | --- |
| filters | Defaults do painel (sem form; o formulário fica em cada tabela) |
| filterUi.buttonStyle | Cores e variantes de Filtros / Buscar / Limpar / Stop por tema (light / dark) |
| filterUi.panelHeader | Cabeçalho do drawer (color, textColor Quasar por tema) |
| filterUi.chips | Chips de filtros ativos (color, textColor por tema) |
| filterUi.chipsPanel | Borda e fundo do contêiner de chips (borderColor, backgroundColor CSS por tema) |
| pagination | pageSize, placement, scrollOnPageChange, pageSizeOptions |
| fetchOnMount | Carga inicial ao montar (default biblioteca: false) |
| persistData | Conservar sessão Pinia ao desmontar a tabela |
| onFetchError | Hook global de erros de fetch |
| fetchAbort | Abort global opcional (useTableFetchAbortStore ou adapter próprio) + allowUserCancel |
Use configureTableBuilderDefaults se você só precisa de defaults sem registrar fetchAbort.
Prioridade de filterUi: defineTable({ filterUi }) → boot → built-in. O tema ativo segue $q.dark do host.
Cancelamento detalhado: § Cancelamento.
Comandos scaffold no host
A extensão registra bor:make-table no host (scripts/bor/bor-make-table.mjs → runBorMakeTable em /scaffold). Execute npm run bor:help (Form Builder) para ver todos os geradores instalados.
Tabelas (bor:make-table)
npm run bor:make-table -- <nombre-o-ruta>Exemplo — criar UsuariosTable.vue:
npm run bor:make-table -- usuariosResultado:
src/components/TableBuilder/UsuariosTable.vuetableName: "UsuariosTable"(PascalCase, alinhado ao arquivo)- Tipos
UsuariosFilters/UsuarioRowy variableusuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable")
Regras de naming:
- O nome do arquivo é normalizado para PascalCase e termina em
Table.vue. - Se você enviar
UsuariosTable, não duplica o sufixo. usuarios→UsuariosTable.vue;personas/equipo→personas/EquipoTable.vue.- Você pode forçar overwrite com
--force(ou-f). - O rascunho inclui filtros drawer, colunas com slots (
icon,actions),selectTableStoreedeleteRowviapatchItems. - Template override no 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: "Usuários",
columns: [
{ name: "name", label: "Nome", field: "name", align: "left", sortable: true },
{ name: "role", label: "Papel", 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: "Papel",
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 e pt. Por padrão os textos de toolbar, drawer, estados vazios e skeleton seguem o idioma do Quasar ($q.lang.isoName); se o Quasar não tiver idioma, o fallback é en.
Resolução de locale: boot locale → prefixo de $q.lang.isoName (es* → es, pt* → pt) → en.
| Opção | Quando usar |
| --- | --- |
| framework.lang em quasar.config.ts | Recomendado — alinha Table Builder ao resto do Quasar |
| locale: 'pt' em boot | Fixar idioma (en | es | pt); ignora mudanças de $q.lang |
| messages: { search: '…' } em boot | Alterar um texto sem fixar locale |
Exemplo em quasar.config.ts (português para toda a app):
// quasar.config.ts
framework: {
lang: "pt-BR",
},Template boot (após install):
configureTableBuilder({
// locale: "pt", // fixa só o idioma do Table Builder
// messages: { search: "Consultar" },
});Chaves i18n: botões (filters, search, refresh, stopQuery, clear), drawer (filtersPanelTitle), estados vazios (emptyNotQueried*, noResults*, fetchCancelled*), tooltip de busca (searchGateDefault), skeleton (loading*Title, loading*Hint) e aria (filtersAppliedAria, removeFilterAria com {label}).
Prioridade de mensagens: boot messages funde sobre o catálogo built-in do locale ativo.
TypeScript no seu IDE
A config da tabela é tipada de ponta a ponta: deixe o IDE guiar você.
- Importe de
@benjaminor-dev/quasar-app-extension-table-builder/types(TableBuilderConfig,TableColumnDef,TableFetchContext, …). defineTable<MeusFiltros, MinhaLinha>({ ... })— os genéricos tipamfilters, o contexto defetche as linhas nos slots.- Ctrl+Space (Windows/Linux) ou Cmd+Space (macOS) dentro de
defineTablemostra colunas,pagination,filters, hooks, etc. - No callback
fetch, o parâmetro incluifilters,page,pageSize,sortBy,descendinge, se precisar,signalAbort— tudo autocompletado conforme seus genéricos. - Filtros: os
fieldsdo formulário são os mesmos tipos do Form Builder; combine com@benjaminor-dev/quasar-app-extension-form-builder/typesse compartilhar modelos. - Em
configureTableBuilder({ filterUi: { ... } }), Ctrl+Space / Cmd+Space listabuttonStyle,panelHeader,chips,chipsPanele campos por tema (light/dark). - No boot,
localeaceita"en" \| "es" \| "pt";messagesaceitaPartial<TableBuilderMessages>(toolbar, drawer, estados vazios, skeleton). - Override pontual em
defineTable({ filterUi })para uma única tabela.
Se uma opção não aparecer no autocompletar, revise o import de /types antes de buscar no README.
Configuração de defineTable
Campos principais de defineTable<F, R>():
| Campo | Descrição |
| --- | --- |
| tableName | Identificador único (nome do form de filtros derivado se não passar formName). |
| title, hint, prependIcon, helpTooltip | Apresentação na toolbar. |
| columns | Definição de colunas (name, label, field?, format, sortable, align, …). sortable por padrão false. |
| selection | single | multiple para checkboxes; omitir para desativar. Ref: selectedRows, clearSelection(). |
| pagination | Modo, pageSize, placement y defaultSort (server). |
| filters | Panel Form Builder o false. |
| fetch | Função async que retorna linhas (e metadados no modo server). |
| fetchOnMount | Carga inicial ao montar (respeita regras de busca). |
| persistData | true conserva a sessão Pinia ao desmontar a tabela (navegar e voltar na SPA). |
| persistDataConfig | Ajuste fino (reuseOnMount). Ganha sobre persistData se definir ambos. |
| onBeforeFetch, onAfterFetch, onFetchError | Hooks do ciclo de consulta. |
| allowUserCancel | Stop / cancelFetch() para o usuário (override do boot). Default: false. Boot global: só via fetchAbort.allowUserCancel. |
| filterUi | Override de botões, cabeçalho drawer, chips e painel de chips por tema (light / dark). |
Colunas (columns)
defineTable não exige ordem fixa: o runtime normaliza a lista ao resolver a config.
| Comportamento | Detalhe |
| --- | --- |
| Coluna # | Se não definir name: "index", é injetada no início com label: "#" e field: "index". Largura compacta por padrão. O fetch não deve retorná-la: é calculada em cada página (1, 2, … conforme offset). |
| field | Opcional em colunas só-slot (#column-{name}): omita se o valor vier do slot. Se declarar, usa para value/text e ordenação em client. |
| label | Opcional em colunas só-slot; se faltar, o cabeçalho fica vazio. |
| align | Por padrão "left". Use "center" ou "right" onde aplicável (ex.: ícones ou ações). |
| Ordem | As demais colunas (incluindo actions) aparecem na mesma ordem de columns. |
| sortable | Por padrão false. Marque sortable: true nas colunas que ordenar; em server use sortBy / descending no fetch. |
Para personalizar a coluna índice (largura, alinhamento, ocultá-la), declare explicitamente com name: "index".
Filtros e busca
Layout
| Valor | Comportamento |
| --- | --- |
| drawer (default) | Formulário em painel lateral; toolbar com grupo Filtros + lupa/refresh. |
| inline | Painel sobre a tabela com cabeçalho Filtros, formulário, Buscar / Limpar. Os chips (se ativos) ficam abaixo do formulário. |
Regras para habilitar busca (AND)
filters: {
minActiveFilters: 1,
canSearch: (filters) => Boolean(filters.search?.trim()) || filters.role != null,
searchDisabledMessage: "Indique busca ou papel para consultar.",
showFilterChips: true,
form: { fields: [...] },
}| Opção | Default | Descrição |
| --- | --- | --- |
| minActiveFilters | 0 | Mínimo de filtros com valor ativo. |
| canSearch | true | boolean ou (filters: F) => boolean. |
| searchDisabledMessage | "Ajuste os filtros para consultar." | Tooltip quando falha canSearch ou minActiveFilters. |
| showFilterChips | true | Chips da última busca bem-sucedida. |
| resetFiltersOnMount | false | Restaura filtros ao montar. |
| openFiltersOnMount | false | Abre drawer ao montar (true | "desktop"). |
Os chips refletem o snapshot da última busca bem-sucedida, não o formulário ao vivo.
Formulário de filtros (filters.form)
Mesma forma que defineForm do Form Builder:
fields— obrigatório.defaultValues— opcional: só overrides ou pré-carga; não é preciso listar todo o modelo.formName— opcional; por padrãotable:{tableName}:filters.
filters: {
form: {
fields: [{ type: "InputSearch", model: "search", label: "Buscar" }],
defaultValues: { role: "admin" },
},
}Você também pode passar o resultado de defineForm(...) se preferir definir o formulário fora de defineTable.
Toolbar com drawer (grupo fundido)
Um QBtnGroup outline agrupa os botões da toolbar somente quando Filtros e Buscar/Stop compartilham variante outline. Se misturar variantes (ex.: outline + flat), renderiza como botões separados.
- Filtros: abre o drawer.
- Lupa / refresh (mesmo botão): primeira busca se ainda não houver linhas; refresh se já houver resultados (recarrega sem resetar página).
- Stop: durante a carga, cancela a consulta — só se
allowUserCancelestiver ativo (boot ou tabela).
UI de filtros por tema (filterUi)
| Bloco boot | O que controla |
| --- | --- |
| buttonStyle | Botões Filtros / Buscar / Limpar / Stop (*Color, *Variant por tema) |
| panelHeader | Cabeçalho do drawer (color, textColor Quasar) |
| chips | QChip de filtros ativos (color, textColor) |
| chipsPanel | Borda e fundo do contêiner 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" } },
},Paginação
pagination: {
mode: "server",
pageSize: 25,
placement: "auto",
scrollOnPageChange: "auto",
defaultSort: { sortBy: "name" },
}| placement | Comportamento |
| --- | --- |
| bottom | Só embaixo (default). |
| top | Só em cima. |
| both | Em cima e embaixo. |
| auto | Embaixo sempre; em cima só se a página atual mostrar ≥15 linhas visíveis (conta linhas renderizadas, não o valor do seletor «linhas por página»). |
| scrollOnPageChange | Comportamento |
| --- | --- |
| auto (default) | Scroll ao título no mobile ou quando a paginação está só embaixo. |
| true | Sempre após mudar de página. |
| false | Nunca. |
| defaultSort | Ordem inicial no modo server (sortBy, descending?). Ignorado em client salvo se QTable receber o estado inicial via paginação interna. |
Modo server: fetch deve retornar { rows, pagination } com pelo menos total, page e pageSize. Os campos from, to e lastPage são opcionais (o runtime calcula se faltarem). Ao clicar um cabeçalho sortable, reconsulta com sortBy e descending (página 1).
Modo client: fetch pode retornar R[] ou { rows }; pagina e ordena QTable em memória.
Ordenação
| Modo | Comportamento |
| --- | --- |
| client | Marque colunas sortable: true onde quiser. QTable ordena as linhas carregadas sem novo fetch. |
| server | Marque sortable: true nas colunas que sua API suporta. Ao clicar o cabeçalho executa fetch na página 1 com sortBy (nome da coluna) e descending. |
Ordem inicial no servidor:
pagination: {
mode: "server",
defaultSort: { sortBy: "name", descending: false },
}sortBy e descending também chegam a onBeforeFetch com a mesma forma que em fetch.
Seleção de linhas
const config = defineTable<Filters, Row>({
// ...
selection: "multiple", // ou "single"
});| Valor | UI |
| --- | --- |
| "multiple" | Checkbox por linha |
| "single" | Radio por linha |
| Omitir / false | Sem seleção |
A seleção é limpa após cada fetch bem-sucedido (busca, paginação, ordenação ou reload).
No script, leia as linhas pela ref:
const tableRef = useTableRef();
const selectedCount = computed(() => tableRef.value?.selectedRows?.length ?? 0);No template, use um computed (não acesse tableRef.selectedRows diretamente: vue-tsc não desembrulha o ShallowRef no template).
fetch, cancelamento e hooks
Contexto de fetch
type TableFetchContext<F> = {
filters: F;
page: number;
pageSize: number;
sortBy: string | null;
descending: boolean;
signalAbort?: AbortSignal;
};Retorna linhas (e metadados no 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 },
}),Cancelamento — escolha um caminho
O cancelamento não passa por onFetchError.
Quem pode parar a consulta?
Por padrão ninguém (sem botão Stop). Ative no boot (fetchAbort.allowUserCancel) ou por tabela (defineTable.allowUserCancel + signalAbort no fetch).
Prioridade: defineTable.allowUserCancel → fetchAbort.allowUserCancel (boot) → false.
| Boot fetchAbort.allowUserCancel | Tabela | Stop / cancelFetch() |
| --- | --- | --- |
| true | (sem definir) | ✅ todas |
| true | false | ❌ só essa tabela |
| false / sem boot | true | ✅ só essa tabela |
| false | (sem definir) | ❌ nenhuma |
// Boot — todas as tabelas com Stop (abort global do host)
configureTableBuilder({
fetchAbort: {
allowUserCancel: true,
getSignal: () => useAbortStore().getSignal(),
abort: () => useAbortStore().abort(),
},
});
// Uma tabela sem Stop mesmo com boot ativo
defineTable({ tableName: "Logs", allowUserCancel: false, ... });
// Só esta tabela com Stop (Caminho A portátil)
defineTable({
tableName: "Usuarios",
allowUserCancel: true,
fetch: ({ signalAbort, ...ctx }) => UsersService.list(ctx.filters, signalAbort),
});Uma nova busca ou mudança de página continua abortando a requisição anterior mesmo com Stop desativado (evita sobreposições).
Caminho A — portátil (default)
Table Builder cria um AbortController por consulta. Reencaminhe signalAbort ao cliente HTTP no 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 };
},Se o service vive fora do componente, passe como argumento:
fetch: ({ filters, signalAbort }) => UsersService.list(filters, signalAbort),Recomendado com várias tabelas em paralelo (cada consulta tem seu próprio sinal).
Caminho B — abort global do host (sem signalAbort em cada tabela)
Se sua app já centraliza o cancelamento (Pinia + axios que auto-injeta signal em cada requisição), registre o adaptador uma vez no boot do host.
Opção incluída — store Pinia da extensão (useTableFetchAbortStore):
configureTableBuilder({
fetchAbort: {
useStore: true,
allowUserCancel: false, // recomendado: false
},
});Injete useTableFetchAbortStore().getSignal() na sua camada HTTP (axios, etc.).
Opção custom — store próprio do 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"),
},
});
});Sua camada HTTP deve continuar injetando o mesmo signal do store quando a requisição não traz um explícito (como em ApiUtils.makeNewConfig).
Então o fetch da tabela não precisa usar signalAbort:
fetch: ({ filters, page, pageSize }) =>
UsersService.index(filters, { page, pageSize }),Table Builder chamará abort() do store ao cancelar ou iniciar outra consulta; axios captará como antes.
Nota: um abort store global implica uma consulta abortável ativa na app. Com várias tabelas carregando ao mesmo tempo, use o caminho A.
Hooks (controle opcional)
| Hook | Quando | Comportamento |
| --- | --- | --- |
| onBeforeFetch | Antes de iniciar | Retorna false para não consultar (validação prévia). |
| onAfterFetch | Após sucesso | Linhas já normalizadas (index incluído). |
| onFetchError | Erro real (rede, HTTP, exceção) | Se definir, não há notify por padrão. |
Cancelamento (AbortError, axios canceled, …) não dispara onFetchError.
Exemplo — erros de validação nos filtros (adapte ao contrato da sua 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: "Erro ao 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 após a consulta
- Sem linhas após busca → mensagem contextual (com/sem filtros aplicados).
- Busca cancelada manualmente → «Consulta cancelada» (conforme origem da carga).
- Carga → skeleton com mensagem conforme motivo (
search,pagination,sort,refresh, …).
Slots
<TableBuilder :config="config">
<template #toolbar>
<q-chip v-if="selectedCount > 0" dense color="primary">
{{ selectedCount }} selecionados
</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 | — (ações extras à direita do 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 | Descrição |
| --- | --- |
| reload(options?) | Executa fetch novamente. resetFilters: true restaura filtros e vai para página 1 (salvo resetPage: false explícito). |
| resetFilters() | Restaura filtros aos defaults (sem recarregar). |
| openFilters() / closeFilters() | Drawer de filtros. |
| cancelFetch() | Cancela o fetch em curso (abort do signalAbort ativo). |
| clearSelection() | Desseleciona todas as linhas. |
| invalidate() | Esvazia linhas e metadados no Pinia. Útil antes de reload() após CRUD externo se não usar patchItems. |
| loading | true enquanto há um fetch em curso. |
| selectedRows | Linhas selecionadas quando selection está ativo (limpa após cada fetch bem-sucedido). |
Tipos TypeScript exportados
Do entrypoint principal e /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";Extensões relacionadas
| Extensão | Relação |
| --- | --- |
| @benjaminor-dev/datatable | Obrigatória. Provê DataTable, barra de paginação e estilos de grade usados por TableBuilder. |
| @benjaminor-dev/form-builder | Obrigatório. Filtros, store e validação via defineForm / FormBuilder. |
Table Builder não inclui gerador de formulários próprio: reutiliza Form Builder para não duplicar inputs nem regras.
Ordem 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)Em quasar.config você verá o defaults boot de cada extensão (ex.: bor/bor-table-builder-defaults); os boots do pacote são injetados pela extensão ao compilar — não aparecem como entradas curtas na sua config.
Sessão e store Pinia
Uma sessão por tableName no Pinia (mesmo padrão do store do Form Builder para filtros).
Acesso: useTableBuilder().selectTableStore(tableName) ou selectTableStore(tableName) importado do pacote.
selectTableStore<R>(tableName)
Visão tipada da sessão. O mesmo tableName em outro .vue ou composable compartilha linhas e metadados.
const { defineTable, selectTableStore } = useTableBuilder();
const usuariosTableStore = selectTableStore<UsuarioRow>("UsuariosTable");
// Após apagar na API, remover linha sem refetch
await api.delete(id);
usuariosTableStore.patchItems((rows) => rows.filter((r) => r.id !== id));
// Leitura reativa
if (usuariosTableStore.hasItems.value) {
const rows = usuariosTableStore.items.value;
}| Membro | Descrição |
| --- | --- |
| tableName | Identificador (defineTable.tableName) |
| items | Linhas visíveis (reativo) |
| hasItems | true se houver pelo menos uma linha |
| pagination | Metadados no modo server |
| appliedFilters | Snapshot da última busca bem-sucedida |
| hasSearched | true após pelo menos um fetch bem-sucedido |
| getItems() | Snapshot não reativo |
| setItems / patchItems | Mutação local sem refetch |
| clearItems() | Esvazia linhas e metadados |
| remove() | Remove a sessão Pinia |
Tipo exportado: TableSessionHandle<R> em /types.
Filtros continuam no Form Builder (table:{tableName}:filters por padrão).
persistData (opcional)
Conserva a sessão Pinia ao desmontar <TableBuilder /> (navegar para outra rota e voltar).
const config = defineTable<Filters, Row>({
tableName: "usuarios",
columns: [...],
fetch: async ({ filters, page, pageSize, signalAbort }) => ({ rows, pagination: { ... } }),
persistData: true,
fetchOnMount: false, // vazio até a primeira busca
});| Comportamento | Detalhe |
| --- | --- |
| persistData: true | Ao sair da rota, o store conserva linhas, página e snapshot de filtros aplicados. |
| persistData: false (default) | Ao desmontar, remove a sessão dessa tableName. |
| Navegar e voltar | Com persistData: true, hidrata do Pinia (reuseOnMount: true por padrão). |
| Recarregar aba (F5) | Perde-se (só memória da sessão SPA). |
| reload() / buscar | Sempre vão à rede. |
| tableRef.invalidate() | Esvazia linhas e metadados no store (útil antes de reload() após CRUD externo). |
Ajuste fino: persistDataConfig: { reuseOnMount: false } força fetch ao montar mesmo com dados no 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, colunas, fetch, boot e sessão (TableBuilderConfig, TableFetchContext, TableSessionHandle, ConfigureTableBuilderOptions, …) — autocompletar no 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 pela extensão) |
| @benjaminor-dev/quasar-app-extension-table-builder/main.css | Estilos de tabela, drawer, paginação e skeleton |
Troubleshooting
Os textos da UI continuam em inglês
- Configure
framework.langemquasar.configou fixe a extensão:configureTableBuilder({ locale: "pt" })emsrc/boot/bor/bor-table-builder-defaults.ts.
O defaults boot não aparece em quasar.config
Em condições normais o install registra sozinho. Se faltar (projeto antigo ou edição manual):
npx quasar ext invoke @benjaminor-dev/table-builderErro de Pinia / store de filtros ou tabelas
- Pinia do host deve ir antes de Form Builder e Table Builder.
- Instale e faça boot de Form Builder e DataTable antes de Table Builder.
- Se vir erros de sessão não inicializada, verifique se
<TableBuilder />está montado e setableNameé único por tabela.
fetch em modo server sem metadados
Em pagination.mode: "server", retorne { rows, pagination } com pelo menos total, page e pageSize. Não é preciso calcular from/to/lastPage manualmente.
return { rows: items, pagination: { total, page, pageSize } };Botão buscar desabilitado
Revise minActiveFilters e canSearch. O tooltip indica o motivo. Os valores são lidos do formulário ao vivo, não do último snapshot de chips.
Os chips não coincidem com o que vejo no drawer
É esperado: chips = última busca bem-sucedida. Abra filtros, altere valores e clique buscar para atualizar.
Cancelar durante paginação vs busca
- Busca cancelada sem linhas anteriores → mensagem «Consulta cancelada».
- Paginação, ordenação ou remover chip cancelada → reverte página/ordem/filtro e mantém linhas atuais.
Ordenar em server não faz nada
Verifique se a coluna tem sortable: true e se seu fetch usa sortBy / descending ao chamar a API.
selectedRows no template dá erro de tipos
Use tableRef.value?.selectedRows no script (ex.: com computed), não tableRef.selectedRows no template.
Stop não cancela a requisição HTTP
- Caminho A (default): seu
fetchdeve reencaminharsignalAbortao cliente (fetch, axios como{ signal: signalAbort }, etc.). - Caminho B (adapter): registre
configureTableBuilder({ fetchAbort: … })no boot (oufetchAbort: { useStore: true }) e verifique se sua camada HTTP injeta o mesmoAbortSignaldo store.configureTableFetchAborté a API de baixo nível equivalente. Se axios lançarmessage: "canceled", incluaisAbortErrorno adapter (ver README § Cancelamento).
Erros do endpoint não chegam ao formulário de filtros
Implemente onFetchError e mapeie o corpo do erro com selectFormStore(formName).setErrors(...) conforme o contrato da sua API. Verifique se formName coincide com o do form de filtros.
MIT © Benjamín Olvera R.
