Form Builder para Quasar
🇺🇸 English | 🇧🇷 Versão em português
Extensión Quasar para construir formularios declarativos en apps Vue 3 + Quasar.
Tabla de contenidos
- En 30 segundos
- Qué es y qué incluye
- Requisitos y compatibilidad
- Instalación en app Quasar
- Qué agrega la extensión al host
- Defaults globales (boot)
- Comandos scaffold en host
- Uso rápido
- Idioma (i18n)
- TypeScript en tu IDE
- Guía de configuración de formularios
- Field presets (campos reutilizables)
- API pública
- Catálogo de campos soportados
- Directivas registradas
- Store y mapa conceptual de estado/errores
- SlotField y uso de refs
- Extensiones opcionales relacionadas
- Customizar plantilla scaffold
- Entrypoints públicos
- Troubleshooting
En 30 segundos
Flujo base recomendado:
- Definir tipo de payload.
- Construir config con useFormBuilder().defineForm(...).
- Renderizar FormBuilder con esa config.
- Manejar submit/reset por eventos.
La extensión instala boots, directivas y scripts en el proyecto host para que el uso sea inmediato.
Qué es y qué incluye
Form Builder arma formularios declarativos sobre Quasar: un modelo Pinia por formName, campos tipados y validación reutilizable.
defineForm+<FormBuilder>— config tipada; props dinámicas conFormAwareValue.- ~30 tipos de campo (texto, fechas, selects locales/remotos, archivos,
FormList, …) — catálogo; detalle de props en el IDE. inputRules,requiredOn,selectFormStore, directivasv-form-builder-*.FormListeInputFileMultipleusanDataTable(@benjaminor-dev/datatable); para listados declarativos usa Table Builder.- Defaults globales en boot (
configureFormBuilderDefaults); scaffoldbor:help,bor:make-form,bor:make-formlist,bor:make-field.
Requisitos y compatibilidad
| Item | Valor |
|---|---|
| Node | >=20.0.0 |
| Quasar | ^2.6.0 |
| Vue | ^3.4.18 |
| Pinia | ^2.0.11 || ^3.0.0 |
| moment | ^2.30.1 |
| DataTable | @benjaminor-dev/datatable ^0.1.0 — peer requerido |
| Formato de salida | ES modules |
El boot de Pinia del host debe correr antes del boot de store de esta extensión.
Instalación en app Quasar
Instala DataTable antes que Form Builder (peer requerido — FormList, InputFileMultiple y el check de compatibilidad lo exigen):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/form-builder
Extensión recomendada: vista previa de archivos
Con InputFile / InputFileMultiple, conviene instalar dialog-file-preview (opcional). showPreview viene en true por defecto: con la extensión, ver/descargar abre el modal; sin ella, el campo sigue como QFile estándar.
quasar ext add @benjaminor-dev/dialog-file-preview
Orden de boots (stack completo): ver Extensiones opcionales.
Remover extensión:
quasar ext remove @benjaminor-dev/form-builder
Qué agrega la extensión al host
| Recurso | Ruta |
|---|---|
| Boot store | ~@benjaminor-dev/quasar-app-extension-form-builder/boot/store |
| Boot directivas | ~@benjaminor-dev/quasar-app-extension-form-builder/boot/directives |
| CSS Form Builder | ~@benjaminor-dev/quasar-app-extension-form-builder/main.css |
| CSS DataTable | ~@benjaminor-dev/quasar-app-extension-datatable/main.css (peer; registrado también por esta extensión) |
| 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 — prefijo bor- del ecosistema @benjaminor-dev |
| Defaults (host) | src/boot/bor/bor-form-builder-defaults.ts — creado en install |
Install (quasar ext add / invoke): crea src/boot/bor/bor-form-builder-defaults.ts (o .js) y lo registra en quasar.config → boot: [] automáticamente. No hace falta editarlo a mano en una instalación normal. Los boots de paquete (boot/store, boot/directives) y CSS los inyecta la extensión en cada dev/build (no aparecen como entradas cortas en quasar.config).
Remove (quasar ext remove): elimina scaffolds generados, el defaults boot, su entrada en quasar.config y los scripts npm bor:* de esta extensión (solo si siguen apuntando a los archivos del install).
Si borras el defaults boot, la app sigue con built-in (filled, stacked, lazy, md).
Orden de boots (esta extensión): Pinia → DataTable (CSS) → boot store FB → bor/bor-form-builder-defaults → resto.
Defaults globales (boot)
Evita repetir fieldDesign, labelPosition, validationMode y gap en cada defineForm. Prioridad: defineForm / props de <FormBuilder> → boot (configureFormBuilderDefaults) → built-in (filled, stacked, lazy, md).
Tras quasar ext add, revisa src/boot/bor/bor-form-builder-defaults.ts — plantilla completa con defaultFieldProps, comentarios // recomendado, etc.
// src/boot/bor/bor-form-builder-defaults.ts (extracto)
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 es alias de configureFormBuilderDefaults.
| Opción boot | Descripción |
|---|---|
fieldDesign |
Diseño global de campos (outlined, filled, …) |
labelPosition |
inner | stacked | top |
validationMode |
lazy (recomendado) | eager |
gap |
Espaciado vertical en <FormBuilder> |
defaultFieldProps |
Props estáticas por fields[].type (BootFieldPropsFor<"InputX"> en /types); incluye fieldUi (tokens visuales del input) |
defaultFormListUi |
Tokens visuales globales de FormList (toolbar, diálogos); override en props.listUi del field |
| Boot | defineForm |
Efectivo |
|---|---|---|
filled |
— | filled |
filled |
fieldDesign: "outlined" |
outlined solo en ese form |
Comandos scaffold en host
La extensión registra scripts npm en el host para formularios y formularios con lista (FormList). Cada runner en scripts/bor/bor-make-*.mjs delega en la API npm /scaffold (runBorHelp, runBorMakeForm, …). Ejecuta npm run bor:help para ver todos los scaffolds instalados en el proyecto, opciones y guía de uso.
Ayuda (bor:help)
Lista los generadores instalados, la sintaxis con -- y las opciones comunes (--force):
npm run bor:help
Formularios (bor:make-form)
Después de -- va el nombre o ruta del formulario a generar.
npm run bor:make-form -- <nombre-o-ruta-del-formulario>
El argumento se normaliza a PascalCase y el archivo generado siempre termina en Form.vue.
Ejemplo — crear un formulario de login (LoginForm.vue):
npm run bor:make-form -- login
Resultado:
src/components/FormBuilder/LoginForm.vue
Ejemplo con subcarpeta — crear users/RegisterForm.vue y sobrescribir si ya existe:
npm run bor:make-form -- users/register --force
Resultado:
src/components/FormBuilder/users/RegisterForm.vue
Reglas de naming:
- El nombre final del archivo se normaliza a PascalCase.
- Siempre termina en
Form.vue. - Si envías
RegisterForm, no duplicaFormForm. formNameen el borrador: PascalCase alineado al archivo (p. ej.LoginForm,TempForm).- Puedes forzar overwrite con
--force(o-f). - El borrador incluye
selectFormStore, botón de envío y (opcional) «Limpiar (store)» víareset()del handle.
Formularios con lista (bor:make-formlist)
Genera un borrador Vue con FormBuilder, un campo FormList embebido y botones de envío (SlotField) en src/components/FormBuilder/ — misma convención de ruta y nombre que bor:make-form; solo cambia la plantilla:
npm run bor:make-formlist -- <nombre-o-ruta-del-formulario>
Ejemplo:
npm run bor:make-formlist -- temp
npm run bor:make-formlist -- users/TeamRegister
- Siempre termina en
Form.vue(p. ej.temp→TempForm.vue, igual quebor:make-form temp). - Si envías
TeamRegisterFormoTeamRegisterList, no duplica sufijos. formNameen el borrador: PascalCase alineado al archivo (p. ej.TempForm).- Puedes forzar overwrite con
--force(o-f). - Plantilla override en el host:
scripts/bor/stubs/bor-make-formlist.stub. - El borrador incluye
selectFormStorey un botón «Vaciar lista (store)» que limpia el arreglo delFormListembebido.
Field presets (bor:make-field)
Genera un archivo reutilizable en src/components/FormBuilder/Fields/ con defineFieldPreset — para selects, servidores u otros campos que repites en muchos formularios.
npm run bor:make-field -- <NombrePreset> [FieldType]
| Entrada | Genera |
|---|---|
CountrySelect InputSelect |
Fields/CountrySelect.field.ts → export countrySelectField |
catalog/CitySelect InputSelectServer |
Fields/catalog/CitySelect.field.ts |
CountrySelect (sin tipo) |
Menú interactivo en terminal para elegir el type |
selectserver (como tipo) |
Acepta el nombre sin distinguir mayúsculas (InputSelectServer) |
--force / -f |
Sobrescribe si el archivo ya existe |
Ejemplo — preset de país (select local):
npm run bor:make-field -- CountrySelect InputSelect
Edita el archivo generado (servicio, options, iconos) y úsalo en cualquier defineForm — ver Field presets.
Plantilla override en el host: scripts/bor/stubs/bor-make-field.stub.
Uso rápido
Ejemplo mínimo del escenario alta de cliente corporativo (identidad + envío). El ejemplo integrado más abajo muestra el formulario de referencia casi completo.
<script setup lang="ts">
import FormBuilder, {
useFormBuilder,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
type CorporateIdentityForm = {
legalName: string | null;
tradeName: string | null;
taxId: string | null;
brandColor: string | null;
corporateEmail: string | null;
acceptContract: boolean;
};
const { defineForm, inputRules } = useFormBuilder();
const config = defineForm<CorporateIdentityForm>({
formName: "corporateIdentityForm",
fieldDesign: "filled",
labelPosition: "stacked",
fields: [
{
type: "InputText",
model: "legalName",
label: "Razón social",
requiredOn: () => true,
rules: [
inputRules.legalTextExtendedRule(),
],
props: {
placeholder: "Ej. Acme Servicios S.A. de C.V.",
},
},
{
type: "InputText",
model: "tradeName",
label: "Nombre comercial",
readonlyOn: (form) => !form?.legalName,
props: {
placeholder: "Opcional — cómo se conoce la marca",
},
},
{
type: "InputText",
model: "taxId",
label: "RFC / identificador fiscal",
requiredOn: (form) => !!form?.legalName,
rules: [
inputRules.rfc(),
],
props: {
placeholder: "Ej. ACME850101ABC",
maxLength: 13,
},
},
{
type: "InputColor",
model: "brandColor",
label: "Color corporativo",
props: {
defaultColor: "#1976D2",
placeholder: "#RRGGBB",
},
},
{
type: "InputEmail",
model: "corporateEmail",
label: "Correo corporativo",
requiredOn: () => true,
props: {
placeholder: "contacto@empresa.com",
},
},
{
type: "ToggleButton",
model: "acceptContract",
label: "Acepto contrato marco y tratamiento de datos",
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="Limpiar" />
<q-btn type="submit" color="primary" label="Enviar solicitud" />
</div>
</template>
</FormBuilder>
</template>
Idioma (i18n)
Integrado en, es y pt. Por defecto los textos de inputs, FormList y archivos siguen el idioma de Quasar ($q.lang.isoName); si Quasar no tiene idioma, el fallback es en.
Resolución de locale: boot locale → prefijo de $q.lang.isoName (es* → es, pt* → pt) → en.
| Opción | Cuándo usarla |
|---|---|
framework.lang en quasar.config.ts |
Recomendado — alinea Form Builder con el resto de Quasar |
locale: 'es' en boot |
Fijar idioma (en | es | pt); ignora cambios de $q.lang |
messages: { formListAdd: '…' } en boot |
Cambiar un texto sin fijar locale |
Ejemplo en quasar.config.ts (español para toda la app):
// quasar.config.ts
framework: {
lang: "es",
},
Plantilla boot (tras install):
configureFormBuilderDefaults({
// locale: "es", // fija solo el idioma de Form Builder
// messages: { formListAdd: "Agregar" },
});
Claves i18n: acciones comunes (clear, save, cancel, preview), aria de inputs (showHidePassword, openDatePicker, clearField), toolbar del editor, chrome de FormList (formListAdd, formListValidationTitle, formListMinimized, …) y rechazos de archivo (fileRejectionMaxSize, fileCapacityOverLimit con plantillas {max}, {name}, {reason}). Catálogo completo en FormBuilderMessages (/types).
Prioridad de mensajes: boot messages fusiona sobre el catálogo built-in del locale activo.
API en runtime: useFormBuilderI18n() devuelve messages reactivos según $q.lang.isoName. Para acceso imperativo (tests, código fuera de Vue), usa resolveFormBuilderMessages(isoName?) — misma resolución boot → $q.lang → en.
import {
configureFormBuilderDefaults,
resolveFormBuilderMessages,
useFormBuilderI18n,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
configureFormBuilderDefaults({ locale: "es" });
const msgs = resolveFormBuilderMessages(); // locale de boot
// Dentro de un componente:
const { messages } = useFormBuilderI18n();
// messages.value.formListAdd, messages.value.clear, …
TypeScript en tu IDE
No hace falta memorizar cada prop: el tipado y el autocompletado te muestran el contrato mientras escribes.
- Importa tipos desde
@benjaminor-dev/quasar-app-extension-form-builder/types(p. ej.FormAwareValue,BootFieldPropsFor,FieldUiConfigFor, tipos de cada input). defineForm<MiPayload>({ ... })— el genérico fija la forma del formulario; los callbacks*Onreciben ese payload tipado.fieldsadmite un arreglo (forma habitual) o un callback({ preset }) => [...]cuando instancias field presets conpreset(...). Ambas formas son válidas.- Dentro de
fields[], usa Ctrl+Space (Windows/Linux) o Cmd+Space (macOS):- en
type:verás los inputs válidos (InputText,InputSelect, …); - en
props:las props públicas de ese input, con JSDoc en el tooltip al pasar el cursor.
- en
fieldUi(enpropso bootdefaultFieldProps[type]): tokens visuales por tipo — autocompletado conFieldUiConfigFor<"InputText">(cadatypees independiente).placeholder,hint, etc. van fuera defieldUi.FormList→props.listUi: tokens del chrome de lista; boot global condefaultFormListUi.- El catálogo completo de
typevive enFieldTypes; el detalle de props lo tienes en el IDE al escribir — el catálogo del README es referencia rápida, no sustituto del autocompletado. - Componentes sueltos:
@benjaminor-dev/quasar-app-extension-form-builder/inputscon los mismos tipos exportados. - En boot,
localeacepta"en" \| "es" \| "pt";messagesaceptaPartial<FormBuilderMessages>(inputs,FormList, archivos, pickers).
Si una prop no aparece en autocompletado, probablemente no existe en la API pública o el type del campo no la admite.
Guía de configuración de formularios
Anatomía de un campo
Cada entrada de fields[] describe un control del formulario:
| Propiedad | Descripción |
|---|---|
type |
Tipo de campo (InputText, InputSelect, …) |
model |
Clave en el modelo tipado del formulario |
label |
FormAwareValue<string> |
requiredOn / hideOn / disabledOn / readonlyOn / unmountOn |
Comportamiento condicional (form?) => boolean |
rules |
Reglas Quasar adicionales — ver inputRules |
props |
Configuración del componente (placeholder, hint, helpTooltip, prependIcon, FormAwareValue, …) |
onChange, onMounted, … |
Efectos con acceso opcional al formulario |
// dentro de defineForm({ fields: [ ... ] })
{
type: "InputEmail",
model: "corporateEmail",
label: "Correo corporativo",
requiredOn: () => true,
rules: [
inputRules.email(),
],
props: {
placeholder: "nombre@empresa.com",
clearable: true,
},
onChange: (newValue, _oldValue, form) => {
console.log(newValue, form?.legalName);
},
},
requiredOn ya inyecta la regla required; no la dupliques en rules salvo que necesites otra validación independiente.
Nivel raíz vs props: en el objeto del campo van type, model, label, rules, callbacks condicionales (*On) y lifecycles (onChange, …). La presentación del widget (placeholder, hint, helpTooltip, prependIcon) va siempre dentro de props del tipo de input correspondiente. label (nivel raíz o en props), placeholder, hint, helpTooltip y prependIcon aceptan valor fijo o callback según el modelo (ver tabla).
Props de presentación del widget (props)
| Prop | Tipo | Descripción |
|---|---|---|
placeholder |
FormAwareValue<string> |
Texto de ayuda dentro del control (no aplica a grupos, toggle, archivos, editor, SlotField) |
hint |
FormAwareValue<string | undefined> |
Texto bajo el campo (Quasar hint) |
helpTooltip |
FormAwareValue<string> |
Ayuda contextual: icono ? en prepend en la mayoría de inputs; excepción InputCheckbox (icono ? junto al label, sin prepend marginal). En InputFile / InputFileMultiple con dragDrop vacío se muestra como leyenda bajo dropHint |
prependIcon |
FormAwareValue<string> |
Icono Material en prepend; callback (form) => …. En FormList: FormListPresentationProp<string | null> (= FormAware con FormListAwareContext) — (data) => … con data.parentForm / data.items. No aplica a SlotField ni InputCheckbox |
Tooltip nativo del label: en pantallas estrechas el texto del label puede truncarse. Al pasar el cursor sobre el campo o sobre el label visible, el navegador muestra el label completo (atributo HTML title, resuelto con FormAware igual que label). No confundir con helpTooltip (popover del ?).
// dentro de fields: [ ... ]
{
type: "InputText",
model: "legalName",
label: "Razón social",
requiredOn: () => true,
props: {
placeholder: "Ej. Acme Servicios S.A. de C.V.",
hint: "Nombre registrado ante el SAT.",
helpTooltip: "Debe coincidir con el acta constitutiva.",
},
},
Callbacks condicionales
| Callback | Efecto |
|---|---|
requiredOn |
Obligatoriedad condicional (inyecta required) |
hideOn |
Oculta con v-show; el valor en el store se conserva |
unmountOn |
Si devuelve false, desmonta el campo del DOM |
disabledOn |
Desactiva interacción |
readonlyOn |
Solo lectura |
Firma: (form?: FormBuilderSubmitPayload<T>) => boolean. Usa form?.campo o () => valor cuando no necesites el modelo.
Excepciones por tipo de campo:
| Tipo | requiredOn |
disabledOn / readonlyOn |
|---|---|---|
| Inputs estándar | Validación Quasar automática | Bloqueo automático en el componente |
ToggleButton |
Obligatorio = valor true (marcado) |
Bloqueo del toggle |
SlotField separador (sin model) |
No usar | No usar |
SlotField custom (con model) |
Pasas required al slot; tú enlazas :rules si quieres validación al submit |
Pasas readonly y disable al control del slot |
Ordena fields[] de arriba hacia abajo: los callbacks condicionales deben depender solo de campos anteriores (ver ejemplo integrado).
Valores iniciales (defaultValues)
Propiedad opcional. Si la omites, cada campo con model recibe el vacío según su type:
| Vacío por defecto | Tipos de campo |
|---|---|
null |
Texto, email, número, fecha, select simple, archivo único, etc. |
[] |
InputCheckboxGroup, InputSelectMultiple, InputSelectServerMultiple, InputFileMultiple, FormList |
false |
ToggleButton, InputCheckbox |
Puedes pasar un objeto parcial con solo los overrides que necesites; las claves omitidas usan la tabla anterior.
También puedes pasar una función (sync o async) que devuelve un objeto parcial. FormBuilder la ejecuta en onMounted — útil para precarga o edición desde API. Al resolver, actualiza el store y la base del reset (resetForm restaura lo precargado, no el estado vacío intermedio).
defaultValues: async () => {
const record = await fetchCorporateRecord(id);
return {
legalName: record.name,
corporateEmail: record.email,
billingCountry: record.country,
};
},
El formulario arranca con vacíos por tipo; tras la precarga puede verse un instante con esos vacíos antes de hidratar los valores. Puedes llamar defineForm en el componente padre y pasar el config resultante a FormBuilder; la precarga se ejecuta igual al montar el formulario.
Props y callbacks según el formulario (FormAware)
Además de los callbacks condicionales de campo (requiredOn, hideOn, …), puedes leer el modelo actual en props de configuración y en callbacks de efecto. Tres capas complementarias:
| Capa | Mecanismo | Cuándo usarla |
|---|---|---|
| Condicional de campo | requiredOn, hideOn, disabledOn, readonlyOn, unmountOn |
Mostrar, bloquear u obligar el campo |
| Prop de configuración | FormAwareValue<T> = T | (form?) => T |
Límites, textos, flags que dependen del modelo (sin efectos secundarios) |
| Callback de efecto | Último argumento opcional form? |
Reaccionar al cambio o a una acción explícita |
FormAwareValue — valor fijo o (form?) => T. Sin efectos secundarios; ideal para límites, textos y flags. Cada campo documenta sus props en el catálogo.
Además de límites y flags por tipo, label (nivel raíz), placeholder, hint, helpTooltip y prependIcon (en props) aceptan FormAwareValue para textos e iconos que cambian con el modelo.
En FormList, las mismas props de presentación en props usan FormListPresentationProp<T> (homólogo FormAware con contexto parentForm / items / draftItem): callback (data) => … en lugar de (form) => ….
// dentro de fields: [ ... ]
{
type: "InputFile",
model: "contractPdf",
label: "Contrato",
props: {
maxSize: (form) =>
form?.plan === "enterprise" ? 10 * 1024 * 1024 : 5 * 1024 * 1024,
},
},
{
type: "InputIntlPhone",
model: "contactIntlPhone",
label: (form) =>
form?.billingCountry === "US"
? "US contact phone"
: "Teléfono internacional",
props: {
hint: (form) =>
form?.billingCountry && form.billingCountry !== "MX"
? "Obligatorio fuera de México."
: undefined,
},
},
Callbacks con form — el snapshot del modelo se pasa como último argumento opcional:
| Callback | Firma |
|---|---|
onChange (cualquier campo) |
(newValue, oldValue, form?) => void |
onBeforeMount / onMounted |
(fieldProps, form?) => void |
onSearch (InputSearch) |
(value, form?) => void |
optionLabel (grupos y selects) |
(option, form?) => string |
El pipeline de opciones ya exponía form en options(form), filterOptions(options, form) y callOptionsOn(form). Con optionLabel(option, form?) los textos visibles se recalculan cuando cambia el modelo sin volver a cargar opciones.
optionLabel: (option, form) => {
const discount = Number(form?.negotiatedDiscount ?? 0);
return discount > 0
? `${option.label} (desc. ${discount}%)`
: `${option.label} (${option.meta?.tier ?? "n/a"})`;
},
Tipos exportados en /types: FormAwareValue, FormSnapshot, FormListAwareValue, FormListPresentationProp, OptionLabelFn.
Patrones avanzados: condicional + servidor + SlotField
Formulario de referencia que combina hideOn encadenado, catálogo remoto y secciones con SlotField. El flujo es solo hacia adelante: cada bloque depende de valores ya capturados. El fragmento siguiente es ilustrativo; combínalo con el catálogo y el autocompletado del IDE.
function planOptionsVisible(form?: { priorityScore?: number | null }) {
return Number(form?.priorityScore ?? 0) >= 5;
}
const config = defineForm<CorporateForm>({
formName: "corporateOnboardingForm",
fields: [
{
type: "InputNumber",
model: "priorityScore",
label: "Prioridad (1–10)",
props: { min: 1, max: 10 },
},
{ type: "SlotField", props: { slotName: "section-location" } },
{
type: "InputSelect",
model: "billingCountry",
label: "País de facturación",
hideOn: (form) => !planOptionsVisible(form),
props: {
callOptionsOn: (form) => form?.priorityScore,
options: (form) => (planOptionsVisible(form) ? COUNTRY_OPTIONS : []),
},
},
{
type: "InputIntlPhone",
model: "contactIntlPhone",
label: "Teléfono internacional",
hideOn: (form) => !form?.billingCountry || form.billingCountry === "MX",
requiredOn: (form) =>
form?.billingCountry != null && form.billingCountry !== "MX",
},
{
type: "InputSelectServer",
model: "headquartersCity",
label: "Ciudad sede (catálogo remoto)",
hideOn: (form) => !form?.billingCountry,
props: {
callOptionsOn: (form) => form?.billingCountry,
optionsService: fetchMockCities,
usePagination: true,
searchOnServer: true,
perPage: 50,
},
},
{ type: "SlotField", props: { slotName: "actions" } },
],
});
En la plantilla, nombra los slots #slot-field-{slotName} (p. ej. #slot-field-section-location, #slot-field-actions).
Flujo condicional resumido (mismo formulario de referencia):
| Disparador | Efecto (campos posteriores) |
|---|---|
| Aceptar contrato | Cláusulas (InputEditor), contrato PDF (InputFile) y anexos (InputFileMultiple) visibles; PDF y cláusulas requeridos |
| Prioridad ≥ 5 | Descuento, plan, país, canales |
| Plan Enterprise | Presupuesto anual obligatorio |
| País ≠ México | Teléfono internacional visible y requerido |
| País elegido | Ciudad sede (catálogo remoto) |
Field presets (campos reutilizables)
Cuando el mismo campo se repite en muchos formularios (mismo servicio, mismas opciones, mismo icono…), extrae la config a un field preset en src/components/FormBuilder/Fields/.
Las dos formas de fields (compatibles)
| Forma | Cuándo usarla |
|---|---|
fields: [ ... ] |
Formularios sin presets o campos totalmente inline. |
fields: ({ preset }) => [ ... ] |
Con field presets: mezcla inline + preset(...) sin repetir el genérico del formulario. |
No hace falta migrar formularios existentes: el arreglo clásico sigue siendo válido. El callback solo aplica al defineForm raíz; FormList → itemForm.fields sigue siendo arreglo.
1. Crear el preset
npm run bor:make-field -- CountrySelect InputSelect
// src/components/FormBuilder/Fields/CountrySelect.field.ts
import { defineFieldPreset } from "@benjaminor-dev/quasar-app-extension-form-builder";
export const countrySelectField = defineFieldPreset("InputSelect", {
label: "País",
props: {
prependIcon: "public",
placeholder: "Seleccione país",
options: [
{ label: "México", value: "MX" },
{ label: "Colombia", value: "CO" },
],
},
});
2. Usarlo en el formulario
import { countrySelectField } from "src/components/FormBuilder/Fields/CountrySelect.field";
defineForm<RegisterForm>({
formName: "register",
fields: ({ preset }) => [
{ type: "InputText", model: "legalName", label: "Razón social" },
preset(countrySelectField, { model: "country" }),
preset(countrySelectField, {
model: "billingCountry",
label: "País de facturación",
}),
],
});
API de presets
| Export | Uso |
|---|---|
defineFieldPreset(type, base) |
Define un preset exportable (archivo en Fields/) |
preset(fieldPreset, overrides) |
Dentro de fields: ({ preset }) => … — model obligatorio; props parcial y FormAware |
Los overrides respetan el mismo tipado que un campo inline (hideOn, label: (form) => …, props.placeholder, etc.).
API pública
useFormBuilder() retorna:
| Miembro | Uso |
|---|---|
defineForm<T>(config) |
Construye la configuración del formulario (config tipado como DefineFormInput<T> en /types) |
defineFieldPreset |
Field presets reutilizables — ver Field presets |
inputRules |
Reglas de validación reutilizables |
selectFormStore<T>(formName) |
Vista tipada de la sesión Pinia (values, errors, reset, …) |
utils |
Helpers para condicionales y búsqueda tolerante |
defineForm(config)
| Propiedad | Tipo | Descripción |
|---|---|---|
formName |
string |
Identificador único del formulario |
fields |
FormField<T>[] | ({ preset }) => FormField<T>[] |
Listado de campos (arreglo clásico o callback con preset() para field presets) |
defaultValues |
Partial<T> | () => Partial<T> | Promise<Partial<T>> |
Opcional. Overrides por campo; las claves omitidas usan el vacío del tipo ([], false, null). Si es función, se ejecuta en onMounted (precarga / edición) |
fieldDesign |
standard | outlined | filled | standout | borderless |
Diseño global (default filled). No aplica a SlotField ni ToggleButton |
labelPosition |
inner | stacked | top |
Posición del label (default stacked) |
validationMode |
lazy | eager |
Cuándo valida Quasar (default lazy: blur o submit) |
Prop de <FormBuilder> (no en defineForm):
| Propiedad | Tipo | Descripción |
|---|---|---|
gap |
false | xs | sm | md | lg | xl |
Gutter vertical entre campos (default md) |
Presentación global (fieldDesign, labelPosition, validationMode, gap, defaultFieldProps): § Defaults globales (boot).
Formularios grandes y hijos con el modelo
Un solo FormBuilder por pantalla (un <q-form>). Reparte fields en módulos .ts o en Fields/*.field.ts (presets). Para leer o mutar el modelo en composables, cabeceras o hijos profundos, usa selectFormStore:
const { defineForm, selectFormStore } = useFormBuilder();
const formName = "LoginForm";
const config = defineForm<LoginForm>({
formName,
fields: [...identityFields, ...billingFields],
defaultValues: { email: null },
});
// Mismo estado en cualquier componente o 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();
| Necesidad | API recomendada |
|---|---|
| Modelo reactivo (padre, hijo, composable) | selectFormStore<T>(formName).values |
| Errores por campo | selectFormStore<T>(formName).errors / hasErrors |
| Asignar valores | setValues({ ... }) |
| Restaurar defaults | reset() |
| Varios forms en la misma página | Un formName distinto por formulario |
FormBuilder (componente)
| Prop | Tipo | Descripción |
|---|---|---|
config |
retorno de defineForm |
Configuración del formulario |
gap |
false | xs | sm | md | lg | xl |
Espaciado vertical entre campos: aplica q-gutter-{size} en el <q-form> (default md). Con :gap="false" no añade gutter integrado — usa tu propio espaciado en class o style |
Por defecto los campos quedan separados con q-gutter-md. Para otro tamaño Quasar, cambia la prop; para Tailwind (gap-4), una clase SCSS o style, desactiva el gutter integrado y define el gap en el host:
<!-- Default: q-gutter-md -->
<FormBuilder :config="config" @submit="onSubmit" />
<!-- Otro tamaño Quasar -->
<FormBuilder :config="config" gap="sm" @submit="onSubmit" />
<!-- Espaciado propio (Tailwind, SCSS, style, …) -->
<FormBuilder
:config="config"
:gap="false"
class="flex flex-col gap-4"
@submit="onSubmit"
/>
<FormBuilder
:config="config"
:gap="false"
:style="{ gap: '1.25rem' }"
@submit="onSubmit"
/>
Eventos:
| Evento | Payload | Descripción |
|---|---|---|
@submit |
form |
Valores actuales tras validación de Quasar y de cada FormList visible del config |
@reset |
form, resetForm |
Al pulsar un control type="reset". resetForm() restaura defaultValues y limpia errores. Si no escuchas @reset, el reset del store se aplica automáticamente |
Ejemplo con reset personalizado:
<script setup lang="ts">
import type { FormBuilderResetAction } from "@benjaminor-dev/quasar-app-extension-form-builder/types";
function onReset(
_form: CorporateIdentityForm,
resetForm: FormBuilderResetAction,
) {
if (confirm("¿Descartar cambios?")) {
resetForm();
}
// Si no llamas resetForm(), el formulario conserva los valores actuales
}
</script>
<template>
<FormBuilder :config="config" @submit="onSubmit" @reset="onReset" />
</template>
inputRules
Reglas para el arreglo rules de cada campo. La obligatoriedad va en requiredOn, no aquí — inputRules no expone required. En InputDate, las reglas de fecha reciben el valor interno YYYY-MM-DD; en InputNumber, InputDecimal y InputCurrency el modelo es string | null.
| Regla | Valida | Parámetros | Ejemplo |
|---|---|---|---|
email |
Correo electrónico | message? |
inputRules.email() |
url |
URL http/https |
message? |
inputRules.url() |
ip |
Dirección IPv4 o IPv6 (version opcional: 'ipv4', 'ipv6', 'both') |
message?, version? |
inputRules.ip() · inputRules.ip("IP inválida", "ipv4") |
mac |
Dirección MAC de 6 octetos hex (separator opcional) |
message?, separator? |
inputRules.mac() · inputRules.mac("MAC inválida", "dash") |
uuid |
UUID (versiones 1–8) | message? |
inputRules.uuid() |
password |
Mín. 8 caracteres, mayúscula, minúscula y símbolo | message? |
inputRules.password() |
regex |
Patrón personalizado | pattern, message? |
inputRules.regex(/^[A-Z]{3}$/) |
rfc |
RFC mexicano | message? |
inputRules.rfc() |
curp |
CURP mexicana | message? |
inputRules.curp() |
postalCode |
Código postal de 5 dígitos | message? |
inputRules.postalCode() |
onlyLetters |
Solo letras | message? |
inputRules.onlyLetters() |
onlyLettersAndSpaces |
Letras y espacios | message? |
inputRules.onlyLettersAndSpaces() |
onlyNumbers |
Solo dígitos | message? |
inputRules.onlyNumbers() |
onlyNumbersAndLetters |
Alfanumérico sin espacios | message? |
inputRules.onlyNumbersAndLetters() |
onlyNumbersLettersAndSpaces |
Alfanumérico con espacios | message? |
inputRules.onlyNumbersLettersAndSpaces() |
alphaDash |
Letras, dígitos, - y _ |
message? |
inputRules.alphaDash() |
legalTextRule |
Texto en una línea (sin emojis) | message? |
inputRules.legalTextRule() |
legalTextExtendedRule |
Texto multilínea permitido | message? |
inputRules.legalTextExtendedRule() |
startsWith |
Prefijo obligatorio | prefix, message? |
inputRules.startsWith("MX-") |
endsWith |
Sufijo obligatorio | suffix, message? |
inputRules.endsWith(".pdf") |
date |
Fecha YYYY-MM-DD |
message? |
inputRules.date() |
dateBeforeNow |
Fecha anterior a hoy | message? |
inputRules.dateBeforeNow() |
dateLTEnow |
Fecha ≤ hoy | message? |
inputRules.dateLTEnow() |
dateAfterNow |
Fecha posterior a hoy | message? |
inputRules.dateAfterNow() |
dateGTEnow |
Fecha ≥ hoy | message? |
inputRules.dateGTEnow() |
dateMin |
Fecha ≥ límite | minDate, message? |
inputRules.dateMin("2025-01-01") |
dateMax |
Fecha ≤ límite | maxDate, message? |
inputRules.dateMax("2025-12-31") |
minLength |
Longitud mínima | min, message? |
inputRules.minLength(3) |
maxLength |
Longitud máxima | max, message? |
inputRules.maxLength(120) |
length |
Longitud exacta | length, message? |
inputRules.length(10) |
between |
Valor, longitud o cantidad en rango | min, max, message? |
inputRules.between(1, 100) |
minNumberValue |
Número ≥ tope | min, message? |
inputRules.minNumberValue(1) |
maxNumberValue |
Número ≤ tope | max, message? |
inputRules.maxNumberValue(100) |
integer |
Entero (sin decimales) | message? |
inputRules.integer() |
decimal |
Decimales en rango (si hay punto) | minDecimals, maxDecimals?, message? |
inputRules.decimal(2, 4) |
inValue |
Valor en lista permitida | values[], message? |
inputRules.inValue(["draft", "published"]) |
notInValues |
Valor fuera de lista prohibida | values[], message? |
inputRules.notInValues(["admin"]) |
noRepeatedNumbers |
Sin un solo dígito repetido | message? |
inputRules.noRepeatedNumbers() |
file |
Instancia File (o arreglo de File) |
message? |
inputRules.file() |
image |
Archivo(s) con MIME image/* |
message? |
inputRules.image() |
{
type: "InputText",
model: "legalName",
label: "Razón social",
requiredOn: () => true,
rules: [inputRules.legalTextExtendedRule(), inputRules.minLength(3)],
},
InputFile aplica file internamente; no hace falta repetirla salvo validación manual fuera de ese campo.
selectFormStore
Acceso tipado al estado Pinia del formulario (también disponible como función independiente desde el entrypoint principal).
Fuera del template, usa selectFormStore<T>(formName) (values, errors, setValues, reset, …) — ver Formularios grandes y hijos con el modelo y Store y mapa conceptual de estado/errores.
utils
Utilidades generales para lógica condicional en el host, normalización de búsqueda y limpieza numérica. Disponibles con const { utils } = useFormBuilder().
| Función | Descripción |
|---|---|
| isEmpty / isNotEmpty | Comprueba vacío (null, undefined, "", arrays u objetos anidados) |
| cleanText | Normaliza texto para búsqueda tolerante (minúsculas, sin acentos ni espacios/guiones) |
| cleanNumber | Elimina ceros a la izquierda mientras el usuario escribe un número |
| cleanLeadingZeros / cleanTrailingZeros | Variantes de limpieza numérica en strings |
cleanText alinea la búsqueda remota con la de InputSelect / InputSelectServer y con el selector de países en InputIntlPhone. isEmpty / isNotEmpty evitan reimplementar vacío en requiredOn, hideOn u options. Ver ejemplo integrado (fetchMockCities).
Catálogo de campos soportados
Referencia rápida de fields[].type. Props, reglas y ejemplos al día: autocompletado en el IDE (type + props con Ctrl/Cmd+Space) y tipos en /types — ver TypeScript en tu IDE.
type |
/inputs |
Diseño* | model típico |
Notas |
|---|---|---|---|---|
InputText |
✓ | ✓ | string | null |
Texto general |
InputTextarea |
✓ | ✓ | string | null |
Multilínea |
InputEmail |
✓ | ✓ | string | null |
Regla email integrada |
InputUrl |
✓ | ✓ | string | null |
Regla URL integrada |
InputSearch |
✓ | ✓ | string | null |
Icono búsqueda + debounce |
InputPassword |
✓ | ✓ | string | null |
Toggle visibilidad |
InputNumber |
✓ | ✓ | string | null |
Solo dígitos en texto |
InputNumericCode |
✓ | ✓ | string | null |
Código numérico (ceros a la izquierda) |
InputOtp |
✓ | ✓ | string | null |
Celdas OTP; onComplete |
InputDecimal |
✓ | ✓ | string | null |
Decimal formateado |
InputCurrency |
✓ | ✓ | string | null |
Moneda + símbolo |
InputPercent |
✓ | ✓ | number |
Steppers ±; 0–100 default |
InputSlider |
✓ | ✓ | number |
Solo arrastre (QSlider) |
InputSliderRange |
✓ | ✓ | { min, max } | null |
Rango con dos thumbs |
InputCheckbox |
✓ | — | boolean |
Sin fieldDesign; borderless |
InputCheckboxGroup |
✓ | — | (string | number)[] |
Opciones locales; multiselección |
InputRadioGroup |
✓ | ✓ | string | number | null |
Opciones locales; una selección |
InputSelect |
✓ | ✓ | string | number | null |
Dropdown + búsqueda local |
InputSelectMultiple |
✓ | ✓ | (string | number)[] |
Chips; multiselección |
InputSelectServer |
✓ | ✓ | string | number | null |
Catálogo remoto; optionsService |
InputSelectServerMultiple |
✓ | ✓ | (string | number)[] |
Remoto + chips |
InputDate |
✓ | ✓ | string | null |
YYYY-MM-DD; picker; clearable (default true) |
InputDateRange |
✓ | ✓ | rango fechas | minDate / maxDate / maxRange; clearable |
InputTime |
✓ | ✓ | string | null |
HH:mm:ss; 12h/24h; clearable |
InputTel |
✓ | ✓ | string | null |
Solo dígitos; formato país |
InputIntlPhone |
✓ | ✓ | string | null |
E.164 o structured; selector país |
InputIpAddress |
✓ | ✓ | string | null |
IPv4/IPv6; inputRules.ip() |
InputMacAddress |
✓ | ✓ | string | null |
Máscara MAC; inputRules.mac(); clearable |
InputColor |
✓ | ✓ | string | null |
Hex/RGB; picker modal; clearable |
InputEditor |
✓ | ✓ | string | null |
HTML sanitizado; toolbar; clearable |
InputFile |
✓ | ✓ | File | null |
Un archivo; drag & drop; preview opcional |
InputFileMultiple |
✓ | ✓ | File[] |
Varios archivos; tabla inline |
FormList |
— | — | T[] |
Lista CRUD en tabla + diálogo; maximize centra la columna del ítem (~900px) |
ToggleButton |
✓ | — | boolean |
Booleano estilo toggle |
SlotField |
— | — | según slot | UI custom vía #slot-field-{name} |
* Diseño = soporta fieldDesign / labelPosition del formulario. — = presentación propia (borderless u otro).
Tipos por campo: FieldTypes["InputSelect"], BootFieldPropsFor<"InputSelect">, etc. en @benjaminor-dev/quasar-app-extension-form-builder/types.
Ejemplos representativos
Texto + select local
// dentro de fields: [ ... ]
{ type: "InputText", model: "legalName", label: "Razón social", requiredOn: () => true },
{
type: "InputSelect",
model: "plan",
label: "Plan",
props: {
options: [
{ label: "Básico", value: "basic" },
{ label: "Pro", value: "pro" },
],
clearable: true,
},
},
Fecha con límites dinámicos
{
type: "InputDate",
model: "startDate",
label: "Inicio",
props: {
minDate: () => moment().format("YYYY-MM-DD"),
maxDate: (form) => form?.endDate ?? undefined,
clearable: true,
},
},
Limpiar valor (clearable) — en pickers (InputDate, InputDateRange, InputTime, InputColor), InputMacAddress e InputEditor: prop FormAwareValue<boolean> (default true). Desactívala por campo (clearable: false) o en boot (defaultFieldProps.InputDate: { clearable: false }). InputSelect* usa el :clearable nativo de Quasar.
Archivo con preview (extensión dialog-file-preview opcional en el host)
{
type: "InputFile",
model: "contract",
label: "Contrato",
props: {
accept: ".pdf",
maxSize: 10 * 1024 * 1024,
dragDrop: true,
showPreview: true,
},
},
Select remoto (catálogo grande / API)
{
type: "InputSelectServer",
model: "cityId",
label: "Ciudad",
props: {
optionsService: async ({ query, pagination, form }) => {
const res = await api.searchCities({ q: query, country: form?.country, ...pagination });
return { items: res.items, pagination: res.meta };
},
searchOnServer: true,
usePagination: true,
perPage: 50,
},
},
FormList embebido
{
type: "FormList",
model: "teamMembers",
label: "Integrantes",
requiredOn: () => true,
props: {
listName: "members",
itemLabel: "Miembro",
itemForm: {
fields: [
{ type: "InputText", model: "fullName", label: "Nombre", requiredOn: () => true },
],
},
columns: [{ name: "fullName", label: "Nombre", field: "fullName" }],
},
},
Slot de columna en el FormBuilder padre: #form-list-members-column-fullName. Más detalle en ejemplo integrado y tipos FormListConfig, InputFormListProps en /types.
Directivas registradas
Convención runtime: form-builder-{directiveName}. En templates Vue se usan como v-form-builder-*.
- v-form-builder-max-length
- v-form-builder-only-numbers
- v-form-builder-only-decimal-number
- v-form-builder-format-number
- v-form-builder-max-number
- v-form-builder-currency-symbol
- v-form-builder-color-input (aplicada internamente por
InputColorsegúnformatModel; filtra caracteres al escribir/pegar) - v-form-builder-only-ip-address-chars (aplicada internamente por
InputIpAddresssegúnversion; filtra a dígitos,.,:y hex permitidos) - v-form-builder-only-mac-address-chars (aplicada internamente por
InputMacAddresssegúnseparator; filtra hex y normaliza separadores junto a la máscara Quasar)
Store y mapa conceptual de estado/errores
Acceso: useFormBuilder().selectFormStore(formName) o selectFormStore(formName) importado del paquete.
selectFormStore<T>(formName)
Vista tipada de la sesión Pinia. El mismo formName en otro .vue o composable apunta al mismo estado.
| Miembro | Descripción |
|---|---|
formName |
Identificador (defineForm.formName) |
values |
Ref<T> — modelo anidado reactivo |
flat |
Ref — modelo aplanado (claves con punto) |
errors |
Errores por campo (reactivo) |
hasErrors |
true si hay algún mensaje |
getValues() / getFlat() |
Snapshots no reactivos |
setValues(partial) |
Asigna por claves planas |
hydrateDefaults(partial) |
Precarga y actualiza base del reset |
setErrors / clearErrors |
Errores de validación |
reset() |
Restaura defaults y limpia errores |
Tipo exportado: FormSessionHandle<T> en /types.
Flujo conceptual:
defineFormcrea (si no existe) la estructura del formulario en el store con vacíos por tipo o con el objeto parcial dedefaultValues.- Si
defaultValueses función,FormBuilderla ejecuta enonMounted, hidrataformsy actualizaformsDefaults(base del reset). - Cada input escribe en
forms[formName][campo]. - Errores de campo viven en
formsErrors[formName][campo]. resetFormrestauraformsDefaultsy limpia errores.
SlotField y uso de refs
En slots de SlotField con model, model y error son refs y se enlazan con .value.
| Nombre | Qué es |
|---|---|
type: "SlotField" |
Tipo de campo en config.fields[] |
SlotFieldProps |
Props del field (slotName, lifecycles) — /types |
SlotFieldBind |
Payload del slot scoped (attrs) — /types |
Patrón canónico en el host:
- Importar
SlotFieldBinddesde@benjaminor-dev/quasar-app-extension-form-builder/types. - En el template:
#slot-field-{slotName}="attrs: SlotFieldBind". - En el control hijo:
v-model="attrs.model.value"(yattrs.error.valuesi aplica).
Los separadores de sección (slotName: "section-…", sin model) no usan attrs.model; solo markup estático.
Para campos custom con model, FormBuilder resuelve requiredOn / disabledOn / readonlyOn y expone required, readonly y disable en el slot; debes cablearlos en tu control y, si usas requiredOn, añadir :rules en el template para que q-form valide al enviar.
<script setup lang="ts">
import FormBuilder, {
useFormBuilder,
} from "@benjaminor-dev/quasar-app-extension-form-builder";
import type { SlotFieldBind } from "@benjaminor-dev/quasar-app-extension-form-builder/types";
type DemoForm = {
customText: string | null;
};
const { defineForm } = useFormBuilder();
const config = defineForm<DemoForm>({
formName: "demoForm",
fields: [
{
type: "SlotField",
model: "customText",
props: {
slotName: "custom-text",
},
},
],
});
</script>
<template>
<FormBuilder :config="config">
<template #slot-field-custom-text="attrs: SlotFieldBind">
<q-input
v-model="attrs.model.value"
:error="!!attrs.error.value"
:error-message="attrs.error.value || ''"
:label="attrs.label"
:hint="attrs.hint"
:readonly="attrs.readonly"
:disable="attrs.disable"
:rules="attrs.required ? [(v) => !!v || 'Campo requerido'] : undefined"
>
<template v-if="attrs.required" #append>
<span class="text-negative">*</span>
</template>
</q-input>
</template>
</FormBuilder>
</template>
Ejemplo con lista embebida (patrón recomendado — campo nativo FormList):
// dentro de fields: [ ... ]
{
type: "FormList",
model: "teamMembers",
label: "Integrantes",
requiredOn: () => true,
props: {
listName: "members",
itemLabel: "Miembro",
workflow: "free",
itemForm: {
fields: [
{
type: "InputText",
model: "fullName",
label: "Nombre",
requiredOn: () => true,
},
],
},
columns: [
{ name: "fullName", label: "Nombre", field: "fullName" },
],
},
},
El scaffold bor:make-formlist genera un FormBuilder con campo FormList embebido (patrón canónico).
Extensiones opcionales relacionadas
| Extensión | Propósito | Relación con Form Builder |
|---|---|---|
| @benjaminor-dev/datatable | Requerida. Tabla homologada (DataTable) usada por FormList, InputFileMultiple y Table Builder |
Peer npm — instalar con quasar ext add @benjaminor-dev/datatable |
| @benjaminor-dev/table-builder | Tablas declarativas con filtros Form Builder, paginación servidor/cliente y cards en móvil | Peer requerido de Table Builder; usa Form Builder y DataTable |
| @benjaminor-dev/dialog-file-preview | Diálogo modal para previsualizar File, Blob o URL (imagen con zoom, PDF con scroll continuo en documentos ligeros o paginado en archivos pesados, video, audio, texto) vía useDialogFilePreview(); loader mientras prepara y carga el visor |
InputFile e InputFileMultiple la detectan automáticamente (showPreview default true): append o filas de la tabla inline con ver/descargar + limpiar; también usable manualmente con useDialogFilePreview(); no es dependencia de este paquete |
Instalación en el host (extensiones adicionales):
quasar ext add @benjaminor-dev/datatable
quasar ext add @benjaminor-dev/dialog-file-preview
Orden de boots (stack completo):
1. Pinia (host)
2. DataTable — CSS
3. Form Builder — boot/store
4. bor/bor-form-builder-defaults (host)
5. Table Builder — boot/store
6. bor/bor-table-builder-defaults (host)
7. Dialog File Preview — boot/dialog
8. bor/bor-dialog-file-preview-defaults (host)
9. Dialog Loader — boot/dialog
10. bor/bor-dialog-loader-defaults (host)
11. Dialog Message — boot/dialog
12. bor/bor-dialog-message-defaults (host)
Regla: registra el defaults boot antes del primer uso (defineForm, defineTable, show()). En quasar.config verás entradas como bor/bor-form-builder-defaults; los boots del paquete los inyecta la extensión al compilar — no aparecen como entradas cortas en tu config.
Customizar plantilla scaffold
Puedes reemplazar la plantilla que genera bor:make-form creando en tu app host:
scripts/bor/stubs/bor-make-form.stub
Para formularios con lista (bor:make-formlist):
scripts/bor/stubs/bor-make-formlist.stub
Para field presets (bor:make-field):
scripts/bor/stubs/bor-make-field.stub
Ese archivo tiene prioridad sobre la plantilla incluida en la extensión.
Entrypoints públicos
| Entrypoint | Propósito |
|---|---|
| @benjaminor-dev/quasar-app-extension-form-builder | FormBuilder, useFormBuilder, selectFormStore, defineFieldPreset, configureFormBuilderDefaults, configureFormBuilder (alias), boots e inputs reexportados |
| @benjaminor-dev/quasar-app-extension-datatable | DataTable, TablePaginationBar, composables y helpers de paginación (peer requerido de Form Builder) |
| @benjaminor-dev/quasar-app-extension-form-builder/boot/store | Boot de integración Pinia para formularios |
| @benjaminor-dev/quasar-app-extension-form-builder/boot/directives | Boot de registro de directivas |
| @benjaminor-dev/quasar-app-extension-form-builder/inputs | Componentes de input publicados |
@benjaminor-dev/quasar-app-extension-form-builder/scaffold |
runBorHelp, runBorMakeForm, runBorMakeFormlist, runBorMakeField (bor:help, bor:make-form, bor:make-formlist, bor:make-field) |
@benjaminor-dev/quasar-app-extension-form-builder/types |
Tipos de config, campos, boot (DefineFormInput, DefineFieldPresetFn, DefineFormFieldsContext, FormBuilderDefaults, BootFieldPropsFor, FieldUiConfigFor, FormListConfig, FormAwareValue, …) — autocompletado en IDE |
| @benjaminor-dev/quasar-app-extension-form-builder/main.css | Estilos de la librería |
No hay entrypoint /form-list ni paquete npm aparte para listas: FormList es un tipo de campo nativo de este paquete.
Troubleshooting
El callback condicional marca error de tipos
Si declaras requiredOn: (form: MyForm) => ..., TypeScript puede fallar por contravarianza. La firma real acepta form opcional.
Usa:
requiredOn: (form) =>
form?.phone === "123";
o:
requiredOn: (
form?: FormBuilderSubmitPayload<MyForm>,
) => form?.phone === "123";
Duplicar validación required en rules
requiredOn ya inyecta la obligatoriedad internamente. No añadas reglas manuales de «campo requerido» en rules; resérvalas para validaciones adicionales (email, rfc, tamaño, etc.).
El defaults boot no aparece en quasar.config
En condiciones normales el install lo registra solo. Si falta (proyecto antiguo o edición manual):
npx quasar ext invoke @benjaminor-dev/form-builder
El comando npm no aparece en host
Ejecuta en el host:
npx quasar ext invoke @benjaminor-dev/form-builder
Pinia no inicializada
Asegura que el boot de Pinia corra antes del boot store de la extensión.
Falla el check de @benjaminor-dev/quasar-app-extension-datatable
Form Builder exige el peer DataTable. Instálalo antes que esta extensión:
quasar ext add @benjaminor-dev/datatable
Sin él, FormList / InputFileMultiple no resuelven el componente y faltan estilos de tabla.
InputDate/InputDateRange/InputTime no respetan límites
Verifica formato de FormAwareValue:
minDate/maxDate:"YYYY-MM-DD",() => "YYYY-MM-DD"o(form) => "YYYY-MM-DD"maxRange: número,() => numbero(form) => number(días)minTime/maxTime:"HH:mm:ss",() => "HH:mm:ss"o(form) => "HH:mm:ss"
gap="false" no desactiva el gutter de Quasar
Para quitar q-gutter-* usa :gap="false" (boolean con v-bind). Si escribes gap="false" sin :, Vue envía el string "false"; desde 0.9.7 el runtime lo trata como desactivado, pero en plantillas nuevas prefiere siempre :gap="false".
validationMode eager molesta en campos con máscara
validationMode: 'eager' hace que Quasar evalúe reglas en cada pulsación. En campos con formato visual (fechas, teléfonos, moneda) suele ser mejor el default lazy, que valida al salir del campo o al enviar el formulario.
InputFile / InputFileMultiple no muestran archivos precargados
InputFile solo acepta File | null; InputFileMultiple solo acepta File[] (vacío = []). Si pasas un Blob suelto, una URL, base64 o un objeto { url, name }, el valor se normaliza a vacío. Crea File en el cliente:
const blob = await fetch(url).then(
(r) => r.blob(),
);
form.signedContractPdf = new File(
[blob],
"documento.pdf",
{
type:
blob.type || "application/pdf",
},
);
En formularios de alta pura, deja el campo en null hasta que el usuario seleccione el archivo.
No aparece el botón ver/descargar en InputFile / InputFileMultiple
- Verifica que no hayas puesto
showPreview: falseenpropsdel campo (truepor defecto). - Instala
@benjaminor-dev/dialog-file-previewy verifica boots: Pinia → Form Builder → Dialog File Preview. - En
InputFile, los botones del append solo aparecen cuando hay unFileválido en el modelo y la extensión de preview está disponible. EnInputFileMultiple, el preview está en cada fila de la tabla inline. - Con
disablese oculta el preview; enreadonlyla vista previa o descarga sí funciona (eliminar/limpiar no). - Tras
npm run builden el host o al enlazar la extensión confile:, reiniciaquasar devpara cargar eldistactualizado de Form Builder.
Select/radio/checkbox group no reconoce el valor precargado
Los campos con opciones comparan modelValue con options[].value de forma estricta (===). Si el API devuelve cityId: 1 pero las opciones usan value: "1", no coincidirán. Alinea tipos en defineForm, en defaultValues y en el value de cada opción. Con valores numéricos, 0 es selección válida para required en selección única.
El archivo tarda en aparecer tras elegirlo en el diálogo del sistema
Es normal con archivos pesados: InputFile e InputFileMultiple muestran un indicador de carga mientras el navegador expone el File al modelo. El diálogo de @benjaminor-dev/dialog-file-preview también muestra loader al abrir y mientras el visor carga el contenido (PDF, imagen grande, etc.).
defaultValues como función no precarga el formulario
- La función (sync o async) se ejecuta en
onMounteddeFormBuilder, no al llamardefineFormen el padre. - Pasa el
configretornado pordefineForma<FormBuilder :config="config" />; no reemplacesdefaultValuespor un objeto plano antes de montar. - Tras la precarga verás un instante con vacíos por tipo; es el comportamiento esperado hasta que termine la promesa.
- El reset restaura los valores ya precargados (actualizan
formsDefaultsal hidratar).
La lista no aparece en el submit del formulario padre
- Declara el arreglo en el tipo del formulario (
teamMembers: Member[]) y usatype: "FormList"conmodel: "teamMembers"enfields[]. - En
@submit, el handler recibeFormBuilderSubmitPayload<T>— es el propio formulario (payload.teamMembers, nopayload.form). Igual que el resto de campos. - Para un borrador inicial, usa
npm run bor:make-formlist -- <nombre>: genera unFormBuildercon un campoFormListya cableado (misma ruta quebor:make-form). - Antes de
@submit,FormBuildervalida cadaFormListvisible; si hay filas inválidas, el evento no se emite. - Con
workflow: "review", las filas inválidas también se resaltan en la tabla; el diálogo integrado lista los registros a corregir con ellabeldel field en cabecera. - Para alertas propias en lugar del diálogo integrado, define
onAfterValidateErrorenpropsdel field (ver catálogoFormList).
El diálogo de filas inválidas solo aparece una vez
- Versiones anteriores bloqueaban el segundo intento de submit vía reglas del
QField; el runtime actual muestra el error bajo el campo sin impedir reintentos: cada pulso de Enviar vuelve a validar y puede reabrir el diálogo. - Si definiste
onAfterValidateError, el diálogo integrado no se abre: implementa ahí la lógica de reintento/alerta.
MIT © Benjamín Olvera R.