Skip to content

useForm (Vue)

This content is not available in your language yet.

useForm() es el punto de entrada en Vue. Crea un FormController reactivo y expone refs computados con todo el state.

function useForm(
options: MaybeRefOrGetter<FormControllerOptions>
): UseFormReturn;
interface FormControllerOptions {
schema: {
modelSchema: Record<string, FieldDefinition>;
locale?: 'es' | 'en' | 'pt';
};
initialValues?: Record<string, any>;
validateOn?: 'change' | 'blur' | 'submit';
}

El argumento puede ser un objeto literal, un ref o un getter. Si pasas un getter, el form se reconstruye cuando cambien las dependencias reactivas.

interface UseFormReturn {
// Refs computados reactivos
values: ComputedRef<Record<string, any>>;
errors: ComputedRef<Record<string, string[]>>;
touched: ComputedRef<Record<string, boolean>>;
isValid: ComputedRef<boolean>;
isDirty: ComputedRef<boolean>;
isSubmitting: ComputedRef<boolean>;
fields: ComputedRef<FieldDescriptor[]>;
// Acciones (sin .value, son métodos)
setValue(name: string, value: any): void;
setValues(partial: Record<string, any>): void;
touch(name: string): void;
touchAll(): void;
setSchema(instance: FormSchemaLike): void;
validate(): boolean;
submit(): Promise<SubmitResult>; // { values, isValid, errors }
reset(values?: Record<string, any>): void;
// Bajo nivel
controller: Ref<FormController>;
}
<script setup>
import { useForm, CoFormRenderer } from '@prolibu-suite/cobalt-form-vue';
const form = useForm(() => ({
schema: {
modelSchema: {
email: { type: 'String', required: true, format: 'email' },
password: { type: 'String', required: true, minLength: 8 },
},
locale: 'es',
},
}));
// El submit se maneja con el evento del renderer (recibe solo valores válidos)
async function onSubmit({ values }) {
await api.login(values);
}
</script>
<template>
<CoFormRenderer :form="form" @submit="onSubmit" />
</template>

Con eso ya tienes:

  • Render automático de campos según el schema
  • Validación en vivo con AJV
  • Mensajes traducidos a español
  • Submit habilitado solo si el form es válido
<script setup>
const form = useForm(() => ({ /* ... */ }));
// Leer
console.log(form.values.value.email); // unwrap con .value
console.log(form.errors.value.email);
console.log(form.isValid.value);
// Watchear cambios
import { watch } from 'vue';
watch(form.values, (next) => {
console.log('values changed:', next);
}, { deep: true });
watch(form.isDirty, (dirty) => {
if (dirty) console.log('hay cambios sin guardar');
});
</script>
<template>
<p v-if="!form.isValid.value">El formulario tiene errores</p>
<p v-if="form.isDirty.value">Hay cambios sin guardar</p>
<pre>{{ form.values.value }}</pre>
</template>
// Setear un valor
form.setValue('email', 'foo@bar.com');
// Setear varios (merge, no replace)
form.setValues({ email: 'foo@bar.com', age: 30 });
// Marcar como touched (muestra errores)
form.touch('email');
form.touchAll();
// Forzar validación
const isOk = form.validate();
// Submit: valida y retorna { isValid, values, errors } (no ejecuta callbacks)
const result = await form.submit();
if (result.isValid) { /* usar result.values */ }
// Reset a initialValues + defaults del schema
form.reset();
// Reset a valores específicos
form.reset({ email: 'nuevo@ejemplo.com' });

Si el schema depende de props o state (ej. cambia según el rol del usuario), pasa un getter:

<script setup>
const props = defineProps({ role: String });
const form = useForm(() => ({
schema: {
modelSchema: props.role === 'admin'
? adminSchema
: userSchema,
locale: 'es',
},
}));
</script>

Cuando props.role cambie, useForm reconstruye el controller con el nuevo schema. Los valores existentes se preservan en la medida que sean compatibles con el nuevo schema.

const form = useForm(() => ({
schema: { modelSchema: userSchema, locale: 'es' },
initialValues: {
email: 'usuario@empresa.com',
role: 'admin',
newsletter: true,
},
}));

initialValues se mergea con los default del schema. Los valores explícitos siempre ganan sobre los defaults.

isDirty se calcula contra esta combinación (initialValues + defaults). Si haces setValue y luego vuelves al valor inicial, isDirty regresa a false.

form.submit() valida y retorna { isValid, values, errors } — no ejecuta ningún callback. Encadená tu lógica leyendo el resultado:

<script setup>
const form = useForm(() => ({
schema: { modelSchema: userSchema, locale: 'es' },
}));
async function onSubmit() {
const { isValid, values } = await form.submit();
if (!isValid) return; // los errores quedan en form.errors.value
await api.createUser(values);
toast.success('Usuario creado');
}
</script>
<template>
<form @submit.prevent="onSubmit">
<CoFormRenderer :form="form" />
<co-button
label="Crear"
variant="primary"
type="submit"
:disabled="!form.isValid.value || form.isSubmitting.value"
/>
</form>
</template>

Si usás <CoFormRenderer> sin envolverlo en tu propio <form>, escuchá su evento @submit (solo dispara con valores válidos) — el renderer llama touchAll() y hace scroll al primer error ante un submit inválido.

  1. Setea isSubmitting = true.
  2. validate() — re-valida con AJV (+ reglas requiredIf).
  3. Retorna { values, isValid, errors } (snapshot del estado).
  4. En el finally: isSubmitting = false.

submit() no marca campos como touched ni ejecuta callbacks. <CoFormRenderer> es quien, ante un submit inválido, llama touchAll() y emite @invalid para revelar todos los errores.

Si necesitas un listener (ej. para logging, telemetría), usa form.controller.value.subscribe:

const unsubscribe = form.controller.value.subscribe((state) => {
console.log('form state:', state);
});
onUnmounted(unsubscribe);