npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

@benjaminor-dev/quasar-app-extension-form-builder

v0.9.22

Published

A Form Builder for Quasar Framework

Readme

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.

  • defineForm + <FormBuilder> — config tipada; props dinâmicas com FormAwareValue.
  • ~30 tipos de campo (texto, datas, selects locais/remotos, arquivos, FormList, …) — catálogo; detalhe de props no IDE.
  • inputRules, requiredOn, selectFormStore, diretivas v-form-builder-*.
  • FormList e InputFileMultiple usam DataTable (@benjaminor-dev/datatable); para listagens declarativas use Table Builder.
  • Defaults globais em boot (configureFormBuilderDefaults); scaffold bor: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.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:

  • 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 duplica FormForm.
  • formName no 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)» via reset() 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.: tempTempForm.vue, igual a bor:make-form temp).
  • Se enviar TeamRegisterForm ou TeamRegisterList, não duplica sufixos.
  • formName no 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 selectFormStore e um botão «Esvaziar lista (store)» que limpa o array do FormList embutido.

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: "[email protected]",
      },
    },
    {
      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.

  • 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 *On recebem esse payload tipado.
  • fields aceita um array (forma habitual) ou um callback ({ preset }) => [...] quando você usa field presets com preset(...). 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.
  • fieldUi (em props ou boot defaultFieldProps[type]): tokens visuais por tipo — autocompletar com FieldUiConfigFor<"InputText"> (cada type é independente). placeholder, hint, etc. ficam fora de fieldUi.
  • FormListprops.listUi: tokens do chrome da lista; boot global com defaultFormListUi.
  • O catálogo completo de type está em FieldTypes; 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/inputs com os mesmos tipos exportados.
  • No boot, locale aceita "en" \| "es" \| "pt"; messages aceita Partial<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> | Rótulo visível (nível raiz ou em props) | | 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: "[email protected]",
    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 = "[email protected]";
loginFormStore.setValues({ email: "[email protected]" });
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-*.

  • 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 InputColor conforme formatModel; filtra caracteres ao digitar/colar)
  • v-form-builder-only-ip-address-chars (aplicada internamente por InputIpAddress conforme version; filtra para dígitos, ., : e hex permitidos)
  • v-form-builder-only-mac-address-chars (aplicada internamente por InputMacAddress conforme separator; 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:

  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:

  • minDate / maxDate: "YYYY-MM-DD", () => "YYYY-MM-DD" o (form) => "YYYY-MM-DD"
  • maxRange: número, () => number ou (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: false em props do campo (true por padrão).
  • Instale @benjaminor-dev/dialog-file-preview e verifique boots: Pinia → Form Builder → Dialog File Preview.
  • Em InputFile, os botões do append só aparecem quando há um File válido no modelo e a extensão de preview está disponível. Em InputFileMultiple, o preview fica em cada linha da tabela inline.
  • Com disable o preview fica oculto; em readonly a pré-visualização ou download funciona (excluir/limpar não).
  • Após npm run build no host ou ao linkar a extensão com file:, reinicie quasar dev para carregar o dist atualizado 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 onMounted do FormBuilder, não ao chamar defineForm no pai.
  • Passe o config retornado por defineForm para <FormBuilder :config="config" />; não substitua defaultValues por 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 formsDefaults ao hidratar).

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

  • Declare o array no tipo do formulário (teamMembers: Member[]) e use type: "FormList" com model: "teamMembers" em fields[].
  • Em @submit, o handler recebe FormBuilderSubmitPayload<T>é o próprio formulário (payload.teamMembers, não payload.form). Igual aos demais campos.
  • Para um rascunho inicial, use npm run bor:make-formlist -- <nome>: gera um FormBuilder com um campo FormList já conectado (mesmo caminho que bor:make-form).
  • Antes de @submit, FormBuilder valida cada FormList visí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 o label do field no cabeçalho.
  • Para alertas próprias em vez do diálogo integrado, defina onAfterValidateError em props do field (ver catálogo FormList).

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.