Skip to content

useForm

  • Con este composable recibes las variables necesarias para construir un formulario.
// Importa un listado de países con sus prefijos telefónicos
import { COUNTRIES_AND_PHONE } from '@/constants/countriesPhone'
import type { ShallowRef } from 'vue'
// Extrae todos los prefijos telefónicos en un array
const codes = COUNTRIES_AND_PHONE.map((c) => c.codePhone)
/**
* Hook personalizado para manejar formularios reactivos con validación y soporte de internacionalización.
*
* @param defaults Valores por defecto del formulario
* @param locales Referencia reactiva a los textos localizados para los mensajes de validación
* @returns Un objeto con el estado del formulario, validez, mensajes de error, etc.
*/
export function useForm<
T extends Record<string, unknown>,
L extends Record<string, unknown> = {},
>(defaults: Partial<T>, locales: ShallowRef<L | undefined>) {
// Estado reactivo del formulario, inicializado con los valores por defecto
const formData = shallowReactive<T>({
...(defaults ?? {}),
} as T)
// Flag para evitar reacciones durante asignaciones programadas
let shouldWatch = true
/**
* Rellena el formulario con nuevos datos, y previene efectos secundarios temporales.
* @param data Datos parciales para rellenar el formulario
*/
function fillFormData(data: Partial<T>) {
shouldWatch = false // Desactiva temporalmente los watchers
Object.assign(formData, data) // Asigna los nuevos valores
// Asigna manualmente el campo "code" si existe
if ('code' in data && 'code' in formData) {
;(formData as Record<string, unknown>).code = data.code
}
// Reactiva los watchers en el siguiente tick del DOM
nextTick(() => {
shouldWatch = true
})
}
// Computed que indica si hay campos inválidos en el formulario
const invalidFields = computed(() => {
const checks = validator.validateFields(formData)
// Devuelve true si al menos un campo válido es falso
return Array.from(checks.entries()).some(([key, result]) => {
return key in formData && result.valid !== true
})
})
// Mapa reactivo para almacenar los mensajes de validación por campo
const validationMessages = ref<Map<string, string>>(new Map())
// Watcher que valida el formulario cada vez que cambia cualquier campo
watch(
formData,
(newFields) => {
validationMessages.value.clear() // Limpia los mensajes anteriores
// Ejecuta la validación con posibles textos localizados
const result = validator.validateFields(newFields, locales)
// Asigna el mensaje de error correspondiente por cada campo
for (const [key] of Object.entries(newFields)) {
const message = result.get(key)?.message || ''
validationMessages.value.set(key, message)
}
},
{ deep: true }, // Reacciona a cambios profundos en objetos anidados
)
// Si el formulario contiene los campos `country` y `code`, sincroniza el código telefónico
if ('country' in formData && 'code' in formData) {
watch(
() => formData.country,
(newCountry: unknown | unknown[]) => {
if (!shouldWatch) return // Ignora si está temporalmente deshabilitado
// Toma el país seleccionado (soporta arrays)
const countryCode = Array.isArray(newCountry) ? newCountry[0] : newCountry
// Busca el código telefónico correspondiente al país
const selected = COUNTRIES_AND_PHONE.find((c) => c.code === countryCode)
// Asigna el código telefónico correspondiente, o vacío si no se encuentra
;(formData as Record<string, unknown>).code = selected?.codePhone ?? ''
},
{ flush: 'sync' }, // Ejecuta inmediatamente después del cambio (antes del render)
)
}
// Devuelve las referencias útiles del formulario
return {
formData, // Estado reactivo del formulario
fillFormData, // Función para rellenar el formulario
codes: 'code' in formData ? codes : undefined, // Lista de códigos solo si se usa el campo "code"
invalidFields, // Computed que indica si hay errores
validationMessages, // Mensajes de validación por campo
}
}
<script setup lang="ts">
//logica de login
const { data } = await useLocales<LoginFormLocales>("login");
const loginFormLocales = ref(data);
const {
formData: login, // para usarlo en los v-model
invalidFields, // devuelve un booleano con el estado de validacion.
validationMessages, // objeto con funcion get para tener el feedback de la validación.
} = useForm<LoginData, LoginFormLocales>(
{
email: "",
password_not_empty: "",
youre_a_bot: "",
},
loginFormLocales
);
// paso el objeto con los nombres de los v-model de los inputs
// y como segundo argumento el locales de la pagina
</script>
<template>
<section class="form-container">
<form class="login-form" @submit.prevent="loginProcess">
<span class="login-form__grid-item login-form__title">{{
loginFormLocales?.login_title
}}</span>
<UiFormInputField
class="login-form__grid-item"
auto-complete="on"
:feedback="validationMessages.get('email')"
input-id="email"
:label="loginFormLocales?.email_text ?? ''"
type="email"
v-model.trim="login.email"
></UiFormInputField>
<UiFormInputField
class="login-form__grid-item"
auto-complete="off"
:feedback="validationMessages.get('password_not_empty')"
input-id="password"
:label="loginFormLocales?.password_text ?? ''"
type="password"
v-model.trim="login.password_not_empty"
></UiFormInputField>
<UiFormInputField
class="login-form__grid-item--hidden"
input-id="secondaryEmail"
:label="loginFormLocales?.youre_a_bot_text ?? ''"
v-model.trim="login.youre_a_bot"
></UiFormInputField>
<p class="login-form__grid-item">
{{ loginFormLocales?.forgot_password }}
<NuxtLinkLocale :to="{ name: 'olvido-contrasena' }" class="redirection">
{{ loginFormLocales?.forgot_password_click }}
</NuxtLinkLocale>
</p>
<p v-if="invalidFields" class="login-form__grid-item login-form__message">
{{ loginFormLocales?.general_error }}
</p>
<UiButtonsMainButton
class="login-form__grid-item--button"
:disabled="invalidFields"
type="'submit'"
accesskey="s"
>
{{ loginFormLocales?.button_text }}
</UiButtonsMainButton>
<p class="login-form__grid-item">
{{ loginFormLocales?.to_register_text }}
<NuxtLinkLocale :to="{ name: 'registro' }" class="redirection">
{{ loginFormLocales?.to_register_click }}
</NuxtLinkLocale>
</p>
</form>
</section>
</template>