Form Builder for Quasar

πŸ‡ͺπŸ‡Έ VersiΓ³n en espaΓ±ol | πŸ‡§πŸ‡· VersΓ£o em portuguΓͺs

npm version license Vue Quasar TypeScript Node

Quasar extension for building declarative forms in Vue 3 + Quasar apps.

Table of contents

In 30 seconds

Recommended flow:

  1. Define the payload type.
  2. Build config with useFormBuilder().defineForm(...).
  3. Render <FormBuilder> with that config.
  4. Handle submit/reset via events.

The extension installs boots, directives, and scripts in the host project for immediate use.

What it is and what it includes

Form Builder builds declarative forms on Quasar: one Pinia model per formName, typed fields, and reusable validation.

Requirements and compatibility

Item Value
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 β€” required peer
Package format ES modules

The host Pinia boot must run before this extension's store boot.

Install in a Quasar app

Install DataTable before Form Builder (required peer β€” FormList, InputFileMultiple, and the compatibility check require it):

quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder

With InputFile / InputFileMultiple, install dialog-file-preview (optional). showPreview defaults to true: with the extension, view/download opens the modal; without it, the field behaves like standard QFile.

quasar ext add @benjaminor-dev/dialog-file-preview

Boot order (full stack): see Related optional extensions.

Remove extension:

quasar ext remove @benjaminor-dev/form-builder

What the extension adds to the host

Resource Path
Store boot ~@benjaminor-dev/quasar-app-extension-form-builder/boot/store
Directives boot ~@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; also registered by this extension)
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 β€” bor- prefix from the @benjaminor-dev ecosystem
Host defaults src/boot/bor/bor-form-builder-defaults.ts β€” created on install

Install (quasar ext add / invoke): creates src/boot/bor/bor-form-builder-defaults.ts (or .js) and registers it in quasar.config β†’ boot: [] automatically. You do not need to edit it manually in a normal install. Package boots (boot/store, boot/directives) and CSS are injected by the extension on every dev/build (they do not appear as short entries in quasar.config).

Remove (quasar ext remove): removes generated scaffolds, the defaults boot, its entry in quasar.config, and this extension's bor:* npm scripts (only if they still point to install files).

If you delete the defaults boot, the app still uses built-ins (filled, stacked, lazy, md).

Boot order (this extension): Pinia β†’ DataTable (CSS) β†’ FB store boot β†’ bor/bor-form-builder-defaults β†’ rest.

Global defaults (boot)

Avoid repeating fieldDesign, labelPosition, validationMode, and gap in every defineForm. Priority: defineForm / <FormBuilder> props β†’ boot (configureFormBuilderDefaults) β†’ built-in (filled, stacked, lazy, md).

After quasar ext add, review src/boot/bor/bor-form-builder-defaults.ts β€” full template with defaultFieldProps, // recommended comments, etc.

// src/boot/bor/bor-form-builder-defaults.ts (excerpt)
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 is an alias of configureFormBuilderDefaults.

Boot option Description
fieldDesign Global field design (outlined, filled, …)
labelPosition inner | stacked | top
validationMode lazy (recommended) | eager
gap Vertical spacing in <FormBuilder>
defaultFieldProps Static props per fields[].type (BootFieldPropsFor<"InputX"> in /types); includes fieldUi (input visual tokens)
defaultFormListUi Global FormList visual tokens (toolbar, dialogs); override in field props.listUi
Boot defineForm Effective
filled β€” filled
filled fieldDesign: "outlined" outlined only in that form

Scaffold commands in the host

The extension registers npm scripts in the host for forms and list forms (FormList). Each runner in scripts/bor/bor-make-*.mjs delegates to the npm /scaffold API (runBorHelp, runBorMakeForm, …). Run npm run bor:help to see all scaffolds installed in the project, options, and usage guide.

Help (bor:help)

Lists installed generators, -- syntax, and common options (--force):

npm run bor:help

Forms (bor:make-form)

After -- goes the name or path of the form to generate.

npm run bor:make-form -- <form-name-or-path>

The argument is normalized to PascalCase and the generated file always ends in Form.vue.

Example β€” create a login form (LoginForm.vue):

npm run bor:make-form -- login

Result:

Subfolder example β€” create users/RegisterForm.vue and overwrite if it already exists:

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

Result:

Naming rules:

List forms (bor:make-formlist)

Generates a Vue scaffold with FormBuilder, an embedded FormList field, and submit buttons (SlotField) in src/components/FormBuilder/ β€” same path and naming convention as bor:make-form; only the template differs:

npm run bor:make-formlist -- <form-name-or-path>

Example:

npm run bor:make-formlist -- temp
npm run bor:make-formlist -- users/TeamRegister

Field presets (bor:make-field)

Generates a reusable file in src/components/FormBuilder/Fields/ with defineFieldPreset β€” for selects, server fields, or other fields you repeat across many forms.

npm run bor:make-field -- <NombrePreset> [FieldType]
Input Generates
CountrySelect InputSelect Fields/CountrySelect.field.ts β†’ export countrySelectField
catalog/CitySelect InputSelectServer Fields/catalog/CitySelect.field.ts
CountrySelect (no type) Interactive terminal menu to choose type
selectserver (as type) Accepts the name case-insensitively (InputSelectServer)
--force / -f Overwrites if the file already exists

