Form Builder para Quasar
🇺🇸 English | 🇪🇸 Versión en español
Extensão Quasar para construir formulários declarativos em apps Vue 3 + Quasar.
Í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
- Guia de configuração de formulários
- Field presets (campos reutilizáveis)
- API pública
- Catálogo de campos suportados
- Diretivas registradas
- Store e mapa conceitual de estado/erros
- SlotField e uso de refs
- Extensões opcionais relacionadas
- Customizar template scaffold
- Entrypoints públicos
- Troubleshooting
Em 30 segundos
Fluxo base recomendado:
- Definir tipo de payload.
- Construir config com
useFormBuilder().defineForm(...). - Renderizar
<FormBuilder>com essa config. - Tratar submit/reset por eventos.
A extensão instala boots, diretivas e scripts no projeto host para uso imediato.
O que é e o que inclui
Form Builder monta formulários declarativos sobre Quasar: um modelo Pinia por formName, campos tipados e validação reutilizável.
defineForm+<FormBuilder>— config tipada; props dinâmicas comFormAwareValue.- ~30 tipos de campo (texto, datas, selects locais/remotos, arquivos,
FormList, …) — catálogo; detalhe de props no IDE. inputRules,requiredOn,selectFormStore, diretivasv-form-builder-*.FormListeInputFileMultipleusamDataTable(@benjaminor-dev/datatable); para listagens declarativas use Table Builder.- Defaults globais em boot (
configureFormBuilderDefaults); scaffoldbor:help,bor:make-form,bor:make-formlist,bor:make-field.
Requisitos e compatibilidade
| Item | Valor |
|---|---|
| Node | >=20.0.0 |
| Quasar | ^2.6.0 |
| Vue | ^3.4.18 |
| Pinia | ^2.0.11 || ^3.0.0 |
| moment | ^2.30.1 |
| DataTable | @benjaminor-dev/datatable ^0.1.0 — peer obrigatório |
| Formato de saída | ES modules |
O boot de Pinia do host deve rodar antes do boot de store desta extensão.
Instalação em app Quasar
Instale DataTable antes do Form Builder (peer obrigatório — FormList, InputFileMultiple e o check de compatibilidade exigem):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder
Extensão recomendada: pré-visualização de arquivos
Com InputFile / InputFileMultiple, convém instalar dialog-file-preview (opcional). showPreview vem true por padrão: com a extensão, ver/baixar abre o modal; sem ela, o campo segue como QFile padrão.
quasar ext add @benjaminor-dev/dialog-file-preview
Ordem de boots (stack completo): ver Extensões opcionais.
Remover extensão:
quasar ext remove @benjaminor-dev/form-builder
O que a extensão adiciona ao host
| Recurso | Caminho |
|---|---|
| Boot store | ~@benjaminor-dev/quasar-app-extension-form-builder/boot/store |
| Boot diretivas | ~@benjaminor-dev/quasar-app-extension-form-builder/boot/directives |
| CSS Form Builder | ~@benjaminor-dev/quasar-app-extension-form-builder/main.css |
| CSS DataTable | ~@benjaminor-dev/quasar-app-extension-datatable/main.css (peer; registrado também por esta extensão) |
| Scaffold | bor:help, bor:make-form, bor:make-formlist, bor:make-field |
Scripts host (scripts/bor/) |
bor-help.mjs, bor-make-form.mjs, bor-make-formlist.mjs, bor-make-field.mjs — prefixo bor- do ecossistema @benjaminor-dev |
| Defaults (host) | src/boot/bor/bor-form-builder-defaults.ts — criado no install |
Install (quasar ext add / invoke): cria src/boot/bor/bor-form-builder-defaults.ts (ou .js) e registra em quasar.config → boot: [] automaticamente. Não é preciso editar manualmente numa instalação normal. Os boots do pacote (boot/store, boot/directives) e CSS são injetados pela extensão em cada dev/build (não aparecem como entradas curtas em quasar.config).
Remove (quasar ext remove): remove scaffolds gerados, o defaults boot, sua entrada em quasar.config e os scripts npm bor:* desta extensão (somente se ainda apontarem para os arquivos do install).
Se apagar o defaults boot, o app segue com built-in (filled, stacked, lazy, md).
Ordem de boots (esta extensão): Pinia → DataTable (CSS) → boot store FB → bor/bor-form-builder-defaults → resto.
Defaults globais (boot)
Evite repetir fieldDesign, labelPosition, validationMode e gap em cada defineForm. Prioridade: defineForm / props de <FormBuilder> → boot (configureFormBuilderDefaults) → built-in (filled, stacked, lazy, md).
Após quasar ext add, revise src/boot/bor/bor-form-builder-defaults.ts — modelo completo com defaultFieldProps, comentários // recomendado, etc.
// src/boot/bor/bor-form-builder-defaults.ts (trecho)
import { configureFormBuilderDefaults } from "@benjaminor-dev/quasar-app-extension-form-builder";
configureFormBuilderDefaults({
fieldDesign: "filled",
labelPosition: "stacked",
validationMode: "lazy",
gap: "md",
// defaultFieldProps: { InputSelect: { clearable: true, fieldUi: { color: "primary" } } }
// defaultFormListUi: { addBtnColor: { light: "white", dark: "primary" } }
});
configureFormBuilder é alias de configureFormBuilderDefaults.
| Opção boot | Descrição |
|---|---|
fieldDesign |
Design global de campos (outlined, filled, …) |
labelPosition |
inner | stacked | top |
validationMode |
lazy (recomendado) | eager |
gap |
Espaciado vertical en <FormBuilder> |
defaultFieldProps |
Props estáticas por fields[].type (BootFieldPropsFor<"InputX"> em /types); inclui fieldUi (tokens visuais do input) |
defaultFormListUi |
Tokens visuais globais de FormList (toolbar, diálogos); override em props.listUi do field |
| Boot | defineForm |
Efetivo |
|---|---|---|
filled |
— | filled |
filled |
fieldDesign: "outlined" |
outlined só nesse form |
Comandos scaffold no host
A extensão registra scripts npm no host para formulários e formulários com lista (FormList). Cada runner em scripts/bor/bor-make-*.mjs delega na API npm /scaffold (runBorHelp, runBorMakeForm, …). Execute npm run bor:help para ver todos os scaffolds instalados no projeto, opções e guia de uso.
Ajuda (bor:help)
Lista os geradores instalados, a sintaxe com -- e as opções comuns (--force):
npm run bor:help
Formulários (bor:make-form)
Depois de -- vai o nome ou caminho do formulário a gerar.
npm run bor:make-form -- <nome-ou-caminho-do-formulario>
O argumento é normalizado para PascalCase e o arquivo gerado sempre termina em Form.vue.
Exemplo — criar um formulário de login (LoginForm.vue):
npm run bor:make-form -- login
Resultado:
src/components/FormBuilder/LoginForm.vue
Exemplo com subpasta — criar users/RegisterForm.vue e sobrescrever se já existir:
npm run bor:make-form -- users/register --force
Resultado:
src/components/FormBuilder/users/RegisterForm.vue
Regras de naming:
- O nome final do arquivo é normalizado para PascalCase.
- Siempre termina en
Form.vue. - Se enviar
RegisterForm, não duplicaFormForm. formNameno rascunho: PascalCase alinhado ao arquivo (ex.:LoginForm,TempForm).- Você pode forçar overwrite com
--force(ou-f). - O rascunho inclui
selectFormStore, botão de envio e (opcional) «Limpar (store)» viareset()do handle.
Formulários com lista (bor:make-formlist)
Gera um rascunho Vue com FormBuilder, um campo FormList embutido e botões de envio (SlotField) em src/components/FormBuilder/ — mesma convenção de caminho e nome que bor:make-form; só muda o template:
npm run bor:make-formlist -- <nome-ou-caminho-do-formulario>
Exemplo:
npm run bor:make-formlist -- temp
npm run bor:make-formlist -- users/TeamRegister
- Sempre termina em
Form.vue(ex.:temp→TempForm.vue, igual abor:make-form temp). - Se enviar
TeamRegisterFormouTeamRegisterList, não duplica sufixos. formNameno rascunho: PascalCase alinhado ao arquivo (ex.:TempForm).- Você pode forçar overwrite com
--force(ou-f). - Template override no host:
scripts/bor/stubs/bor-make-formlist.stub. - O rascunho inclui
selectFormStoree um botão «Esvaziar lista (store)» que limpa o array doFormListembutido.
Field presets (bor:make-field)
Gera um arquivo reutilizável em src/components/FormBuilder/Fields/ com defineFieldPreset — para selects, campos de servidor ou outros campos que você repete em muitos formulários.
npm run bor:make-field -- <NombrePreset> [FieldType]
| Entrada | Gera |
|---|---|
CountrySelect InputSelect |
Fields/CountrySelect.field.ts → export countrySelectField |
catalog/CitySelect InputSelectServer |
Fields/catalog/CitySelect.field.ts |
CountrySelect (sem tipo) |
Menu interativo no terminal para escolher o type |
selectserver (como tipo) |
Aceita o nome sem distinguir maiúsculas (InputSelectServer) |
--force / -f |
Sobrescreve se o arquivo já existir |
Exemplo — preset de país (select local):
npm run bor:make-field -- CountrySelect InputSelect
Edite o arquivo gerado (serviço, options, ícones) e use em qualquer defineForm — ver Field presets.
Template override no host: scripts/bor/stubs/bor-make-field.stub.
Uso rápido
Exemplo mínimo do cenário cadastro de cliente corporativo (identidade + envio). O exemplo integrado abaixo mostra o formulário de referência quase completo.
<script setup lang="ts">
import FormBuilder, {
useFormBuilder,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
type CorporateIdentityForm = {
legalName: string | null;
tradeName: string | null;
taxId: string | null;
brandColor: string | null;
corporateEmail: string | null;
acceptContract: boolean;
};
const { defineForm, inputRules } = useFormBuilder();
const config = defineForm<CorporateIdentityForm>({
formName: "corporateIdentityForm",
fieldDesign: "filled",
labelPosition: "stacked",
fields: [
{
type: "InputText",
model: "legalName",
label: "Razão social",
requiredOn: () => true,
rules: [
inputRules.legalTextExtendedRule(),
],
props: {
placeholder: "Ex. Acme Serviços Ltda.",
},
},
{
type: "InputText",
model: "tradeName",
label: "Nome fantasia",
readonlyOn: (form) => !form?.legalName,
props: {
placeholder: "Opcional — como a marca é conhecida",
},
},
{
type: "InputText",
model: "taxId",
label: "CNPJ / identificador fiscal",
requiredOn: (form) => !!form?.legalName,
rules: [
inputRules.rfc(),
],
props: {
placeholder: "Ex. ACME850101ABC",
maxLength: 13,
},
},
{
type: "InputColor",
model: "brandColor",
label: "Cor corporativa",
props: {
defaultColor: "#1976D2",
placeholder: "#RRGGBB",
},
},
{
type: "InputEmail",
model: "corporateEmail",
label: "E-mail corporativo",
requiredOn: () => true,
props: {
placeholder: "contato@empresa.com",
},
},
{
type: "ToggleButton",
model: "acceptContract",
label: "Aceito contrato quadro e tratamento de dados",
requiredOn: () => true,
},
{
type: "SlotField",
props: {
slotName: "actions",
},
},
],
});
function onSubmit(payload: CorporateIdentityForm) {
console.log("submit", payload);
}
</script>
<template>
<FormBuilder :config="config" @submit="onSubmit">
<template #slot-field-actions>
<div class="flex justify-end q-gutter-sm">
<q-btn type="reset" flat label="Limpar" />
<q-btn type="submit" color="primary" label="Enviar solicitação" />
</div>
</template>
</FormBuilder>
</template>
Idioma (i18n)
Integrado en, es e pt. Por padrão, textos de inputs, FormList e arquivos 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 Form Builder ao resto do Quasar |
locale: 'es' no boot |
Fixar idioma (en | es | pt); ignora mudanças de $q.lang |
messages: { formListAdd: '…' } no boot |
Alterar um texto sem fixar locale |
Exemplo em quasar.config.ts (espanhol para o app inteiro):
// quasar.config.ts
framework: {
lang: "es",
},
Modelo boot (após install):
configureFormBuilderDefaults({
// locale: "es", // fixa só o idioma do Form Builder
// messages: { formListAdd: "Adicionar" },
});
Chaves i18n: ações comuns (clear, save, cancel, preview), aria de inputs (showHidePassword, openDatePicker, clearField), toolbar do editor, chrome de FormList (formListAdd, formListValidationTitle, formListMinimized, …) e rejeições de arquivo (fileRejectionMaxSize, fileCapacityOverLimit com templates {max}, {name}, {reason}). Catálogo completo em FormBuilderMessages (/types).
Prioridade de mensagens: boot messages mescla sobre o catálogo built-in do locale ativo.
API em runtime: useFormBuilderI18n() retorna messages reativos conforme $q.lang.isoName. Para acesso imperativo (testes, código fora do Vue), use resolveFormBuilderMessages(isoName?) — mesma resolução boot → $q.lang → en.
import {
configureFormBuilderDefaults,
resolveFormBuilderMessages,
useFormBuilderI18n,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
configureFormBuilderDefaults({ locale: "es" });
const msgs = resolveFormBuilderMessages(); // locale do boot
// Dentro de um componente:
const { messages } = useFormBuilderI18n();
// messages.value.formListAdd, messages.value.clear, …
TypeScript no seu IDE
Não é preciso memorizar cada prop: a tipagem e o autocompletar mostram o contrato enquanto você escreve.
- Importe tipos de
@benjaminor-dev/quasar-app-extension-form-builder/types(ex.:FormAwareValue,BootFieldPropsFor,FieldUiConfigFor, tipos de cada input). defineForm<MeuPayload>({ ... })— o genérico fixa a forma do formulário; os callbacks*Onrecebem esse payload tipado.fieldsaceita um array (forma habitual) ou um callback({ preset }) => [...]quando você usa field presets compreset(...). Ambas as formas são válidas.- Dentro de
fields[], use Ctrl+Space (Windows/Linux) ou Cmd+Space (macOS):- em
type:você verá os inputs válidos (InputText,InputSelect, …); - em
props:as props públicas desse input, com JSDoc no tooltip ao passar o cursor.
- em
fieldUi(empropsou bootdefaultFieldProps[type]): tokens visuais por tipo — autocompletar comFieldUiConfigFor<"InputText">(cadatypeé independente).placeholder,hint, etc. ficam fora defieldUi.FormList→props.listUi: tokens do chrome da lista; boot global comdefaultFormListUi.- O catálogo completo de
typeestá emFieldTypes; o detalhe de props você tem no IDE ao escrever — o catálogo deste README é referência rápida, não substituto do autocompletar. - Componentes avulsos:
@benjaminor-dev/quasar-app-extension-form-builder/inputscom os mesmos tipos exportados. - No boot,
localeaceita"en" \| "es" \| "pt";messagesaceitaPartial<FormBuilderMessages>(inputs,FormList, arquivos, pickers).
Se uma prop não aparecer no autocompletar, provavelmente não existe na API pública ou o type do campo não a suporta.
Guia de configuração de formulários
Anatomia de um campo
Cada entrada de fields[] descreve um controle do formulário:
| Propriedade | Descrição |
|---|---|
type |
Tipo de campo (InputText, InputSelect, …) |
model |
Chave no modelo tipado do formulário |
label |
FormAwareValue<string> |
requiredOn / hideOn / disabledOn / readonlyOn / unmountOn |
Comportamento condicional (form?) => boolean |
rules |
Regras Quasar adicionais — ver inputRules |
props |
Configuração do componente (placeholder, hint, helpTooltip, prependIcon, FormAwareValue, …) |
onChange, onMounted, … |
Efeitos com acesso opcional ao formulário |
// dentro de defineForm({ fields: [ ... ] })
{
type: "InputEmail",
model: "corporateEmail",
label: "E-mail corporativo",
requiredOn: () => true,
rules: [
inputRules.email(),
],
props: {
placeholder: "nome@empresa.com",
clearable: true,
},
onChange: (newValue, _oldValue, form) => {
console.log(newValue, form?.legalName);
},
},
requiredOn já injeta a regra required; não a duplique em rules salvo se precisar de outra validação independente.
Nível raiz vs props: no objeto do campo vão type, model, label, rules, callbacks condicionais (*On) e lifecycles (onChange, …). A apresentação do widget (placeholder, hint, helpTooltip, prependIcon) fica sempre dentro de props do tipo de input correspondente. label (nível raiz ou em props), placeholder, hint, helpTooltip e prependIcon aceitam valor fixo ou callback conforme o modelo (ver tabela).
Props de apresentação do widget (props)
| Prop | Tipo | Descrição |
|---|---|---|
placeholder |
FormAwareValue<string> |
Texto de ajuda dentro do controle (não se aplica a grupos, toggle, arquivos, editor, SlotField) |
hint |
FormAwareValue<string | undefined> |
Texto abaixo do campo (Quasar hint) |
helpTooltip |
FormAwareValue<string> |
Ajuda contextual: ícone ? no prepend na maioria dos inputs; exceção InputCheckbox (ícone ? junto ao label, sem prepend marginal). Em InputFile / InputFileMultiple com dragDrop vazio, exibido como legenda sob dropHint |
prependIcon |
FormAwareValue<string> |
Ícone Material no prepend; callback (form) => …. Em FormList: FormListPresentationProp<string | null> (= FormAware com FormListAwareContext) — (data) => … com data.parentForm / data.items. Não se aplica a SlotField nem InputCheckbox |
Tooltip nativo do label: em telas estreitas o texto do label pode truncar. Ao passar o cursor sobre o campo ou sobre o label visível, o navegador mostra o label completo (atributo HTML title, resolvido com FormAware como label). Não confundir com helpTooltip (popover do ?).
// dentro de fields: [ ... ]
{
type: "InputText",
model: "legalName",
label: "Razão social",
requiredOn: () => true,
props: {
placeholder: "Ex. Acme Serviços Ltda.",
hint: "Nome registrado na Receita Federal.",
helpTooltip: "Deve coincidir com o contrato social.",
},
},
Callbacks condicionais
| Callback | Efeito |
|---|---|
requiredOn |
Obrigatoriedade condicional (injeta required) |
hideOn |
Oculta com v-show; o valor no store é preservado |
unmountOn |
Se retornar false, desmonta o campo do DOM |
disabledOn |
Desativa interação |
readonlyOn |
Somente leitura |
Assinatura: (form?: FormBuilderSubmitPayload<T>) => boolean. Use form?.campo ou () => valor quando não precisar do modelo.
Exceções por tipo de campo:
| Tipo | requiredOn |
disabledOn / readonlyOn |
|---|---|---|
| Inputs padrão | Validação Quasar automática | Bloqueio automático no componente |
ToggleButton |
Obrigatório = valor true (marcado) |
Bloqueio do toggle |
SlotField separador (sem model) |
Não usar | Não usar |
SlotField custom (com model) |
Passe required ao slot; você vincula :rules se quiser validação no submit |
Passe readonly e disable ao controle do slot |
Ordene fields[] de cima para baixo: callbacks condicionais devem depender apenas de campos anteriores (ver exemplo integrado).
Valores iniciais (defaultValues)
Propriedade opcional. Se omitida, cada campo com model recebe o vazio conforme seu type:
| Vazio padrão | Tipos de campo |
|---|---|
null |
Texto, email, número, data, select simples, arquivo único, etc. |
[] |
InputCheckboxGroup, InputSelectMultiple, InputSelectServerMultiple, InputFileMultiple, FormList |
false |
ToggleButton, InputCheckbox |
Você pode passar um objeto parcial só com os overrides necessários; chaves omitidas usam a tabela acima.
Também pode passar uma função (sync ou async) que retorna um objeto parcial. FormBuilder a executa em onMounted — útil para pré-carga ou edição via API. Ao resolver, atualiza o store e a base do reset (resetForm restaura o pré-carregado, não o estado vazio intermediário).
defaultValues: async () => {
const record = await fetchCorporateRecord(id);
return {
legalName: record.name,
corporateEmail: record.email,
billingCountry: record.country,
};
},
O formulário inicia com vazios por tipo; após a pré-carga pode haver um instante com esses vazios antes de hidratar os valores. Você pode chamar defineForm no componente pai e passar o config resultante ao FormBuilder; a pré-carga executa igual ao montar o formulário.
Props e callbacks conforme o formulário (FormAware)
Além dos callbacks condicionais de campo (requiredOn, hideOn, …), você pode ler o modelo atual em props de configuração e callbacks de efeito. Três camadas complementares:
| Camada | Mecanismo | Quando usar |
|---|---|---|
| Condicional de campo | requiredOn, hideOn, disabledOn, readonlyOn, unmountOn |
Mostrar, bloquear ou obrigar o campo |
| Prop de configuração | FormAwareValue<T> = T | (form?) => T |
Limites, textos, flags que dependem do modelo (sem efeitos colaterais) |
| Callback de efeito | Último argumento opcional form? |
Reagir à mudança ou a uma ação explícita |
FormAwareValue — valor fixo ou (form?) => T. Sem efeitos colaterais; ideal para limites, textos e flags. Cada campo documenta suas props no catálogo.
Além de limites e flags por tipo, label (nível raiz), placeholder, hint, helpTooltip e prependIcon (em props) aceitam FormAwareValue para textos e ícones que mudam com o modelo.
Em FormList, as mesmas props de apresentação em props usam FormListPresentationProp<T> (homólogo FormAware com contexto parentForm / items / draftItem): callback (data) => … em vez de (form) => ….
// dentro de fields: [ ... ]
{
type: "InputFile",
model: "contractPdf",
label: "Contrato",
props: {
maxSize: (form) =>
form?.plan === "enterprise" ? 10 * 1024 * 1024 : 5 * 1024 * 1024,
},
},
{
type: "InputIntlPhone",
model: "contactIntlPhone",
label: (form) =>
form?.billingCountry === "US"
? "US contact phone"
: "Telefone internacional",
props: {
hint: (form) =>
form?.billingCountry && form.billingCountry !== "MX"
? "Obrigatório fora do México."
: undefined,
},
},
Callbacks com form — o snapshot do modelo é passado como último argumento opcional:
| Callback | Assinatura |
|---|---|
onChange (qualquer campo) |
(newValue, oldValue, form?) => void |
onBeforeMount / onMounted |
(fieldProps, form?) => void |
onSearch (InputSearch) |
(value, form?) => void |
optionLabel (grupos e selects) |
(option, form?) => string |
O pipeline de opções já expunha form em options(form), filterOptions(options, form) e callOptionsOn(form). Com optionLabel(option, form?), os textos visíveis são recalculados quando o modelo muda sem recarregar opções.
optionLabel: (option, form) => {
const discount = Number(form?.negotiatedDiscount ?? 0);
return discount > 0
? `${option.label} (desc. ${discount}%)`
: `${option.label} (${option.meta?.tier ?? "n/a"})`;
},
Tipos exportados em /types: FormAwareValue, FormSnapshot, FormListAwareValue, FormListPresentationProp, OptionLabelFn.
Padrões avançados: condicional + servidor + SlotField
Formulário de referência que combina hideOn encadeado, catálogo remoto e seções com SlotField. O fluxo é somente para frente: cada bloco depende de valores já capturados. O fragmento abaixo é ilustrativo; combine com o catálogo e o autocompletado do IDE.
function planOptionsVisible(form?: { priorityScore?: number | null }) {
return Number(form?.priorityScore ?? 0) >= 5;
}
const config = defineForm<CorporateForm>({
formName: "corporateOnboardingForm",
fields: [
{
type: "InputNumber",
model: "priorityScore",
label: "Prioridade (1–10)",
props: { min: 1, max: 10 },
},
{ type: "SlotField", props: { slotName: "section-location" } },
{
type: "InputSelect",
model: "billingCountry",
label: "País de faturamento",
hideOn: (form) => !planOptionsVisible(form),
props: {
callOptionsOn: (form) => form?.priorityScore,
options: (form) => (planOptionsVisible(form) ? COUNTRY_OPTIONS : []),
},
},
{
type: "InputIntlPhone",
model: "contactIntlPhone",
label: "Telefone internacional",
hideOn: (form) => !form?.billingCountry || form.billingCountry === "MX",
requiredOn: (form) =>
form?.billingCountry != null && form.billingCountry !== "MX",
},
{
type: "InputSelectServer",
model: "headquartersCity",
label: "Cidade sede (catálogo remoto)",
hideOn: (form) => !form?.billingCountry,
props: {
callOptionsOn: (form) => form?.billingCountry,
optionsService: fetchMockCities,
usePagination: true,
searchOnServer: true,
perPage: 50,
},
},
{ type: "SlotField", props: { slotName: "actions" } },
],
});
No template, nomeie os slots #slot-field-{slotName} (ex.: #slot-field-section-location, #slot-field-actions).
Fluxo condicional resumido (mesmo formulário de referência):
| Disparador | Efeito (campos posteriores) |
|---|---|
| Aceitar contrato | Cláusulas (InputEditor), contrato PDF (InputFile) e anexos (InputFileMultiple) visíveis; PDF e cláusulas obrigatórios |
| Prioridade ≥ 5 | Desconto, plano, país, canais |
| Plano Enterprise | Orçamento anual obrigatório |
| País ≠ México | Telefone internacional visível e obrigatório |
| País escolhido | Cidade sede (catálogo remoto) |
Field presets (campos reutilizáveis)
Quando o mesmo campo se repete em muitos formulários (mesmo serviço, opções, ícone…), extraia a config para um field preset em src/components/FormBuilder/Fields/.
As duas formas de fields (compatíveis)
| Forma | Quando usar |
|---|---|
fields: [ ... ] |
Formulários sem presets ou campos totalmente inline. |
fields: ({ preset }) => [ ... ] |
Com field presets: misture inline + preset(...) sem repetir o genérico do formulário. |
Não é preciso migrar formulários existentes: o array clássico continua válido. O callback só se aplica ao defineForm raiz; FormList → itemForm.fields continua sendo array.
1. Criar o preset
npm run bor:make-field -- CountrySelect InputSelect
// src/components/FormBuilder/Fields/CountrySelect.field.ts
import { defineFieldPreset } from "@benjaminor-dev/quasar-app-extension-form-builder";
export const countrySelectField = defineFieldPreset("InputSelect", {
label: "País",
props: {
prependIcon: "public",
placeholder: "Selecione o país",
options: [
{ label: "México", value: "MX" },
{ label: "Colombia", value: "CO" },
],
},
});
2. Usar no formulário
import { countrySelectField } from "src/components/FormBuilder/Fields/CountrySelect.field";
defineForm<RegisterForm>({
formName: "register",
fields: ({ preset }) => [
{ type: "InputText", model: "legalName", label: "Razão social" },
preset(countrySelectField, { model: "country" }),
preset(countrySelectField, {
model: "billingCountry",
label: "País de faturamento",
}),
],
});
API de presets
| Export | Uso |
|---|---|
defineFieldPreset(type, base) |
Define um preset exportável (arquivo em Fields/) |
preset(fieldPreset, overrides) |
Dentro de fields: ({ preset }) => … — model obrigatório; props parcial e FormAware |
Os overrides respeitam a mesma tipagem de um campo inline (hideOn, label: (form) => …, props.placeholder, etc.).
API pública
useFormBuilder() retorna:
| Membro | Uso |
|---|---|
defineForm<T>(config) |
Constrói a configuração do formulário (config tipado como DefineFormInput<T> em /types) |
defineFieldPreset |
Field presets reutilizáveis — ver Field presets |
inputRules |
Regras de validação reutilizáveis |
selectFormStore<T>(formName) |
Visão tipada da sessão Pinia (values, errors, reset, …) |
utils |
Helpers para condicionais e busca tolerante |
defineForm(config)
| Propriedade | Tipo | Descrição |
|---|---|---|
formName |
string |
Identificador único do formulário |
fields |
FormField<T>[] | ({ preset }) => FormField<T>[] |
Lista de campos (array clássico ou callback com preset() para field presets) |
defaultValues |
Partial<T> | () => Partial<T> | Promise<Partial<T>> |
Opcional. Overrides por campo; chaves omitidas usam o vazio do tipo ([], false, null). Se for função, executa em onMounted (pré-carga / edição) |
fieldDesign |
standard | outlined | filled | standout | borderless |
Design global (default filled). Não se aplica a SlotField nem ToggleButton |
labelPosition |
inner | stacked | top |
Posição do label (default stacked) |
validationMode |
lazy | eager |
Quando o Quasar valida (default lazy: blur ou submit) |
Prop de <FormBuilder> (não em defineForm):
| Propriedade | Tipo | Descrição |
|---|---|---|
gap |
false | xs | sm | md | lg | xl |
Gutter vertical entre campos (default md) |
Apresentação global (fieldDesign, labelPosition, validationMode, gap, defaultFieldProps): § Defaults globais (boot).
Formulários grandes e filhos com o modelo
Um único FormBuilder por tela (um <q-form>). Distribua fields em módulos .ts ou em Fields/*.field.ts (presets). Para ler ou mutar o modelo em composables, cabeçalhos ou filhos profundos, use selectFormStore:
const { defineForm, selectFormStore } = useFormBuilder();
const formName = "LoginForm";
const config = defineForm<LoginForm>({
formName,
fields: [...identityFields, ...billingFields],
defaultValues: { email: null },
});
// Mesmo estado em qualquer componente ou composable
const loginFormStore = selectFormStore<LoginForm>(formName);
loginFormStore.values.value.email = "a@b.com";
loginFormStore.setValues({ email: "a@b.com" });
if (loginFormStore.hasErrors.value) loginFormStore.clearErrors();
loginFormStore.reset();
| Necessidade | API recomendada |
|---|---|
| Modelo reativo (pai, filho, composable) | selectFormStore<T>(formName).values |
| Erros por campo | selectFormStore<T>(formName).errors / hasErrors |
| Atribuir valores | setValues({ ... }) |
| Restaurar defaults | reset() |
| Vários forms na mesma página | Um formName distinto por formulário |
FormBuilder (componente)
| Prop | Tipo | Descrição |
|---|---|---|
config |
retorno de defineForm |
Configuração do formulário |
gap |
false | xs | sm | md | lg | xl |
Espaçamento vertical entre campos: aplica q-gutter-{size} no <q-form> (default md). Com :gap="false" não adiciona gutter integrado — use seu próprio espaçamento em class ou style |
Por padrão os campos ficam separados com q-gutter-md. Para outro tamanho Quasar, altere a prop; para Tailwind (gap-4), uma classe SCSS ou style, desative o gutter integrado e defina o gap no host:
<!-- Default: q-gutter-md -->
<FormBuilder :config="config" @submit="onSubmit" />
<!-- Outro tamanho Quasar -->
<FormBuilder :config="config" gap="sm" @submit="onSubmit" />
<!-- Espaçamento próprio (Tailwind, SCSS, style, …) -->
<FormBuilder
:config="config"
:gap="false"
class="flex flex-col gap-4"
@submit="onSubmit"
/>
<FormBuilder
:config="config"
:gap="false"
:style="{ gap: '1.25rem' }"
@submit="onSubmit"
/>
Eventos:
| Evento | Payload | Descrição |
|---|---|---|
@submit |
form |
Valores atuais após validação do Quasar e de cada FormList visível do config |
@reset |
form, resetForm |
Ao pressionar um controle type="reset". resetForm() restaura defaultValues e limpa erros. Se não escutar @reset, o reset do store é aplicado automaticamente |
Exemplo com reset personalizado:
<script setup lang="ts">
import type { FormBuilderResetAction } from "@benjaminor-dev/quasar-app-extension-form-builder/types";
function onReset(
_form: CorporateIdentityForm,
resetForm: FormBuilderResetAction,
) {
if (confirm("Descartar alterações?")) {
resetForm();
}
// Se não chamar resetForm(), o formulário mantém os valores atuais
}
</script>
<template>
<FormBuilder :config="config" @submit="onSubmit" @reset="onReset" />
</template>
inputRules
Regras para o array rules de cada campo. Obrigatoriedade vai em requiredOn, não aqui — inputRules não expõe required. Em InputDate, regras de data recebem o valor interno YYYY-MM-DD; em InputNumber, InputDecimal e InputCurrency o modelo é string | null.
| Regra | Valida | Parâmetros | Exemplo |
|---|---|---|---|
email |
message? |
inputRules.email() |
|
url |
URL http/https |
message? |
inputRules.url() |
ip |
Endereço IPv4 ou IPv6 (version opcional: 'ipv4', 'ipv6', 'both') |
message?, version? |
inputRules.ip() · inputRules.ip("IP inválido", "ipv4") |
mac |
Endereço MAC de 6 octetos hex (separator opcional) |
message?, separator? |
inputRules.mac() · inputRules.mac("MAC inválido", "dash") |
uuid |
UUID (versões 1–8) | message? |
inputRules.uuid() |
password |
Mín. 8 caracteres, maiúscula, minúscula e símbolo | message? |
inputRules.password() |
regex |
Padrão personalizado | pattern, message? |
inputRules.regex(/^[A-Z]{3}$/) |
rfc |
RFC mexicano | message? |
inputRules.rfc() |
curp |
CURP mexicana | message? |
inputRules.curp() |
postalCode |
CEP de 5 dígitos | message? |
inputRules.postalCode() |
onlyLetters |
Somente letras | message? |
inputRules.onlyLetters() |
onlyLettersAndSpaces |
Letras e espaços | message? |
inputRules.onlyLettersAndSpaces() |
onlyNumbers |
Somente dígitos | message? |
inputRules.onlyNumbers() |
onlyNumbersAndLetters |
Alfanumérico sem espaços | message? |
inputRules.onlyNumbersAndLetters() |
onlyNumbersLettersAndSpaces |
Alfanumérico com espaços | message? |
inputRules.onlyNumbersLettersAndSpaces() |
alphaDash |
Letras, dígitos, - e _ |
message? |
inputRules.alphaDash() |
legalTextRule |
Texto em uma linha (sem emojis) | message? |
inputRules.legalTextRule() |
legalTextExtendedRule |
Texto multilinha permitido | message? |
inputRules.legalTextExtendedRule() |
startsWith |
Prefixo obrigatório | prefix, message? |
inputRules.startsWith("MX-") |
endsWith |
Sufixo obrigatório | suffix, message? |
inputRules.endsWith(".pdf") |
date |
Data YYYY-MM-DD |
message? |
inputRules.date() |
dateBeforeNow |
Data anterior a hoje | message? |
inputRules.dateBeforeNow() |
dateLTEnow |
Data ≤ hoje | message? |
inputRules.dateLTEnow() |
dateAfterNow |
Data posterior a hoje | message? |
inputRules.dateAfterNow() |
dateGTEnow |
Data ≥ hoje | message? |
inputRules.dateGTEnow() |
dateMin |
Data ≥ limite | minDate, message? |
inputRules.dateMin("2025-01-01") |
dateMax |
Data ≤ limite | maxDate, message? |
inputRules.dateMax("2025-12-31") |
minLength |
Comprimento mínimo | min, message? |
inputRules.minLength(3) |
maxLength |
Comprimento máximo | max, message? |
inputRules.maxLength(120) |
length |
Comprimento exato | length, message? |
inputRules.length(10) |
between |
Valor, comprimento ou quantidade em intervalo | min, max, message? |
inputRules.between(1, 100) |
minNumberValue |
Número ≥ limite | min, message? |
inputRules.minNumberValue(1) |
maxNumberValue |
Número ≤ limite | max, message? |
inputRules.maxNumberValue(100) |
integer |
Inteiro (sem decimais) | message? |
inputRules.integer() |
decimal |
Decimais em intervalo (se houver ponto) | minDecimals, maxDecimals?, message? |
inputRules.decimal(2, 4) |
inValue |
Valor na lista permitida | values[], message? |
inputRules.inValue(["draft", "published"]) |
notInValues |
Valor fora da lista proibida | values[], message? |
inputRules.notInValues(["admin"]) |
noRepeatedNumbers |
Sem um único dígito repetido | message? |
inputRules.noRepeatedNumbers() |
file |
Instância File (ou array de File) |
message? |
inputRules.file() |
image |
Arquivo(s) com MIME image/* |
message? |
inputRules.image() |
{
type: "InputText",
model: "legalName",
label: "Razão social",
requiredOn: () => true,
rules: [inputRules.legalTextExtendedRule(), inputRules.minLength(3)],
},
InputFile aplica file internamente; não é preciso repetir salvo validação manual fora desse campo.
selectFormStore
Acesso tipado ao estado Pinia do formulário (também disponível como função independente no entrypoint principal).
Fora do template, use selectFormStore<T>(formName) (values, errors, setValues, reset, …) — ver Formulários grandes e filhos com o modelo e Store e mapa conceitual de estado/erros.
utils
Utilitários gerais para lógica condicional no host, normalização de busca e limpeza numérica. Disponíveis com const { utils } = useFormBuilder().
| Função | Descrição |
|---|---|
| isEmpty / isNotEmpty | Verifica vazio (null, undefined, "", arrays ou objetos aninhados) |
| cleanText | Normaliza texto para busca tolerante (minúsculas, sem acentos nem espaços/hífens) |
| cleanNumber | Remove zeros à esquerda enquanto o usuário digita um número |
| cleanLeadingZeros / cleanTrailingZeros | Variantes de limpeza numérica em strings |
cleanText alinha a busca remota com InputSelect / InputSelectServer e o seletor de países em InputIntlPhone. isEmpty / isNotEmpty evitam reimplementar vazio em requiredOn, hideOn ou options. Ver exemplo integrado (fetchMockCities).
Catálogo de campos suportados
Referência rápida de fields[].type. Props, regras e exemplos atualizados: autocompletado no IDE (type + props com Ctrl/Cmd+Space) e tipos em /types — ver TypeScript no seu IDE.
type |
/inputs |
Design* | model típico |
Notas |
|---|---|---|---|---|
InputText |
✓ | ✓ | string | null |
Texto geral |
InputTextarea |
✓ | ✓ | string | null |
Multilinha |
InputEmail |
✓ | ✓ | string | null |
Regra de email integrada |
InputUrl |
✓ | ✓ | string | null |
Regra de URL integrada |
InputSearch |
✓ | ✓ | string | null |
Ícone de busca + debounce |
InputPassword |
✓ | ✓ | string | null |
Alternar visibilidade |
InputNumber |
✓ | ✓ | string | null |
Somente dígitos em texto |
InputNumericCode |
✓ | ✓ | string | null |
Código numérico (zeros à esquerda) |
InputOtp |
✓ | ✓ | string | null |
Células OTP; onComplete |
InputDecimal |
✓ | ✓ | string | null |
Decimal formatado |
InputCurrency |
✓ | ✓ | string | null |
Moeda + símbolo |
InputPercent |
✓ | ✓ | number |
Steppers ±; 0–100 padrão |
InputSlider |
✓ | ✓ | number |
Somente arraste (QSlider) |
InputSliderRange |
✓ | ✓ | { min, max } | null |
Intervalo com dois thumbs |
InputCheckbox |
✓ | — | boolean |
Sem fieldDesign; borderless |
InputCheckboxGroup |
✓ | — | (string | number)[] |
Opções locais; multiseleção |
InputRadioGroup |
✓ | ✓ | string | number | null |
Opções locais; seleção única |
InputSelect |
✓ | ✓ | string | number | null |
Dropdown + busca local |
InputSelectMultiple |
✓ | ✓ | (string | number)[] |
Chips; multiseleção |
InputSelectServer |
✓ | ✓ | string | number | null |
Catálogo remoto; optionsService |
InputSelectServerMultiple |
✓ | ✓ | (string | number)[] |
Remoto + chips |
InputDate |
✓ | ✓ | string | null |
YYYY-MM-DD; picker; clearable (default true) |
InputDateRange |
✓ | ✓ | intervalo de datas | minDate / maxDate / maxRange; clearable |
InputTime |
✓ | ✓ | string | null |
HH:mm:ss; 12h/24h; clearable |
InputTel |
✓ | ✓ | string | null |
Somente dígitos; formato país |
InputIntlPhone |
✓ | ✓ | string | null |
E.164 ou structured; seletor de país |
InputIpAddress |
✓ | ✓ | string | null |
IPv4/IPv6; inputRules.ip() |
InputMacAddress |
✓ | ✓ | string | null |
Máscara MAC; inputRules.mac(); clearable |
InputColor |
✓ | ✓ | string | null |
Hex/RGB; picker modal; clearable |
InputEditor |
✓ | ✓ | string | null |
HTML sanitizado; toolbar; clearable |
InputFile |
✓ | ✓ | File | null |
Um arquivo; drag & drop; preview opcional |
InputFileMultiple |
✓ | ✓ | File[] |
Vários arquivos; tabela inline |
FormList |
— | — | T[] |
Lista CRUD em tabela + diálogo; maximize centraliza a coluna do item (~900px) |
ToggleButton |
✓ | — | boolean |
Booleano estilo toggle |
SlotField |
— | — | conforme slot | UI custom via #slot-field-{name} |
* Design = suporta fieldDesign / labelPosition do formulário. — = apresentação própria (borderless ou outra).
Tipos por campo: FieldTypes["InputSelect"], BootFieldPropsFor<"InputSelect">, etc. em @benjaminor-dev/quasar-app-extension-form-builder/types.
Exemplos representativos
Texto + select local
// dentro de fields: [ ... ]
{ type: "InputText", model: "legalName", label: "Razão social", requiredOn: () => true },
{
type: "InputSelect",
model: "plan",
label: "Plan",
props: {
options: [
{ label: "Básico", value: "basic" },
{ label: "Pro", value: "pro" },
],
clearable: true,
},
},
Data com limites dinâmicos
{
type: "InputDate",
model: "startDate",
label: "Início",
props: {
minDate: () => moment().format("YYYY-MM-DD"),
maxDate: (form) => form?.endDate ?? undefined,
clearable: true,
},
},
Limpar valor (clearable) — em pickers (InputDate, InputDateRange, InputTime, InputColor), InputMacAddress e InputEditor: prop FormAwareValue<boolean> (default true). Desative por campo (clearable: false) ou no boot (defaultFieldProps.InputDate: { clearable: false }). InputSelect* usa o :clearable nativo do Quasar.
Arquivo com preview (extensão dialog-file-preview opcional no host)
{
type: "InputFile",
model: "contract",
label: "Contrato",
props: {
accept: ".pdf",
maxSize: 10 * 1024 * 1024,
dragDrop: true,
showPreview: true,
},
},
Select remoto (catálogo grande / API)
{
type: "InputSelectServer",
model: "cityId",
label: "Cidade",
props: {
optionsService: async ({ query, pagination, form }) => {
const res = await api.searchCities({ q: query, country: form?.country, ...pagination });
return { items: res.items, pagination: res.meta };
},
searchOnServer: true,
usePagination: true,
perPage: 50,
},
},
FormList embutido
{
type: "FormList",
model: "teamMembers",
label: "Integrantes",
requiredOn: () => true,
props: {
listName: "members",
itemLabel: "Membro",
itemForm: {
fields: [
{ type: "InputText", model: "fullName", label: "Nome", requiredOn: () => true },
],
},
columns: [{ name: "fullName", label: "Nome", field: "fullName" }],
},
},
Slot de coluna no FormBuilder pai: #form-list-members-column-fullName. Mais detalhe em exemplo integrado e tipos FormListConfig, InputFormListProps em /types.
Diretivas registradas
Convenção runtime: form-builder-{directiveName}. Em templates Vue use como v-form-builder-*.
- v-form-builder-max-length
- v-form-builder-only-numbers
- v-form-builder-only-decimal-number
- v-form-builder-format-number
- v-form-builder-max-number
- v-form-builder-currency-symbol
- v-form-builder-color-input (aplicada internamente por
InputColorconformeformatModel; filtra caracteres ao digitar/colar) - v-form-builder-only-ip-address-chars (aplicada internamente por
InputIpAddressconformeversion; filtra para dígitos,.,:e hex permitidos) - v-form-builder-only-mac-address-chars (aplicada internamente por
InputMacAddressconformeseparator; filtra hex e normaliza separadores junto à máscara Quasar)
Store e mapa conceitual de estado/erros
Acesso: useFormBuilder().selectFormStore(formName) ou selectFormStore(formName) importado do pacote.
selectFormStore<T>(formName)
Visão tipada da sessão Pinia. O mesmo formName em outro .vue ou composable aponta para o mesmo estado.
| Membro | Descrição |
|---|---|
formName |
Identificador (defineForm.formName) |
values |
Ref<T> — modelo aninhado reativo |
flat |
Ref — modelo achatado (chaves com ponto) |
errors |
Erros por campo (reativo) |
hasErrors |
true se houver alguma mensagem |
getValues() / getFlat() |
Snapshots não reativos |
setValues(partial) |
Atribui por chaves planas |
hydrateDefaults(partial) |
Pré-carrega e atualiza base do reset |
setErrors / clearErrors |
Erros de validação |
reset() |
Restaura defaults e limpa erros |
Tipo exportado: FormSessionHandle<T> en /types.
Fluxo conceitual:
defineFormcria (se não existir) a estrutura do formulário no store com vazios por tipo ou com o objeto parcial dedefaultValues.- Se
defaultValuesfor função,FormBuildera executa emonMounted, hidrataformse atualizaformsDefaults(base do reset). - Cada input escreve em
forms[formName][campo]. - Erros de campo ficam em
formsErrors[formName][campo]. resetFormrestauraformsDefaultse limpa erros.
SlotField e uso de refs
Em slots de SlotField com model, model e error são refs e vinculam com .value.
| Nome | O que é |
|---|---|
type: "SlotField" |
Tipo de campo em config.fields[] |
SlotFieldProps |
Props do field (slotName, lifecycles) — /types |
SlotFieldBind |
Payload do slot scoped (attrs) — /types |
Padrão canônico no host:
- Importar
SlotFieldBindde@benjaminor-dev/quasar-app-extension-form-builder/types. - No template:
#slot-field-{slotName}="attrs: SlotFieldBind". - No controle filho:
v-model="attrs.model.value"(eattrs.error.valuese aplicável).
Separadores de seção (slotName: "section-…", sem model) não usam attrs.model; apenas markup estático.
Para campos custom com model, FormBuilder resolve requiredOn / disabledOn / readonlyOn e expõe required, readonly e disable no slot; você deve conectá-los no seu controle e, se usar requiredOn, adicionar :rules no template para o q-form validar no envio.
<script setup lang="ts">
import FormBuilder, {
useFormBuilder,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
import type { SlotFieldBind } from "@benjaminor-dev/quasar-app-extension-form-builder/types";
type DemoForm = {
customText: string | null;
};
const { defineForm } = useFormBuilder();
const config = defineForm<DemoForm>({
formName: "demoForm",
fields: [
{
type: "SlotField",
model: "customText",
props: {
slotName: "custom-text",
},
},
],
});
</script>
<template>
<FormBuilder :config="config">
<template #slot-field-custom-text="attrs: SlotFieldBind">
<q-input
v-model="attrs.model.value"
:error="!!attrs.error.value"
:error-message="attrs.error.value || ''"
:label="attrs.label"
:hint="attrs.hint"
:readonly="attrs.readonly"
:disable="attrs.disable"
:rules="attrs.required ? [(v) => !!v || 'Campo obrigatório'] : undefined"
>
<template v-if="attrs.required" #append>
<span class="text-negative">*</span>
</template>
</q-input>
</template>
</FormBuilder>
</template>
Exemplo com lista embutida (padrão recomendado — campo nativo FormList):
// dentro de fields: [ ... ]
{
type: "FormList",
model: "teamMembers",
label: "Integrantes",
requiredOn: () => true,
props: {
listName: "members",
itemLabel: "Membro",
workflow: "free",
itemForm: {
fields: [
{
type: "InputText",
model: "fullName",
label: "Nome",
requiredOn: () => true,
},
],
},
columns: [
{ name: "fullName", label: "Nome", field: "fullName" },
],
},
},
O scaffold bor:make-formlist gera um FormBuilder com campo FormList embutido (padrão canônico).
Extensões opcionais relacionadas
| Extensão | Propósito | Relação com Form Builder |
|---|---|---|
| @benjaminor-dev/datatable | Obrigatória. Tabela homologada (DataTable) usada por FormList, InputFileMultiple e Table Builder |
Peer npm — instalar com quasar ext add @benjaminor-dev/datatable |
| @benjaminor-dev/table-builder | Tabelas declarativas com filtros Form Builder, paginação servidor/cliente e cards no mobile | Peer obrigatório do Table Builder; usa Form Builder e DataTable |
| @benjaminor-dev/dialog-file-preview | Diálogo modal para pré-visualizar File, Blob ou URL (imagem com zoom, PDF com scroll contínuo em documentos leves ou paginado em arquivos pesados, vídeo, áudio, texto) via useDialogFilePreview(); loader enquanto prepara e carrega o visualizador |
InputFile e InputFileMultiple detectam automaticamente (showPreview default true): append ou linhas da tabela inline com ver/baixar + limpar; também utilizável manualmente com useDialogFilePreview(); não é dependência deste pacote |
Instalação no host (extensões adicionais):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/dialog-file-preview
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)
Regra: registre o defaults boot antes do primeiro uso (defineForm, defineTable, show()). Em quasar.config você verá entradas como bor/bor-form-builder-defaults; os boots do pacote são injetados pela extensão ao compilar — não aparecem como entradas curtas na sua config.
Customizar template scaffold
Você pode substituir o template gerado por bor:make-form criando no app host:
scripts/bor/stubs/bor-make-form.stub
Para formulários com lista (bor:make-formlist):
scripts/bor/stubs/bor-make-formlist.stub
Para field presets (bor:make-field):
scripts/bor/stubs/bor-make-field.stub
Esse arquivo tem prioridade sobre o template incluído na extensão.
Entrypoints públicos
| Entrypoint | Propósito |
|---|---|
| @benjaminor-dev/quasar-app-extension-form-builder | FormBuilder, useFormBuilder, selectFormStore, defineFieldPreset, configureFormBuilderDefaults, configureFormBuilder (alias), boots e inputs reexportados |
| @benjaminor-dev/quasar-app-extension-datatable | DataTable, TablePaginationBar, composables e helpers de paginação (peer obrigatório do Form Builder) |
| @benjaminor-dev/quasar-app-extension-form-builder/boot/store | Boot de integração Pinia para formulários |
| @benjaminor-dev/quasar-app-extension-form-builder/boot/directives | Boot de registro de diretivas |
| @benjaminor-dev/quasar-app-extension-form-builder/inputs | Componentes de input publicados |
@benjaminor-dev/quasar-app-extension-form-builder/scaffold |
runBorHelp, runBorMakeForm, runBorMakeFormlist, runBorMakeField (bor:help, bor:make-form, bor:make-formlist, bor:make-field) |
@benjaminor-dev/quasar-app-extension-form-builder/types |
Tipos de config, campos, boot (DefineFormInput, DefineFieldPresetFn, DefineFormFieldsContext, FormBuilderDefaults, BootFieldPropsFor, FieldUiConfigFor, FormListConfig, FormAwareValue, …) — autocompletado no IDE |
| @benjaminor-dev/quasar-app-extension-form-builder/main.css | Estilos da biblioteca |
Não há entrypoint /form-list nem pacote npm separado para listas: FormList é um tipo de campo nativo deste pacote.
Troubleshooting
Callback condicional marca erro de tipos
Se declarar requiredOn: (form: MyForm) => ..., o TypeScript pode falhar por contravariância. A assinatura real aceita form opcional.
Use:
requiredOn: (form) =>
form?.phone === "123";
o:
requiredOn: (
form?: FormBuilderSubmitPayload<MyForm>,
) => form?.phone === "123";
Duplicar validação required em rules
requiredOn já injeta obrigatoriedade internamente. Não adicione regras manuais de «campo obrigatório» em rules; reserve-as para validações adicionais (email, rfc, tamanho, etc.).
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/form-builder
Comando npm não aparece no host
Execute no host:
npx quasar ext invoke @benjaminor-dev/form-builder
Pinia não inicializada
Garanta que o boot de Pinia rode antes do boot store da extensão.
Falha o check de @benjaminor-dev/quasar-app-extension-datatable
Form Builder exige o peer DataTable. Instale-o antes desta extensão:
quasar ext add @benjaminor-dev/datatable
Sem ele, FormList / InputFileMultiple não resolvem o componente e faltam estilos de tabela.
InputDate/InputDateRange/InputTime não respeitam limites
Verifique o formato de FormAwareValue:
minDate/maxDate:"YYYY-MM-DD",() => "YYYY-MM-DD"o(form) => "YYYY-MM-DD"maxRange: número,() => numberou(form) => number(dias)minTime/maxTime:"HH:mm:ss",() => "HH:mm:ss"o(form) => "HH:mm:ss"
gap="false" não desativa o gutter do Quasar
Para remover q-gutter-* use :gap="false" (boolean com v-bind). Se escrever gap="false" sem :, o Vue envia a string "false"; desde 0.9.7 o runtime trata como desativado, mas em templates novos prefira sempre :gap="false".
validationMode eager incomoda em campos com máscara
validationMode: 'eager' faz o Quasar avaliar regras a cada tecla. Em campos com formato visual (datas, telefones, moeda) costuma ser melhor o default lazy, que valida ao sair do campo ou ao enviar o formulário.
InputFile / InputFileMultiple não mostram arquivos pré-carregados
InputFile só aceita File | null; InputFileMultiple só aceita File[] (vazio = []). Se passar um Blob solto, URL, base64 ou objeto { url, name }, o valor normaliza para vazio. Crie File no cliente:
const blob = await fetch(url).then(
(r) => r.blob(),
);
form.signedContractPdf = new File(
[blob],
"documento.pdf",
{
type:
blob.type || "application/pdf",
},
);
Em formulários de cadastro puro, deixe o campo em null até o usuário selecionar o arquivo.
Botão ver/baixar não aparece em InputFile / InputFileMultiple
- Verifique se não definiu
showPreview: falseempropsdo campo (truepor padrão). - Instale
@benjaminor-dev/dialog-file-previewe verifique boots: Pinia → Form Builder → Dialog File Preview. - Em
InputFile, os botões do append só aparecem quando há umFileválido no modelo e a extensão de preview está disponível. EmInputFileMultiple, o preview fica em cada linha da tabela inline. - Com
disableo preview fica oculto; emreadonlya pré-visualização ou download funciona (excluir/limpar não). - Após
npm run buildno host ou ao linkar a extensão comfile:, reiniciequasar devpara carregar odistatualizado do Form Builder.
Select/radio/checkbox group não reconhece valor pré-carregado
Campos com opções comparam modelValue com options[].value de forma estrita (===). Se a API retorna cityId: 1 mas as opções usam value: "1", não coincidirão. Alinhe tipos em defineForm, em defaultValues e no value de cada opção. Com valores numéricos, 0 é seleção válida para required em seleção única.
Arquivo demora a aparecer após escolher no diálogo do sistema
É normal com arquivos pesados: InputFile e InputFileMultiple mostram indicador de carregamento enquanto o navegador expõe o File ao modelo. O diálogo de @benjaminor-dev/dialog-file-preview também mostra loader ao abrir e enquanto o visualizador carrega o conteúdo (PDF, imagem grande, etc.).
defaultValues como função não pré-carrega o formulário
- A função (sync ou async) executa em
onMounteddoFormBuilder, não ao chamardefineFormno pai. - Passe o
configretornado pordefineFormpara<FormBuilder :config="config" />; não substituadefaultValuespor um objeto plano antes de montar. - Após a pré-carga verá um instante com vazios por tipo; comportamento esperado até a promessa terminar.
- O reset restaura os valores já pré-carregados (atualizam
formsDefaultsao hidratar).
Lista não aparece no submit do formulário pai
- Declare o array no tipo do formulário (
teamMembers: Member[]) e usetype: "FormList"commodel: "teamMembers"emfields[]. - Em
@submit, o handler recebeFormBuilderSubmitPayload<T>— é o próprio formulário (payload.teamMembers, nãopayload.form). Igual aos demais campos. - Para um rascunho inicial, use
npm run bor:make-formlist -- <nome>: gera umFormBuildercom um campoFormListjá conectado (mesmo caminho quebor:make-form). - Antes de
@submit,FormBuildervalida cadaFormListvisível; se houver linhas inválidas, o evento não é emitido. - Com
workflow: "review", linhas inválidas também são destacadas na tabela; o diálogo integrado lista os registros a corrigir com olabeldo field no cabeçalho. - Para alertas próprias em vez do diálogo integrado, defina
onAfterValidateErrorempropsdo field (ver catálogoFormList).
Diálogo de linhas inválidas só aparece uma vez
- Versões anteriores bloqueavam a segunda tentativa de submit via regras do
QField; o runtime atual mostra o erro sob o campo sem impedir novas tentativas: cada clique em Enviar valida de novo e pode reabrir o diálogo. - Se definiu
onAfterValidateError, o diálogo integrado não abre: implemente a lógica de nova tentativa/alerta ali.
MIT © Benjamín Olvera R.