npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

Readme

Table Builder para Quasar

🇺🇸 English | 🇪🇸 Versión en español

npm version license Vue Quasar TypeScript Node

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

Fluxo base recomendado:

  1. Definir tipos de filtros F e linha R.
  2. Configurar campos de filtro com Form Builder (defineForm ou filters.form inline).
  3. Criar a tabela com defineTable<F, R>({ ... }) a partir de useTableBuilder().
  4. 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 />; filtros drawer | inline ou filters: false.
  • Paginação server | client; ordenação; seleção de linhas; slots #column-* / #toolbar.
  • Ref imperativa (reload, resetFilters, cancelFetch, …) e selectTableStore (Pinia por tabela).
  • persistData opcional; cancelamento via signalAbort ou abort global no boot.
  • Defaults no boot (configureTableBuilder); filterUi tematiza botões, cabeçalho do drawer, chips e painel por light / dark.
  • Grade e paginação via DataTable (@benjaminor-dev/datatable) — cards no mobile < sm.
  • Defaults no boot (configureTableBuilder); scaffold bor: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-builder

Remover:

quasar ext remove @benjaminor-dev/table-builder

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

Resultado:

  • src/components/TableBuilder/UsuariosTable.vue
  • tableName: "UsuariosTable" (PascalCase, alinhado ao arquivo)
  • Tipos UsuariosFilters / UsuarioRow y variable usuariosTableStore = 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.
  • usuariosUsuariosTable.vue; personas/equipopersonas/EquipoTable.vue.
  • Você pode forçar overwrite com --force (ou -f).
  • O rascunho inclui filtros drawer, colunas com slots (icon, actions), selectTableStore e deleteRow via patchItems.
  • 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 tipam filters, o contexto de fetch e as linhas nos slots.
  • Ctrl+Space (Windows/Linux) ou Cmd+Space (macOS) dentro de defineTable mostra colunas, pagination, filters, hooks, etc.
  • No callback fetch, o parâmetro inclui filters, page, pageSize, sortBy, descending e, se precisar, signalAbort — tudo autocompletado conforme seus genéricos.
  • Filtros: os fields do formulário são os mesmos tipos do Form Builder; combine com @benjaminor-dev/quasar-app-extension-form-builder/types se compartilhar modelos.
  • Em configureTableBuilder({ filterUi: { ... } }), Ctrl+Space / Cmd+Space lista buttonStyle, panelHeader, chips, chipsPanel e campos por tema (light / dark).
  • No boot, locale aceita "en" \| "es" \| "pt"; messages aceita Partial<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.
  • defaultValuesopcional: só overrides ou pré-carga; não é preciso listar todo o modelo.
  • formName — opcional; por padrão table:{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 allowUserCancel estiver 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.allowUserCancelfetchAbort.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ãonotify 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.lang em quasar.config ou fixe a extensão: configureTableBuilder({ locale: "pt" }) em src/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-builder

Erro 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 se tableName é ú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 fetch deve reencaminhar signalAbort ao cliente (fetch, axios como { signal: signalAbort }, etc.).
  • Caminho B (adapter): registre configureTableBuilder({ fetchAbort: … }) no boot (ou fetchAbort: { useStore: true }) e verifique se sua camada HTTP injeta o mesmo AbortSignal do store. configureTableFetchAbort é a API de baixo nível equivalente. Se axios lançar message: "canceled", inclua isAbortError no 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.