Example β€” country preset (local select):

npm run bor:make-field -- CountrySelect InputSelect

Edit the generated file (service, options, icons) and use it in any defineForm β€” see Field presets.

Host template override: scripts/bor/stubs/bor-make-field.stub.

Quick start

Minimal example of the corporate client onboarding scenario (identity + submit). The integrated example below shows the near-complete reference form.

<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: "Legal name",
      requiredOn: () => true,
      rules: [
        inputRules.legalTextExtendedRule(),
      ],
      props: {
        placeholder: "E.g. Acme Services Inc.",
      },
    },
    {
      type: "InputText",
      model: "tradeName",
      label: "Trade name",
      readonlyOn: (form) => !form?.legalName,
      props: {
        placeholder: "Optional β€” how the brand is known",
      },
    },
    {
      type: "InputText",
      model: "taxId",
      label: "Tax ID",
      requiredOn: (form) => !!form?.legalName,
      rules: [
        inputRules.rfc(),
      ],
      props: {
        placeholder: "E.g. ACME850101ABC",
        maxLength: 13,
      },
    },
    {
      type: "InputColor",
      model: "brandColor",
      label: "Brand color",
      props: {
        defaultColor: "#1976D2",
        placeholder: "#RRGGBB",
      },
    },
    {
      type: "InputEmail",
      model: "corporateEmail",
      label: "Corporate email",
      requiredOn: () => true,
      props: {
        placeholder: "contact@company.com",
      },
    },
    {
      type: "ToggleButton",
      model: "acceptContract",
      label: "I accept master agreement and data processing",
      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="Clear" />
        <q-btn type="submit" color="primary" label="Submit request" />
      </div>
    </template>
  </FormBuilder>
</template>

Language (i18n)

Built-in en, es, and pt. By default input chrome, FormList, and file-field strings follow Quasar language ($q.lang.isoName); if Quasar has no language, fallback is en.

Locale resolution: boot locale β†’ $q.lang.isoName prefix (es* β†’ es, pt* β†’ pt) β†’ en.

Option When to use
framework.lang in quasar.config.ts Recommended β€” aligns Form Builder with the rest of Quasar
locale: 'es' in boot Pin language (en | es | pt); ignores $q.lang changes
messages: { formListAdd: '…' } in boot Change one string without pinning locale

Quasar config example (Spanish for the whole app):

// quasar.config.ts
framework: {
  lang: "es",
},

Boot template (after install):

configureFormBuilderDefaults({
  // locale: "es", // pins only Form Builder language
  // messages: { formListAdd: "Add" },
});

i18n keys cover common actions (clear, save, cancel, preview), input aria (showHidePassword, openDatePicker, clearField), editor toolbar, FormList chrome (formListAdd, formListValidationTitle, formListMinimized, …), and file rejections (fileRejectionMaxSize, fileCapacityOverLimit with {max}, {name}, {reason} templates). See FormBuilderMessages in /types for the full catalog.

Message priority: boot messages merges over the active locale built-in catalog.

Runtime API: useFormBuilderI18n() returns reactive messages tied to $q.lang.isoName. For imperative access (tests, non-Vue code), use resolveFormBuilderMessages(isoName?) β€” same resolution as boot β†’ $q.lang β†’ en.

import {
  configureFormBuilderDefaults,
  resolveFormBuilderMessages,
  useFormBuilderI18n,
} from "@benjaminor-dev/quasar-app-extension-form-builder";

configureFormBuilderDefaults({ locale: "es" });
const msgs = resolveFormBuilderMessages(); // boot locale

// Inside a component:
const { messages } = useFormBuilderI18n();
// messages.value.formListAdd, messages.value.clear, …

TypeScript in your IDE

You do not need to memorize every prop: typing and autocomplete show the contract as you write.

If a prop does not appear in autocomplete, it probably does not exist in the public API or the field type does not support it.

Form configuration guide

Field anatomy

Each fields[] entry describes a form control:

Property Description
type Field type (InputText, InputSelect, …)
model Key in the typed form model
label FormAwareValue<string>
requiredOn / hideOn / disabledOn / readonlyOn / unmountOn Conditional behavior (form?) => boolean
rules Additional Quasar rules β€” see inputRules
props Component configuration (placeholder, hint, helpTooltip, prependIcon, FormAwareValue, …)
onChange, onMounted, … Effects with optional form access
// inside defineForm({ fields: [ ... ] })
{
  type: "InputEmail",
  model: "corporateEmail",
  label: "Corporate email",
  requiredOn: () => true,
  rules: [
    inputRules.email(),
  ],
  props: {
    placeholder: "name@company.com",
    clearable: true,
  },
  onChange: (newValue, _oldValue, form) => {
    console.log(newValue, form?.legalName);
  },
},

requiredOn already injects the required rule; do not duplicate it in rules unless you need another independent validation.

Root level vs props: the field object includes type, model, label, rules, conditional callbacks (*On), and lifecycles (onChange, …). Widget presentation (placeholder, hint, helpTooltip, prependIcon) always goes inside props for the corresponding input type. label (root or in props), placeholder, hint, helpTooltip, and prependIcon accept a fixed value or a model-based callback (see table).

Widget presentation props (props)

Prop Type Description
placeholder FormAwareValue<string> Help text inside the control (does not apply to groups, toggle, files, editor, SlotField)
hint FormAwareValue<string | undefined> Text below the field (Quasar hint)
helpTooltip FormAwareValue<string> Contextual help: ? icon in prepend on most inputs; exception InputCheckbox (? icon next to the label, no marginal prepend). On InputFile / InputFileMultiple with empty dragDrop, shown as legend under dropHint
prependIcon FormAwareValue<string> Material icon in prepend; callback (form) => …. On FormList: FormListPresentationProp<string | null> (= FormAware with FormListAwareContext) β€” (data) => … with data.parentForm / data.items. Does not apply to SlotField or InputCheckbox

Native label tooltip: on narrow screens the label text may truncate. Hovering the field or the visible label shows the full label in the browser (HTML title attribute, resolved with FormAware like label). Do not confuse with helpTooltip (the ? popover).

// inside fields: [ ... ]
{
  type: "InputText",
  model: "legalName",
  label: "Legal name",
  requiredOn: () => true,
  props: {
    placeholder: "E.g. Acme Services Inc.",
    hint: "Name registered with tax authority.",
    helpTooltip: "Must match the articles of incorporation.",
  },
},

Conditional callbacks

Callback Effect
requiredOn Conditional required (injects required)
hideOn Hides with v-show; store value is preserved
unmountOn If it returns false, unmounts the field from the DOM
disabledOn Disables interaction
readonlyOn Read-only

Signature: (form?: FormBuilderSubmitPayload<T>) => boolean. Use form?.field or () => value when you do not need the model.

Exceptions by field type:

Tipo requiredOn disabledOn / readonlyOn
Standard inputs Automatic Quasar validation Automatic blocking in the component
ToggleButton Required = true value (checked) Toggle blocking
SlotField separator (no model) Do not use Do not use
Custom SlotField (with model) Pass required to the slot; you bind :rules for submit validation Pass readonly and disable to the slot control

Order fields[] top to bottom: conditional callbacks should depend only on earlier fields (see integrated example).

Initial values (defaultValues)

Optional property. If omitted, each field with model gets the empty value for its type:

Default empty Field types
null Text, email, number, date, simple select, single file, etc.
[] InputCheckboxGroup, InputSelectMultiple, InputSelectServerMultiple, InputFileMultiple, FormList
false ToggleButton, InputCheckbox

You can pass a partial object with only the overrides you need; omitted keys use the table above.

You can also pass a function (sync or async) that returns a partial object. FormBuilder runs it on onMounted β€” useful for preload or API edit mode. On resolve, it updates the store and the reset baseline (resetForm restores preloaded values, not the intermediate empty state).

defaultValues: async () => {
  const record = await fetchCorporateRecord(id);
  return {
    legalName: record.name,
    corporateEmail: record.email,
    billingCountry: record.country,
  };
},

The form starts with type empties; after preload you may briefly see those empties before values hydrate. You can call defineForm in the parent and pass the resulting config to FormBuilder; preload still runs when the form mounts.

Props and callbacks from the form (FormAware)

Besides field conditional callbacks (requiredOn, hideOn, …), you can read the current model in configuration props and effect callbacks. Three complementary layers:

Layer Mechanism When to use
Field conditional requiredOn, hideOn, disabledOn, readonlyOn, unmountOn Show, block, or require the field
Configuration prop FormAwareValue<T> = T | (form?) => T Limits, text, flags that depend on the model (no side effects)
Effect callback Optional last argument form? React to a change or explicit action

FormAwareValue β€” fixed value or (form?) => T. No side effects; ideal for limits, text, and flags. Each field documents its props in the catalog.

Besides per-type limits and flags, label (root), placeholder, hint, helpTooltip, and prependIcon (in props) accept FormAwareValue for text and icons that change with the model.

On FormList, the same presentation props in props use FormListPresentationProp<T> (FormAware equivalent with parentForm / items / draftItem context): callback (data) => … instead of (form) => ….

// inside fields: [ ... ]
{
    type: "InputFile",
    model: "contractPdf",
    label: "Contract",
    props: {
      maxSize: (form) =>
        form?.plan === "enterprise" ? 10 * 1024 * 1024 : 5 * 1024 * 1024,
    },
  },
{
    type: "InputIntlPhone",
    model: "contactIntlPhone",
    label: (form) =>
      form?.billingCountry === "US"
        ? "US contact phone"
        : "International phone",
    props: {
      hint: (form) =>
        form?.billingCountry && form.billingCountry !== "MX"
          ? "Required outside Mexico."
          : undefined,
    },
  },

Callbacks with form β€” the model snapshot is passed as the optional last argument:

Callback Signature
onChange (any field) (newValue, oldValue, form?) => void
onBeforeMount / onMounted (fieldProps, form?) => void
onSearch (InputSearch) (value, form?) => void
optionLabel (groups and selects) (option, form?) => string

The options pipeline already exposed form in options(form), filterOptions(options, form), and callOptionsOn(form). With optionLabel(option, form?), visible labels recalculate when the model changes without reloading options.

optionLabel: (option, form) => {
    const discount = Number(form?.negotiatedDiscount ?? 0);
    return discount > 0
      ? `${option.label} (desc. ${discount}%)`
      : `${option.label} (${option.meta?.tier ?? "n/a"})`;
  },

Exported types in /types: FormAwareValue, FormSnapshot, FormListAwareValue, FormListPresentationProp, OptionLabelFn.

Advanced patterns: conditional + server + SlotField

Reference form combining chained hideOn, remote catalog, and SlotField sections. Flow is forward-only: each block depends on values already captured. The fragment below is illustrative; combine it with the catalog and IDE autocomplete.

function planOptionsVisible(form?: { priorityScore?: number | null }) {
  return Number(form?.priorityScore ?? 0) >= 5;
}

const config = defineForm<CorporateForm>({
  formName: "corporateOnboardingForm",
  fields: [
    {
      type: "InputNumber",
      model: "priorityScore",
      label: "Priority (1–10)",
      props: { min: 1, max: 10 },
    },
    { type: "SlotField", props: { slotName: "section-location" } },
    {
      type: "InputSelect",
      model: "billingCountry",
      label: "Billing country",
      hideOn: (form) => !planOptionsVisible(form),
      props: {
        callOptionsOn: (form) => form?.priorityScore,
        options: (form) => (planOptionsVisible(form) ? COUNTRY_OPTIONS : []),
      },
    },
    {
      type: "InputIntlPhone",
      model: "contactIntlPhone",
      label: "International phone",
      hideOn: (form) => !form?.billingCountry || form.billingCountry === "MX",
      requiredOn: (form) =>
        form?.billingCountry != null && form.billingCountry !== "MX",
    },
    {
      type: "InputSelectServer",
      model: "headquartersCity",
      label: "Headquarters city (remote catalog)",
      hideOn: (form) => !form?.billingCountry,
      props: {
        callOptionsOn: (form) => form?.billingCountry,
        optionsService: fetchMockCities,
        usePagination: true,
        searchOnServer: true,
        perPage: 50,
      },
    },
    { type: "SlotField", props: { slotName: "actions" } },
  ],
});

In the template, name slots #slot-field-{slotName} (e.g. #slot-field-section-location, #slot-field-actions).

Conditional flow summary (same reference form):

Trigger Effect (later fields)
Accept contract Clauses (InputEditor), PDF contract (InputFile), and attachments (InputFileMultiple) visible; PDF and clauses required
Priority β‰₯ 5 Discount, plan, country, channels
Enterprise plan Annual budget required
Country β‰  Mexico International phone visible and required
Country selected Headquarters city (remote catalog)

Field presets (reusable fields)

When the same field repeats across many forms (same service, options, icon…), extract the config to a field preset in src/components/FormBuilder/Fields/.

Two fields forms (compatible)

Form When to use
fields: [ ... ] Forms without presets or fully inline fields.
fields: ({ preset }) => [ ... ] With field presets: mix inline + preset(...) without repeating the form generic.

No need to migrate existing forms: the classic array remains valid. The callback applies only to the root defineForm; FormList β†’ itemForm.fields stays an array.

1. Create the 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: "Country",
  props: {
    prependIcon: "public",
    placeholder: "Select country",
    options: [
      { label: "Mexico", value: "MX" },
      { label: "Colombia", value: "CO" },
    ],
  },
});

