Form Builder para Quasar

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

npm version license Vue Quasar TypeScript Node

Extensão Quasar para construir formulários declarativos em apps Vue 3 + Quasar.

Índice

Em 30 segundos

Fluxo base recomendado:

  1. Definir tipo de payload.
  2. Construir config com useFormBuilder().defineForm(...).
  3. Renderizar <FormBuilder> com essa config.
  4. 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.

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

Exemplo com subpasta — criar users/RegisterForm.vue e sobrescrever se já existir:

npm run bor:make-form -- users/register --force

Resultado:

Regras de naming:

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

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

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.

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; FormListitemForm.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 E-mail 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-*.

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:

  1. defineForm cria (se não existir) a estrutura do formulário no store com vazios por tipo ou com o objeto parcial de defaultValues.
  2. Se defaultValues for função, FormBuilder a executa em onMounted, hidrata forms e atualiza formsDefaults (base do reset).
  3. Cada input escreve em forms[formName][campo].
  4. Erros de campo ficam em formsErrors[formName][campo].
  5. resetForm restaura formsDefaults e 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:

  1. Importar SlotFieldBind de @benjaminor-dev/quasar-app-extension-form-builder/types.
  2. No template: #slot-field-{slotName}="attrs: SlotFieldBind".
  3. No controle filho: v-model="attrs.model.value" (e attrs.error.value se 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:

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

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

Lista não aparece no submit do formulário pai

Diálogo de linhas inválidas só aparece uma vez


MIT © Benjamín Olvera R.