Formularios con @QField y @QGroup
Genera schemas de formulario dinámicamente a partir de los metadatos de tu modelo con @QField y @QGroup.
Caso de Uso
Tienes un modelo de usuario y quieres generar un formulario dinámico en React, Angular o Vue sin escribir el schema a mano.
Schema Básico con @QField
typescript
import { QModel, Quick, QField } from 'quickmodel';
interface IContact {
firstName: string;
lastName: string;
email: string;
phone?: string;
bio?: string;
role: string;
birthDate: string;
newsletter: boolean;
}
@Quick({ birthDate: Date })
class ContactModel extends QModel<IContact> {
@QField({
widget: 'input',
inputType: 'text',
label: 'Nombre',
required: true,
})
declare firstName: string;
@QField({
widget: 'input',
inputType: 'text',
label: 'Apellido',
required: true,
})
declare lastName: string;
@QField({
widget: 'input',
inputType: 'email',
label: 'Email',
required: true,
placeholder: 'tu@email.com',
})
declare email: string;
@QField({
widget: 'input',
inputType: 'tel',
label: 'Teléfono',
placeholder: '+34 600 000 000',
})
declare phone?: string;
@QField({
widget: 'textarea',
label: 'Biografía',
placeholder: 'Cuéntanos sobre ti...',
})
declare bio?: string;
@QField({
widget: 'select',
label: 'Rol',
required: true,
options: [
{ value: 'admin', label: 'Administrador' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Lector' },
],
})
declare role: string;
@QField({ widget: 'datepicker', label: 'Fecha de nacimiento' })
declare birthDate: Date;
@QField({ widget: 'switch', label: '¿Suscribirse a la newsletter?' })
declare newsletter: boolean;
}
// Obtener el schema completo
const schema = ContactModel.getFormSchema();
console.log(schema);
// [
// { field: 'firstName', widget: 'input', inputType: 'text', label: 'Nombre', required: true },
// { field: 'lastName', widget: 'input', inputType: 'text', label: 'Apellido', required: true },
// { field: 'email', widget: 'input', inputType: 'email', label: 'Email', ... },
// { field: 'phone', widget: 'input', inputType: 'tel', label: 'Teléfono', ... },
// { field: 'bio', widget: 'textarea', label: 'Biografía', ... },
// { field: 'role', widget: 'select', label: 'Rol', options: [...] },
// { field: 'birthDate', widget: 'datepicker', label: 'Fecha de nacimiento' },
// { field: 'newsletter',widget: 'switch', label: '¿Suscribirse a la newsletter?' },
// ]Agrupación por Secciones con @QGroup
typescript
import { QModel, Quick, QField, QGroup } from 'quickmodel';
interface IUserProfile {
firstName: string;
lastName: string;
email: string;
street: string;
city: string;
country: string;
bio?: string;
}
@Quick()
class UserProfileModel extends QModel<IUserProfile> {
@QField({ widget: 'input', label: 'Nombre', required: true })
@QGroup('Datos personales')
declare firstName: string;
@QField({ widget: 'input', label: 'Apellido', required: true })
@QGroup('Datos personales')
declare lastName: string;
@QField({
widget: 'input',
inputType: 'email',
label: 'Email',
required: true,
})
@QGroup('Datos personales')
declare email: string;
@QField({ widget: 'input', label: 'Calle', required: true })
@QGroup('Dirección')
declare street: string;
@QField({ widget: 'input', label: 'Ciudad', required: true })
@QGroup('Dirección')
declare city: string;
@QField({
widget: 'select',
label: 'País',
required: true,
options: ['España', 'México', 'Argentina', 'Colombia'],
})
@QGroup('Dirección')
declare country: string;
@QField({
widget: 'textarea',
label: 'Sobre mí',
placeholder: 'Cuéntanos sobre ti...',
})
// Sin @QGroup → aparece en grupo undefined
declare bio?: string;
}
// Schema agrupado
const grouped = UserProfileModel.getFormSchemaGrouped();
console.log(grouped);
// [
// {
// group: 'Datos personales',
// fields: [
// { field: 'firstName', widget: 'input', label: 'Nombre', required: true },
// { field: 'lastName', widget: 'input', label: 'Apellido', required: true },
// { field: 'email', widget: 'input', inputType: 'email', label: 'Email', required: true },
// ]
// },
// {
// group: 'Dirección',
// fields: [
// { field: 'street', widget: 'input', label: 'Calle', required: true },
// { field: 'city', widget: 'input', label: 'Ciudad', required: true },
// { field: 'country', widget: 'select', label: 'País', options: [...] },
// ]
// },
// {
// group: undefined,
// fields: [
// { field: 'bio', widget: 'textarea', label: 'Sobre mí' },
// ]
// }
// ]Integración con React (ejemplo práctico)
typescript
// DynamicForm.tsx
import React from 'react';
import type { IQFormSchemaEntry } from 'quickmodel';
interface IDynamicFormProps {
schema: IQFormSchemaEntry[];
values: Record<string, unknown>;
onChange: (field: string, value: unknown) => void;
}
function DynamicField({ entry, value, onChange }: {
entry: IQFormSchemaEntry;
value: unknown;
onChange: (val: unknown) => void;
}) {
const { widget, label, placeholder, inputType, options, required } = entry;
switch (widget) {
case 'input':
return (
<div>
<label>{label}{required && ' *'}</label>
<input
type={inputType ?? 'text'}
placeholder={placeholder}
value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
case 'textarea':
return (
<div>
<label>{label}</label>
<textarea
placeholder={placeholder}
value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
case 'select':
return (
<div>
<label>{label}{required && ' *'}</label>
<select value={String(value ?? '')} onChange={(e) => onChange(e.target.value)}>
{(options as string[]).map((opt) =>
typeof opt === 'string'
? <option key={opt} value={opt}>{opt}</option>
: <option key={opt.value as string} value={opt.value as string}>{opt.label}</option>
)}
</select>
</div>
);
case 'switch':
return (
<div>
<label>
<input type="checkbox" checked={Boolean(value)} onChange={(e) => onChange(e.target.checked)} />
{' '}{label}
</label>
</div>
);
default:
return null;
}
}
export function DynamicForm({ schema, values, onChange }: IDynamicFormProps) {
return (
<form>
{schema.map((entry) => (
<DynamicField
key={entry.field}
entry={entry}
value={values[entry.field]}
onChange={(val) => onChange(entry.field, val)}
/>
))}
</form>
);
}
// Uso:
// const schema = ContactModel.getFormSchema();
// <DynamicForm schema={schema} values={formState} onChange={handleChange} />Integración con Formularios Agrupados (Angular-style)
typescript
// Para Angular u otros frameworks que usan grupos de formulario:
const grouped = UserProfileModel.getFormSchemaGrouped();
// Renderizar como tabs o secciones colapsables
grouped.forEach(({ group, fields }) => {
const sectionTitle = group ?? 'Otros campos';
console.log(`\n== ${sectionTitle} ==`);
fields.forEach((field) => {
console.log(` - ${field.field}: ${field.widget} (${field.label})`);
});
});
// == Datos personales ==
// - firstName: input (Nombre)
// - lastName: input (Apellido)
// - email: input (Email)
// == Dirección ==
// - street: input (Calle)
// - city: input (Ciudad)
// - country: select (País)
// == Otros campos ==
// - bio: textarea (Sobre mí)Metadatos Personalizados
@QField acepta cualquier propiedad extra que tu framework necesite:
typescript
interface IProductForm {
name: string;
price: number;
imageUrl: string;
}
@Quick()
class ProductForm extends QModel<IProductForm> {
@QField({
widget: 'input',
label: 'Nombre del producto',
required: true,
// Metadatos personalizados para tu sistema de validación:
minLength: 3,
maxLength: 100,
pattern: '^[a-zA-Z0-9 ]+$',
})
declare name: string;
@QField({
widget: 'number',
label: 'Precio (€)',
required: true,
// Para formularios React Hook Form:
min: 0,
step: 0.01,
currency: 'EUR',
})
declare price: number;
@QField({
widget: 'input',
inputType: 'url',
label: 'URL de imagen',
// Para upload personalizado:
accept: 'image/*',
maxSizeMb: 5,
})
declare imageUrl: string;
}
const schema = ProductForm.getFormSchema();
// Cada entrada contiene todos los metadatos que definiste
console.log(schema[1].min); // 0
console.log(schema[1].step); // 0.01
console.log(schema[1].currency); // 'EUR'Mejores Prácticas
Separa el modelo de dominio del modelo de formulario
typescript
// ✅ Bien: modelo de dominio separado del formulario
@Quick({ createdAt: Date })
class User extends QModel<IUser> {
declare id: string;
declare name: string;
declare createdAt: Date;
}
// Modelo de formulario solo para el form de creación:
@Quick()
class CreateUserForm extends QModel<Omit<IUser, 'id' | 'createdAt'>> {
@QField({ widget: 'input', label: 'Nombre', required: true })
declare name: string;
}Centraliza las opciones reutilizables
typescript
const COUNTRY_OPTIONS = [
{ value: 'ES', label: 'España' },
{ value: 'MX', label: 'México' },
{ value: 'AR', label: 'Argentina' },
];
// Reutiliza en múltiples formularios
@QField({ widget: 'select', label: 'País', options: COUNTRY_OPTIONS })
declare country: string;Próximos Pasos
- Validación - Añade reglas de negocio con
@QRule - Alias y Mapeo - Mapea campos snake_case con
@QAlias