2. Use it in the form

import { countrySelectField } from "src/components/FormBuilder/Fields/CountrySelect.field";

defineForm<RegisterForm>({
  formName: "register",
  fields: ({ preset }) => [
    { type: "InputText", model: "legalName", label: "Legal name" },
    preset(countrySelectField, { model: "country" }),
    preset(countrySelectField, {
      model: "billingCountry",
      label: "Billing country",
    }),
  ],
});

Preset API

Export Use
defineFieldPreset(type, base) Defines an exportable preset (file in Fields/)
preset(fieldPreset, overrides) Inside fields: ({ preset }) => … β€” model required; partial props and FormAware

Overrides follow the same typing as an inline field (hideOn, label: (form) => …, props.placeholder, etc.).

Public API

useFormBuilder() returns:

Member Use
defineForm<T>(config) Builds form configuration (config typed as DefineFormInput<T> in /types)
defineFieldPreset Reusable field presets β€” see Field presets
inputRules Reusable validation rules
selectFormStore<T>(formName) Typed Pinia session view (values, errors, reset, …)
utils Helpers for conditionals and tolerant search

defineForm(config)

Property Type Description
formName string Unique form identifier
fields FormField<T>[] | ({ preset }) => FormField<T>[] Field list (classic array or callback with preset() for field presets)
defaultValues Partial<T> | () => Partial<T> | Promise<Partial<T>> Optional. Per-field overrides; omitted keys use the type empty ([], false, null). If a function, runs on onMounted (preload / edit)
fieldDesign standard | outlined | filled | standout | borderless Global design (default filled). Does not apply to SlotField or ToggleButton
labelPosition inner | stacked | top Label position (default stacked)
validationMode lazy | eager When Quasar validates (default lazy: blur or submit)

