Form Builder for Quasar
πͺπΈ VersiΓ³n en espaΓ±ol | π§π· VersΓ£o em portuguΓͺs
Quasar extension for building declarative forms in Vue 3 + Quasar apps.
Table of contents
- In 30 seconds
- What it is and what it includes
- Requirements and compatibility
- Install in a Quasar app
- What the extension adds to the host
- Global defaults (boot)
- Scaffold commands in the host
- Quick start
- Language (i18n)
- TypeScript in your IDE
- Form configuration guide
- Field presets (reusable fields)
- Public API
- Supported field catalog
- Registered directives
- Store and conceptual state/error map
- SlotField and ref usage
- Related optional extensions
- Customize scaffold template
- Public entrypoints
- Troubleshooting
In 30 seconds
Recommended flow:
- Define the payload type.
- Build config with
useFormBuilder().defineForm(...). - Render
<FormBuilder>with that config. - 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.
defineForm+<FormBuilder>β typed config; dynamic props withFormAwareValue.- ~30 field types (text, dates, local/remote selects, files,
FormList, β¦) β catalog; prop details in the IDE. inputRules,requiredOn,selectFormStore, directivesv-form-builder-*.FormListandInputFileMultipleuseDataTable(@benjaminor-dev/datatable); for declarative listings use Table Builder.- Global boot defaults (
configureFormBuilderDefaults); scaffoldbor:help,bor:make-form,bor:make-formlist,bor:make-field.
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
Recommended extension: file preview
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:
src/components/FormBuilder/LoginForm.vue
Subfolder example β create users/RegisterForm.vue and overwrite if it already exists:
npm run bor:make-form -- users/register --force
Result:
src/components/FormBuilder/users/RegisterForm.vue
Naming rules:
- The final file name is normalized to PascalCase.
- Always ends in
Form.vue. - If you pass
RegisterForm, it does not duplicateFormForm. formNamein the scaffold: PascalCase aligned with the file (e.g.LoginForm,TempForm).- You can force overwrite with
--force(or-f). - The scaffold includes
selectFormStore, a submit button, and (optional) βClear (store)β via the handle'sreset().
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
- Always ends in
Form.vue(e.g.tempβTempForm.vue, same asbor:make-form temp). - If you pass
TeamRegisterFormorTeamRegisterList, it does not duplicate suffixes. formNamein the scaffold: PascalCase aligned with the file (e.g.TempForm).- You can force overwrite with
--force(or-f). - Host template override:
scripts/bor/stubs/bor-make-formlist.stub. - The scaffold includes
selectFormStoreand a βClear list (store)β button that clears the embeddedFormListarray.
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.
- Import types from
@benjaminor-dev/quasar-app-extension-form-builder/types(e.g.FormAwareValue,BootFieldPropsFor,FieldUiConfigFor, each input's types). defineForm<MyPayload>({ ... })β the generic fixes the form shape;*Oncallbacks receive that typed payload.fieldsaccepts an array (usual form) or a callback({ preset }) => [...]when you use field presets withpreset(...). Both forms are valid.- Inside
fields[], use Ctrl+Space (Windows/Linux) or Cmd+Space (macOS):- on
type:you will see valid inputs (InputText,InputSelect, β¦); - on
props:that input's public props, with JSDoc in the tooltip on hover.
- on
fieldUi(inpropsor bootdefaultFieldProps[type]): visual tokens per type β autocomplete withFieldUiConfigFor<"InputText">(eachtypeis independent).placeholder,hint, etc. go outsidefieldUi.FormListβprops.listUi: list chrome tokens; global boot withdefaultFormListUi.- The full
typecatalog lives inFieldTypes; prop details are in the IDE as you write β this README catalog is a quick reference, not a substitute for autocomplete. - Standalone components:
@benjaminor-dev/quasar-app-extension-form-builder/inputswith the same exported types. - In boot,
localeaccepts"en" \| "es" \| "pt";messagesacceptsPartial<FormBuilderMessages>(inputs,FormList, files, pickers).
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 |
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-*.
- 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 (applied internally by
InputColorperformatModel; filters characters on type/paste) - v-form-builder-only-ip-address-chars (applied internally by
InputIpAddressperversion; filters to digits,.,:, and allowed hex) - v-form-builder-only-mac-address-chars (applied internally by
InputMacAddressperseparator; filters hex and normalizes separators with the Quasar mask)
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:
defineFormcreates (if missing) the form structure in the store with type empties or the partialdefaultValuesobject.- If
defaultValuesis a function,FormBuilderruns it ononMounted, hydratesforms, and updatesformsDefaults(reset baseline). - Each input writes to
forms[formName][field]. - Field errors live in
formsErrors[formName][field]. resetFormrestoresformsDefaultsand 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:
- Import
SlotFieldBindfrom@benjaminor-dev/quasar-app-extension-form-builder/types. - In the template:
#slot-field-{slotName}="attrs: SlotFieldBind". - In the child control:
v-model="attrs.model.value"(andattrs.error.valueif 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).
Related optional extensions
| 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:
minDate/maxDate:"YYYY-MM-DD",() => "YYYY-MM-DD"o(form) => "YYYY-MM-DD"maxRange: number,() => number, or(form) => number(days)minTime/maxTime:"HH:mm:ss",() => "HH:mm:ss"o(form) => "HH:mm:ss"
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
- Check you did not set
showPreview: falsein fieldprops(trueby default). - Install
@benjaminor-dev/dialog-file-previewand verify boots: Pinia β Form Builder β Dialog File Preview. - On
InputFile, append buttons appear only when there is a validFilein the model and the preview extension is available. OnInputFileMultiple, preview is on each inline table row. - With
disable, preview is hidden; withreadonly, preview or download works (delete/clear does not). - After
npm run buildin the host or when linking the extension withfile:, restartquasar devto load the updated Form Builderdist.
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
- The function (sync or async) runs on
FormBuilderonMounted, not when callingdefineFormin the parent. - Pass the
configreturned bydefineFormto<FormBuilder :config="config" />; do not replacedefaultValueswith a plain object before mount. - After preload you will briefly see type empties; expected until the promise resolves.
- Reset restores already preloaded values (they update
formsDefaultson hydrate).
List missing from parent form submit
- Declare the array in the form type (
teamMembers: Member[]) and usetype: "FormList"withmodel: "teamMembers"infields[]. - On
@submit, the handler receivesFormBuilderSubmitPayload<T>β it is the form itself (payload.teamMembers, notpayload.form). Same as other fields. - For an initial scaffold, use
npm run bor:make-formlist -- <name>: generates aFormBuilderwith a wiredFormListfield (same path asbor:make-form). - Before
@submit,FormBuildervalidates each visibleFormList; if invalid rows exist, the event is not emitted. - With
workflow: "review", invalid rows are also highlighted in the table; the built-in dialog lists records to fix with the fieldlabelin the header. - For custom alerts instead of the built-in dialog, define
onAfterValidateErrorin fieldprops(seeFormListcatalog).
Invalid rows dialog appears only once
- Earlier versions blocked the second submit attempt via
QFieldrules; current runtime shows the error under the field without blocking retries: each Submit pulse revalidates and may reopen the dialog. - If you defined
onAfterValidateError, the built-in dialog does not open: implement retry/alert logic there.
MIT Β© BenjamΓn Olvera R.