<FormBuilder> prop (not in defineForm):

Property Type Description
gap false | xs | sm | md | lg | xl Vertical gutter between fields (default md)

Global presentation (fieldDesign, labelPosition, validationMode, gap, defaultFieldProps): Β§ Global defaults (boot).

Large forms and children with the model

One FormBuilder per screen (one <q-form>). Split fields across .ts modules or Fields/*.field.ts (presets). To read or mutate the model in composables, headers, or deep children, use selectFormStore:

const { defineForm, selectFormStore } = useFormBuilder();
const formName = "LoginForm";

const config = defineForm<LoginForm>({
  formName,
  fields: [...identityFields, ...billingFields],
  defaultValues: { email: null },
});

// Same state in any component or 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();
Need Recommended API
Reactive model (parent, child, composable) selectFormStore<T>(formName).values
Per-field errors selectFormStore<T>(formName).errors / hasErrors
Assign values setValues({ ... })
Restore defaults reset()
Multiple forms on the same page A distinct formName per form

FormBuilder (component)

Prop Type Description
config return value of defineForm Form configuration
gap false | xs | sm | md | lg | xl Vertical spacing between fields: applies q-gutter-{size} on <q-form> (default md). With :gap="false" no built-in gutter β€” use your own spacing in class or style

By default fields are spaced with q-gutter-md. For another Quasar size, change the prop; for Tailwind (gap-4), an SCSS class, or style, disable built-in gutter and set gap in the host:

<!-- Default: q-gutter-md -->
<FormBuilder :config="config" @submit="onSubmit" />

<!-- Another Quasar size -->
<FormBuilder :config="config" gap="sm" @submit="onSubmit" />

<!-- Custom spacing (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"
/>

Events:

Event Payload Description
@submit form Current values after Quasar validation and each visible FormList in the config
@reset form, resetForm When pressing a type="reset" control. resetForm() restores defaultValues and clears errors. If you do not listen to @reset, store reset applies automatically

Custom reset example:

<script setup lang="ts">
import type { FormBuilderResetAction } from "@benjaminor-dev/quasar-app-extension-form-builder/types";

function onReset(
  _form: CorporateIdentityForm,
  resetForm: FormBuilderResetAction,
) {
  if (confirm("Discard changes?")) {
    resetForm();
  }
  // If you do not call resetForm(), the form keeps current values
}
</script>

<template>
  <FormBuilder :config="config" @submit="onSubmit" @reset="onReset" />
</template>

inputRules

Rules for each field's rules array. Required goes in requiredOn, not here β€” inputRules does not expose required. On InputDate, date rules receive the internal YYYY-MM-DD value; on InputNumber, InputDecimal, and InputCurrency the model is string | null.

Rule Validates Parameters Example
email Email message? inputRules.email()
url URL http/https message? inputRules.url()
ip IPv4 or IPv6 address (optional version: 'ipv4', 'ipv6', 'both') message?, version? inputRules.ip() Β· inputRules.ip("Invalid IP", "ipv4")
mac 6-octet hex MAC address (optional separator) message?, separator? inputRules.mac() Β· inputRules.mac("Invalid MAC", "dash")
uuid UUID (versions 1–8) message? inputRules.uuid()
password Min. 8 chars, uppercase, lowercase, and symbol message? inputRules.password()
regex Custom pattern pattern, message? inputRules.regex(/^[A-Z]{3}$/)
rfc Mexican RFC message? inputRules.rfc()
curp Mexican CURP message? inputRules.curp()
postalCode 5-digit postal code message? inputRules.postalCode()
onlyLetters Letters only message? inputRules.onlyLetters()
onlyLettersAndSpaces Letters and spaces message? inputRules.onlyLettersAndSpaces()
onlyNumbers Digits only message? inputRules.onlyNumbers()
onlyNumbersAndLetters Alphanumeric without spaces message? inputRules.onlyNumbersAndLetters()
onlyNumbersLettersAndSpaces Alphanumeric with spaces message? inputRules.onlyNumbersLettersAndSpaces()
alphaDash Letters, digits, - and _ message? inputRules.alphaDash()
legalTextRule Single-line text (no emojis) message? inputRules.legalTextRule()
legalTextExtendedRule Multiline text allowed message? inputRules.legalTextExtendedRule()
startsWith Required prefix prefix, message? inputRules.startsWith("MX-")
endsWith Required suffix suffix, message? inputRules.endsWith(".pdf")
date YYYY-MM-DD date message? inputRules.date()
dateBeforeNow Date before today message? inputRules.dateBeforeNow()
dateLTEnow Date ≀ today message? inputRules.dateLTEnow()
dateAfterNow Date after today message? inputRules.dateAfterNow()
dateGTEnow Date β‰₯ today message? inputRules.dateGTEnow()
dateMin Date β‰₯ limit minDate, message? inputRules.dateMin("2025-01-01")
dateMax Date ≀ limit maxDate, message? inputRules.dateMax("2025-12-31")
minLength Minimum length min, message? inputRules.minLength(3)
maxLength Maximum length max, message? inputRules.maxLength(120)
length Exact length length, message? inputRules.length(10)
between Value, length, or count in range min, max, message? inputRules.between(1, 100)
minNumberValue Number β‰₯ bound min, message? inputRules.minNumberValue(1)
maxNumberValue Number ≀ bound max, message? inputRules.maxNumberValue(100)
integer Integer (no decimals) message? inputRules.integer()
decimal Decimals in range (if decimal point) minDecimals, maxDecimals?, message? inputRules.decimal(2, 4)
inValue Value in allowed list values[], message? inputRules.inValue(["draft", "published"])
notInValues Value outside forbidden list values[], message? inputRules.notInValues(["admin"])
noRepeatedNumbers No single repeated digit message? inputRules.noRepeatedNumbers()
file File instance (or File array) message? inputRules.file()
image File(s) with image/* MIME message? inputRules.image()
{
    type: "InputText",
    model: "legalName",
    label: "Legal name",
    requiredOn: () => true,
    rules: [inputRules.legalTextExtendedRule(), inputRules.minLength(3)],
  },

InputFile applies file internally; no need to repeat it unless manual validation outside that field.

selectFormStore

Typed access to the form Pinia state (also available as a standalone function from the main entrypoint).

Outside the template, use selectFormStore<T>(formName) (values, errors, setValues, reset, …) β€” see Large forms and children with the model and Store and conceptual state/error map.

utils

General utilities for conditional logic in the host, search normalization, and numeric cleanup. Available with const { utils } = useFormBuilder().

Function Description
isEmpty / isNotEmpty Checks empty (null, undefined, "", arrays or nested objects)
cleanText Normalizes text for tolerant search (lowercase, no accents or spaces/dashes)
cleanNumber Removes leading zeros while the user types a number
cleanLeadingZeros / cleanTrailingZeros Numeric cleanup variants in strings

cleanText aligns remote search with InputSelect / InputSelectServer and the country selector in InputIntlPhone. isEmpty / isNotEmpty avoid reimplementing empty checks in requiredOn, hideOn, or options. See integrated example (fetchMockCities).

Supported field catalog

Quick reference for fields[].type. Up-to-date props, rules, and examples: IDE autocomplete (type + props with Ctrl/Cmd+Space) and types in /types β€” see TypeScript in your IDE.

type /inputs Design* Typical model Notes
InputText βœ“ βœ“ string | null General text
InputTextarea βœ“ βœ“ string | null Multiline
InputEmail βœ“ βœ“ string | null Built-in email rule
InputUrl βœ“ βœ“ string | null Built-in URL rule
InputSearch βœ“ βœ“ string | null Search icon + debounce
InputPassword βœ“ βœ“ string | null Visibility toggle
InputNumber βœ“ βœ“ string | null Digits only in text
InputNumericCode βœ“ βœ“ string | null Numeric code (leading zeros)
InputOtp βœ“ βœ“ string | null OTP cells; onComplete
InputDecimal βœ“ βœ“ string | null Formatted decimal
InputCurrency βœ“ βœ“ string | null Currency + symbol
InputPercent βœ“ βœ“ number Β± steppers; 0–100 default
InputSlider βœ“ βœ“ number Drag only (QSlider)
InputSliderRange βœ“ βœ“ { min, max } | null Range with two thumbs
InputCheckbox βœ“ β€” boolean No fieldDesign; borderless
InputCheckboxGroup βœ“ β€” (string | number)[] Local options; multiselect
InputRadioGroup βœ“ βœ“ string | number | null Local options; single select
InputSelect βœ“ βœ“ string | number | null Dropdown + local search
InputSelectMultiple βœ“ βœ“ (string | number)[] Chips; multiselect
InputSelectServer βœ“ βœ“ string | number | null Remote catalog; optionsService
InputSelectServerMultiple βœ“ βœ“ (string | number)[] Remote + chips
InputDate βœ“ βœ“ string | null YYYY-MM-DD; picker; clearable (default true)
InputDateRange βœ“ βœ“ date range minDate / maxDate / maxRange; clearable
InputTime βœ“ βœ“ string | null HH:mm:ss; 12h/24h; clearable
InputTel βœ“ βœ“ string | null Digits only; country format
InputIntlPhone βœ“ βœ“ string | null E.164 or structured; country selector
InputIpAddress βœ“ βœ“ string | null IPv4/IPv6; inputRules.ip()
InputMacAddress βœ“ βœ“ string | null MAC mask; inputRules.mac(); clearable
InputColor βœ“ βœ“ string | null Hex/RGB; modal picker; clearable
InputEditor βœ“ βœ“ string | null Sanitized HTML; toolbar; clearable
InputFile βœ“ βœ“ File | null Single file; drag & drop; optional preview
InputFileMultiple βœ“ βœ“ File[] Multiple files; inline table
FormList β€” β€” T[] CRUD list in table + dialog; maximize centers item column (~900px)
ToggleButton βœ“ β€” boolean Boolean toggle style
SlotField β€” β€” per slot Custom UI via #slot-field-{name}

* Design = supports form fieldDesign / labelPosition. β€” = own presentation (borderless or other).

Per-field types: FieldTypes["InputSelect"], BootFieldPropsFor<"InputSelect">, etc. in @benjaminor-dev/quasar-app-extension-form-builder/types.

Representative examples

Text + local select

// inside fields: [ ... ]
{ type: "InputText", model: "legalName", label: "Legal name", requiredOn: () => true },
{
  type: "InputSelect",
  model: "plan",
  label: "Plan",
  props: {
    options: [
      { label: "Basic", value: "basic" },
      { label: "Pro", value: "pro" },
    ],
    clearable: true,
  },
},

Date with dynamic limits

{
  type: "InputDate",
  model: "startDate",
  label: "Start",
  props: {
    minDate: () => moment().format("YYYY-MM-DD"),
    maxDate: (form) => form?.endDate ?? undefined,
    clearable: true,
  },
},

Clear value (clearable) β€” on pickers (InputDate, InputDateRange, InputTime, InputColor), InputMacAddress, and InputEditor: FormAwareValue<boolean> prop (default true). Disable per field (clearable: false) or in boot (defaultFieldProps.InputDate: { clearable: false }). InputSelect* uses Quasar native :clearable.

File with preview (optional dialog-file-preview extension in host)

{
  type: "InputFile",
  model: "contract",
  label: "Contract",
  props: {
    accept: ".pdf",
    maxSize: 10 * 1024 * 1024,
    dragDrop: true,
    showPreview: true,
  },
},

Remote select (large catalog / API)

{
  type: "InputSelectServer",
  model: "cityId",
  label: "City",
  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,
  },
},

Embedded FormList

{
  type: "FormList",
  model: "teamMembers",
  label: "Members",
  requiredOn: () => true,
  props: {
    listName: "members",
    itemLabel: "Member",
    itemForm: {
      fields: [
        { type: "InputText", model: "fullName", label: "Name", requiredOn: () => true },
      ],
    },
    columns: [{ name: "fullName", label: "Name", field: "fullName" }],
  },
},

Column slot on the parent FormBuilder: #form-list-members-column-fullName. More detail in integrated example and types FormListConfig, InputFormListProps in /types.

Registered directives

Runtime convention: form-builder-{directiveName}. In Vue templates use as v-form-builder-*.

Store and conceptual state/error map

Access: useFormBuilder().selectFormStore(formName) or selectFormStore(formName) imported from the package.

selectFormStore<T>(formName)

Typed Pinia session view. The same formName in another .vue or composable points to the same state.

Member Description
formName Identificador (defineForm.formName)
values Ref<T> β€” nested reactive model
flat Ref β€” flattened model (dot keys)
errors Per-field errors (reactive)
hasErrors true if any message exists
getValues() / getFlat() Non-reactive snapshots
setValues(partial) Assigns by flat keys
hydrateDefaults(partial) Preloads and updates reset baseline
setErrors / clearErrors Validation errors
reset() Restores defaults and clears errors

Tipo exportado: FormSessionHandle<T> en /types.

Conceptual flow:

  1. defineForm creates (if missing) the form structure in the store with type empties or the partial defaultValues object.
  2. If defaultValues is a function, FormBuilder runs it on onMounted, hydrates forms, and updates formsDefaults (reset baseline).
  3. Each input writes to forms[formName][field].
  4. Field errors live in formsErrors[formName][field].
  5. resetForm restores formsDefaults and clears errors.

SlotField and ref usage

In SlotField slots with model, model and error are refs and bind with .value.

Name What it is
type: "SlotField" Field type in config.fields[]
SlotFieldProps Field props (slotName, lifecycles) β€” /types
SlotFieldBind Scoped slot payload (attrs) β€” /types

Canonical pattern in the host:

  1. Import SlotFieldBind from @benjaminor-dev/quasar-app-extension-form-builder/types.
  2. In the template: #slot-field-{slotName}="attrs: SlotFieldBind".
  3. In the child control: v-model="attrs.model.value" (and attrs.error.value if applicable).

Section separators (slotName: "section-…", no model) do not use attrs.model; static markup only.

For custom fields with model, FormBuilder resolves requiredOn / disabledOn / readonlyOn and exposes required, readonly, and disable in the slot; you must wire them in your control and, if using requiredOn, add :rules in the template so q-form validates on submit.

<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 || 'Required field'] : undefined"
      >
        <template v-if="attrs.required" #append>
          <span class="text-negative">*</span>
        </template>
      </q-input>
    </template>
  </FormBuilder>
</template>

Embedded list example (recommended pattern β€” native FormList field):

// inside fields: [ ... ]
{
  type: "FormList",
  model: "teamMembers",
  label: "Members",
  requiredOn: () => true,
  props: {
    listName: "members",
    itemLabel: "Member",
    workflow: "free",
    itemForm: {
      fields: [
        {
          type: "InputText",
          model: "fullName",
          label: "Name",
          requiredOn: () => true,
        },
      ],
    },
    columns: [
      { name: "fullName", label: "Name", field: "fullName" },
    ],
  },
},

The bor:make-formlist scaffold generates a FormBuilder with an embedded FormList field (canonical pattern).

Extension Purpose Relation to Form Builder
@benjaminor-dev/datatable Required. Standardized table (DataTable) used by FormList, InputFileMultiple, and Table Builder npm peer β€” install with quasar ext add @benjaminor-dev/datatable
@benjaminor-dev/table-builder Declarative tables with Form Builder filters, server/client pagination, and mobile cards Table Builder required peer; uses Form Builder and DataTable
@benjaminor-dev/dialog-file-preview Modal dialog to preview File, Blob, or URL (image with zoom, PDF with continuous scroll on light documents or paginated on heavy files, video, audio, text) via useDialogFilePreview(); loader while preparing and loading the viewer InputFile and InputFileMultiple detect it automatically (showPreview default true): append or inline table rows with view/download + clear; also usable manually with useDialogFilePreview(); not a dependency of this package

Install in the host (additional extensions):

quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/dialog-file-preview

Boot order (full stack):

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)

Rule: register the defaults boot before first use (defineForm, defineTable, show()). In quasar.config you will see entries such as bor/bor-form-builder-defaults; package boots are injected by the extension at compile time β€” they do not appear as short entries in your config.

Customize scaffold template

You can replace the template generated by bor:make-form by creating in your host app:

scripts/bor/stubs/bor-make-form.stub

For list forms (bor:make-formlist):

scripts/bor/stubs/bor-make-formlist.stub

For field presets (bor:make-field):

scripts/bor/stubs/bor-make-field.stub

That file takes priority over the template included in the extension.

Public entrypoints

Entrypoint Purpose
@benjaminor-dev/quasar-app-extension-form-builder FormBuilder, useFormBuilder, selectFormStore, defineFieldPreset, configureFormBuilderDefaults, configureFormBuilder (alias), reexported boots and inputs
@benjaminor-dev/quasar-app-extension-datatable DataTable, TablePaginationBar, composables and pagination helpers (Form Builder required peer)
@benjaminor-dev/quasar-app-extension-form-builder/boot/store Pinia integration boot for forms
@benjaminor-dev/quasar-app-extension-form-builder/boot/directives Directives registration boot
@benjaminor-dev/quasar-app-extension-form-builder/inputs Published input components
@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 Config, field, boot types (DefineFormInput, DefineFieldPresetFn, DefineFormFieldsContext, FormBuilderDefaults, BootFieldPropsFor, FieldUiConfigFor, FormListConfig, FormAwareValue, …) β€” IDE autocomplete
@benjaminor-dev/quasar-app-extension-form-builder/main.css Library styles

There is no /form-list entrypoint or separate npm package for lists: FormList is a native field type in this package.

Troubleshooting

Conditional callback shows type error

If you declare requiredOn: (form: MyForm) => ..., TypeScript may fail due to contravariance. The real signature accepts optional form.

Use:

requiredOn: (form) =>
  form?.phone === "123";

o:

requiredOn: (
  form?: FormBuilderSubmitPayload<MyForm>,
) => form?.phone === "123";

Duplicating required validation in rules

requiredOn already injects required internally. Do not add manual Β«required fieldΒ» rules in rules; reserve them for additional validation (email, rfc, size, etc.).

Defaults boot missing from quasar.config

Under normal conditions install registers it automatically. If missing (old project or manual edit):

npx quasar ext invoke @benjaminor-dev/form-builder

npm command missing in host

Run in the host:

npx quasar ext invoke @benjaminor-dev/form-builder

Pinia not initialized

Ensure the Pinia boot runs before the extension store boot.

@benjaminor-dev/quasar-app-extension-datatable check fails

Form Builder requires the DataTable peer. Install it before this extension:

quasar ext add @benjaminor-dev/datatable

Without it, FormList / InputFileMultiple cannot resolve the component and table styles are missing.

InputDate/InputDateRange/InputTime ignore limits

Check FormAwareValue format:

gap="false" does not disable Quasar gutter

To remove q-gutter-* use :gap="false" (boolean with v-bind). If you write gap="false" without :, Vue sends the string "false"; since 0.9.7 the runtime treats it as disabled, but in new templates always prefer :gap="false".

validationMode eager is noisy on masked fields

validationMode: 'eager' makes Quasar evaluate rules on every keystroke. On visually formatted fields (dates, phones, currency) the default lazy is usually better β€” validates on blur or submit.

InputFile / InputFileMultiple do not show preloaded files

InputFile only accepts File | null; InputFileMultiple only accepts File[] (empty = []). If you pass a loose Blob, URL, base64, or { url, name } object, the value normalizes to empty. Create File on the client:

const blob = await fetch(url).then(
  (r) => r.blob(),
);
form.signedContractPdf = new File(
  [blob],
  "document.pdf",
  {
    type:
      blob.type || "application/pdf",
  },
);

On pure create forms, leave the field at null until the user selects the file.

View/download button missing on InputFile / InputFileMultiple

Select/radio/checkbox group does not recognize preloaded value

Fields with options compare modelValue with options[].value strictly (===). If the API returns cityId: 1 but options use value: "1", they will not match. Align types in defineForm, defaultValues, and each option value. With numeric values, 0 is a valid selection for required on single select.

File is slow to appear after system dialog selection

This is normal with heavy files: InputFile and InputFileMultiple show a loading indicator while the browser exposes the File to the model. The @benjaminor-dev/dialog-file-preview dialog also shows a loader on open and while the viewer loads content (PDF, large image, etc.).

defaultValues as function does not preload the form

List missing from parent form submit

Invalid rows dialog appears only once


MIT Β© BenjamΓ­n Olvera R.