En este capítulo trabajaremos con un concepto tan importante como los formularios.
Los formularios desempeñan un papel importante en el mundo de la programación web, y en particular en el desarrollo de aplicaciones basadas en Qwik y tecnologías similares como Angular, React, Vue, Astro,etc.
Estas herramientas interactivas permiten a los usuarios ingresar y enviar información de manera eficiente, lo que a su vez habilita una amplia gama de funcionalidades en línea.
En este capítulo, vamos a trabajar con los formularios mediante estos apartados:
Form
ni routeAction$
.Form
y routeAction$
.zod$
.BookmarksDB
.En el contexto de desarrollo web, un formulario es un componente interactivo que recopila datos y permite a los usuarios enviar información al servidor.
Estos formularios suelen estar compuestos por varios elementos, como campos de texto, casillas de verificación, botones y selecciones, que los usuarios pueden completar o seleccionar según sus necesidades. Una vez que se completa el formulario, los datos se envían al servidor para su procesamiento.
Los formularios desempeñan un papel fundamental en la interacción entre los usuarios y las aplicaciones web.
Sus usos son variados y van desde una simple recopilación de información de contacto hasta la presentación de solicitudes, la creación de cuentas de usuario, la búsqueda de productos, la publicación de comentarios y mucho más.
Algunos ejemplos comunes de aplicaciones de formularios que podemos encontrarnos:
Registro de Usuarios
: Los formularios se utilizan para que los usuarios creen cuentas y proporcionen información personal como nombre, dirección de correo electrónico y contraseña.
Búsqueda y Filtrado
: Las barras de búsqueda y los filtros en línea utilizan formularios para recopilar los criterios de búsqueda y, posteriormente, mostrar los resultados relevantes acorde esa información introducida.
Comentarios y Valoraciones
: Los usuarios pueden expresar sus opiniones y calificaciones a través de formularios de comentarios en blogs, sitios de reseñas y redes sociales.
Procesos de Compra en Línea
: Los carritos de compras y los formularios de pago permiten a los usuarios seleccionar productos y proporcionar información de facturación y envío.
Encuestas y Formularios de Retroalimentación
: Las empresas utilizan formularios para obtener comentarios de los usuarios, lo que les permite mejorar sus productos y servicios.
Impacto de los formularios
Los formularios juegan un papel crítico en la experiencia del usuario y el éxito de una aplicación web.
Un formulario bien diseñado puede mejorar la usabilidad y la satisfacción del usuario, mientras que un formulario confuso o complicado puede ser un obstáculo para los usuarios, llevándolos a abandonar la aplicación.
Por lo tanto, es fundamental darle mimo a los aspectos de la arquitectura y el diseño de los formularios en cualquier proyecto web.
Los impactos clave de los formularios incluyen:
Usabilidad: Formularios fáciles de usar y bien estructurados mejoran la experiencia del usuario y reducen la incomodidad que esto pueda ocasionar en su uso.
Recopilación de Datos: Los formularios permiten a las empresas y organizaciones recopilar información valiosa de los usuarios para su análisis y toma de decisiones.
Seguridad y Validación: Los formularios también desempeñan un papel importante en la seguridad, ya que se pueden utilizar para validar y filtrar los datos antes de procesarlos.
Llegados a este punto, se puede concluir que los formularios en el desarrollo web en general son elementos cruciales que van a permitir a los usuarios interactuar con aplicaciones y sitios web.
Su diseño y funcionamiento adecuados son esenciales para una experiencia de usuario positiva y para el éxito de las aplicaciones en línea.
En los siguientes apartados de este capítulo, vamos a profundizar en cómo crear y gestionar formularios en aplicaciones de Qwik de diversas formas.
Form
ni routeAction$
Después de los primeros apartados teóricos, comenzamos con la parte más práctica, donde realizaremos la implementación de varios formularios, desde el básico que vamos a implementar sin las herramientas que nos proporciona Qwik.
Iniciamos un nuevo proyecto llamado 13-01-forms-basic.
El proceso para realizarlo ya lo conocéis de sobra
Abrimos el contenido del siguiente enlace para tener a mano la estructura inicial de nuestro formulario:
Formulario Newsletter básico
Abrimos el proyecto en nuestro editor / IDE de código favorito y nos dirigimos a src/routes/index.tsx
y añadimos el código del Gist que tenemos que tener a mano abierto.
En este caso, se añade la función useStyles$
para cargar los estilos de index.css
. Como seguramente no lo tendréis creado, lo creamos en src/routes/index.css
.
Una vez copiado y guardados los cambios, debería de mostrarnos lo siguiente sin aplicar los estilos (ya que no están añadidos en el ficheros index.css
):
Ahora vamos a darle apariencia añadiendo los estilos en el nuevo fichero index.css
que acabamos de crear usando el siguiente código que lo cogemos desde el siguiente Gist.
Para centrarnos en lo que importa en este capítulo se ha usado un template ya creado que lo he obtenido desde el siguiente enlace:
https://shorten-up.vercel.app/xwSzxa6ynT
Aquí tendréis una colección de plantillas que os pueden ser super útiles para partir desde una base sin tener que crearlo todo desde 0.
Una vez guardado, se muestra de la siguiente forma:
Con esto ya tenemos la apariencia básica del formulario, empezaremos añadiendo el evento onInput$
que se ejecuta cuando la introducción de información.
Añadimos dentro del elemento input
lo siguiente:
onInput$={
(event) => {
console.log((event.target as HTMLInputElement).value);
}
}
Quedando de la siguiente forma:
<input
type="email"
name="email"
id="email"
placeholder="Introduzca su correo electrónico"
onInput$={(event) => {
console.log((event.target as HTMLInputElement).value);
}}
class="formbold-form-input"
required
/>
Y si empezamos a añadir información dentro del campo, en la consola se irá mostrando de la siguiente forma:
Como se puede apreciar, ya está recogiendo esa información pero solo la va registrando sin almacenarla. Para ello usamos cualquier función para gestionar el estado como pueden ser useSignal
o useStore
.
Añadimos el siguiente contenedor con elemento useStore
donde la llamamos formContainer
y hacemos que lo que se vaya registrando con onInput$
se vaya quedando almacenando en este valor formContainer
.
Hacemos los siguientes cambios:
// 1
import { component$, useStore, useStyles$ } from '@builder.io/qwik';
...
export default component$(() => {
// 2
const formContainer = useStore({
email: ''
});
...
// 3
return (
<div class="formbold-main-wrapper">
<div class="formbold-form-wrapper">
<form>
<div class="formbold-email-subscription-form">
<input
type="email"
name="email"
id="email"
placeholder="Introduzca su correo electrónico"
onInput$={(event) =>
(formContainer.email = (
event.target as HTMLInputElement
).value)
}
class="formbold-form-input"
/>
...
</div>
</form>
Email registrado: {formContainer.email}
</div>
</div>
);
});
export const head: DocumentHead = {
...
};
Analizando los apartados indicados con más detalle:
1
: Importamos useStore
dentro del paquete @builder.io/qwik
para poder iniciar el contenedor de información del formulario.2
: Iniciamos el valor con el estado del formulario asignando el valor ""
a la propiedad email
.3
: Vamos almacenando a medida que lo vamos actualizando y a la vez, se muestra la información del estado debajo del campo del formulario.Aquí un ejemplo:
Con esto, ya tenemos la base para empezar a trabajar con la validación y el procesamiento de esta información.
Para validarlo, vamos a usar un patrón (pattern
) como el siguiente que lo añadimos en src/routes/index.tsx
fuera de component$
:
const EMAIL_PATTERN = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
Añadimos dos funciones nuevas dentro del componente principal junto con los cambios propuestos:
import {
...
useComputed$,
...
$,
} from "@builder.io/qwik";
const EMAIL_PATTERN = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
export default component$(() => {
...
const onSubmit = $(() => {
const { email } = formContainer;
console.log({ email });
});
const validateEmail = useComputed$(() =>
EMAIL_PATTERN.test(formContainer.email) ? "" : "invalid-field");
return (
...
<form onSubmit$={onSubmit} preventdefault:submit>
...
)
Donde onSubmit$
es un evento que nos sirve para recoger toda la información del formulario y enviarlo para hacer lo que nosotros creemos oportuno. En este caso, mostramos en la consola la información del email
.
El elemento preventdefault:submit
sirve para cancelar el evento que refresca la página al hacer click en el botón del tipo submit
. Podéis probar a quitarlo y comprobadlo.
Ya podréis observar, que si no se añade preventdefault:submit
, cualquier información que tengamos no se mantiene activa en el input
y se pierde, por lo que os recomiendo dejarlo como os he indicado.
Teniendo las Herramientas del Desarrollador
activas rellenamos con un correo electrónico cualquiera, por ejemplo contacto@qwik-book.es
y pulsamos en Suscribirme
:
1
: Introducimos el correo.2
: Se refleja el cambio detectado en el input
del email
.3
: Ejecutamos el evento onSubmit$
pata procesar información del formulario.4
: Imprime el contenido de lo que tenemos almacenado en el estado formContainer
en la consola del navegador.Con esto, ya tenemos el primer apartado, solo nos quedaría reflejar el estado del campo con los datos introducidos si son válidos o no.
Añadimos los siguientes estilos en src/routes/index.css
junto con lo que ya tenemos añadido:
.invalid-field {
border: 1px solid red;
}
.invalid-message {
background-color: rgb(244, 165, 165);
margin-top: 1rem;
padding: 1rem;
border-radius: 0.5rem;
}
En src/routes/index.tsx
añadimos debajo del input
un nuevo div que solo se mostrará cuando el correo NO SEA correcto y se active la clase .invalid-field
.
Añadimos el siguiente contenido:
...
<form onSubmit$={onSubmit} preventdefault:submit>
<div class="formbold-email-subscription-form">
...
</div>
{
validateEmail.value === 'invalid-field' ?
<div class="invalid-message">
El e-mail introducido no es correcto. Debe de seguir el siguiente formato: contacto@qwik-book.es
</div> : null
}
</form>
Guardamos los cambios y al cargar, como el email no es el correcto, por estar vacio, nos muestra la siguiente apariencia:
Si introducimos un correo electrónico correcto, desaparecerá el mensaje descriptivo y también cambia de estado los bordes del campo del correo electrónico:
Lo único que queda es habilitar / deshabilitar el botón Suscribirme
dependiendo de si es válido o no el dato.
Añadimos estos estilos en src/routes/index.css
:
.formbold-btn:disabled,
.formbold-btn[disabled] {
border: 1px solid #999999;
background-color: #cccccc;
color: #666666;
}
Y en src/routes/index.tsx
en el elemento button
especificamos si está deshabilitado o no, dependiendo de si se ha especificado con NO correcto el dato introducido en el correo:
<button class="formbold-btn" disabled={validateEmail.value === 'invalid-field'}>
...
</button>
Y al guardar los cambios, podemos observar lo siguiente:
Como el email no es correcto (Es requerido y está en blanco, por lo que no es correcto), no puede mandarse esa información y por lo tanto, el botón está deshabilitado junto con el mensaje de error que muestra.
Ahora, si introducimos un correo electrónico correcto, el mensaje de error no se muestra y el botón está disponible para que podamos interactuar y procesar la información:
Y llegados a este punto, como se puede observar, hemos tenido que dar un montón de pasos para poder trabajar únicamente con un campo.
Podemos hacer una simplificación bastante evidente haciendo uso de los recursos que nos ofrece Qwik mediante el uso del componente Form
y mediante actionLoader$
, elementos que pertenecen a Qwik City.
Pasamos al siguiente punto comenzando con un nuevo proyecto llamado 13-02-forms-basic-qwik
El código que encontráis es el resultado final de todo el proceso realizado durante este punto. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y si lo deseáis, podéis ir comparando.
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/0Fbis9EZ5y
Form
y routeAction$
{#form-routeaction}Usando la base del apartado anterior, vamos a trabajar con ello para integrar los elementos que nos proporcionan Qwik, las acciones con routeAction$
y el componente Form
, que servirá para construir el formulario sin ensuciar mucho la parte del código JSX.
routeAction$
En la siguiente referencia tenemos información sobre esta función, aunque con lo que veamos en este capítulo no va a ser necesario usarla.
¿Qué son y para que sirven las acciones?
Las acciones nos van a permitir manejar los envíos de formularios, permitiendo realizar efectos secundarios como escribir en una base de datos o enviar un correo electrónico entre otras cosas.
Las acciones también nos van a dar la opción de poder devolver datos al cliente/navegador, permitiendo actualizar la interfaz de usuario en consecuencia realizando por ejemplo la acción de mostrar un mensaje de éxito después de enviar un formulario.
Las acciones pueden declararse utilizando routeAction$()
o globalAction$()
exportados desde @builder.io/qwik-city
.
Nosotros trabajaremos con routeAction$()
por no tener la intención de trabajar con este formulario en diferentes rutas.
Si se dierá el caso que necesitáis definir una misma acción para diferentes rutas, os recomiendo usar globalAction$()
, esa sería la única diferencia principal entre usar una y otra.
Siguiendo la documentación oficial, definimos la acción de la ruta con routeAction$
con la siguiente información en src/routes/index.tsx
:
...
import { routeAction$ } from '@builder.io/qwik-city';
...
export const useNewsletterAddEmail = routeAction$(async (data) => {
// Esto solo se ejecutará en el servidor cuando el usuario envíe el formulario
// (o cuando la acción se llame programáticamente).
console.log("(FORMULARIO) Datos a enviar", data);
// Respuesta que se devuelve
return {
success: true,
data,
};
});
...
Ahora que ya hemos definido la acción donde se va a gestionar la información del formulario aplicamos el uso del componente Form
que nos proporciona Qwik.
Haremos el cambio del selector form
que ya está incorporado por el componente Form
importando desde:
import { Form, routeAction$ } from '@builder.io/qwik-city';
Añadiendo el cambio dentro de component$
de la siguiente forma:
<Form onSubmit$={onSubmit} preventdefault:submit>
...
</Form>
Ahora eliminamos el evento onSubmit$
ya que usaremos una acción con el atributo action
que cargaremos desde el routeAction$()
que hemos creado en el paso anterior.
Añadimos el valor constante action
dentro de component$()
usando useNewsletterAddEmail()
quedando de la siguiente manera:
import { $, component$, useComputed$, useStore, useStyles$ } from '@builder.io/qwik';
import formStyles from './index.css?inline';
import { Form, routeAction$ } from '@builder.io/qwik-city';
...
export const useNewsletterAddEmail = routeAction$(async (data) => {
...
});
export default component$(() => {
const action = useNewsletterAddEmail();
...
return (
...
);
});
Y ahora que ya tenemos la acción definida la añadimos en el componente Form
para que se pueda comunicar y poder usar la información que vayamos añadiendo:
import { $, component$, useComputed$, useStore, useStyles$ } from '@builder.io/qwik';
import formStyles from './index.css?inline';
import { Form, routeAction$ } from '@builder.io/qwik-city';
...
export const useNewsletterAddEmail = routeAction$(async (data) => {
...
});
export default component$(() => {
const action = useNewsletterAddEmail();
...
return (
<Form action={action}>
...
</Form>
);
});
Ahora lo único que queda es hacer referencia a los elementos del formulario y en este caso es muy importante asignar correctamente el valor de name
ya que será el valor que se almacene en la propiedad que asignemos.
Por lo tanto, teniendo lo siguiente:
<input
type="email"
name="email"
id="email"
placeholder="Introduzca su correo electrónico"
onInput$={(event) =>
(formContainer.email = (
event.target as HTMLInputElement
).value)
}
class="formbold-form-input invalid-field"
required
/>
Lo reducimos a lo siguiente, quitando el elemento id
y el evento onInput$
que no van a ser necesarios:
<input
type="email"
name="email"
placeholder="Introduzca su correo electrónico"
class="formbold-form-input invalid-field"
required
/>
Eliminamos todos los apartados donde se valida con validateEmail
para tener el botón habilitado / deshabilitado y donde se asigna invalid-class
en los casos que el correo no es el correcto.
No os preocupéis, que la validación la implementamos posteriormente con lo que nos proporcione Qwik, vamos paso a paso.
Al realizar los cambios se muestra el formulario de la siguiente forma:
Añadimos cualquier correo electrónico (por ejemplo: contacto@qwik-book.es
) y pulsamos al botón de Suscribirme
.
Ahora para comprobar el registro en consola de lo que se ha establecido en la acción de la ruta (routeAction$
) llamada useNewsletterAddEmail()
tendremos que mirar en la consola del SERVIDOR, donde estamos ejecutando nuestro proyecto Qwik y como podéis observar, ahí aparece el correo electrónico como dato procesado (que es el único):
Lo último que haremos para terminar con este apartado es recibir la información que devolvemos usando el routeAction$
que hemos creado con el nombre useNewsletterAddEmail()
.
Como en este caso desde useNewsletterAddEmail()
se está devolviendo esta información:
export const useNewsletterAddEmail = routeAction$(async (data) => {
...
// Respuesta que se devuelve
return {
success: true,
data,
};
});
Para hacer uso de esa información, debemos de usar el valor de la constante usado como si fuese un elemento creado con useSignal
(o useStore
), accediendo a toda la información mediante la propiedad value
y posteriormente a las propiedades definidas en esa respuesta, que en este caso serían success
y data
.
Siguiendo el contexto de nuestro ejemplo, dentro de component$()
teniendo lo siguiente:
const action = useNewsletterAddEmail();
Y sabiendo que la respuesta de ese routeAction$
es:
return {
success: true,
data,
};
Para acceder a esta respuesta dentro de component$()
lo haremos mediante:
// Primera opción
{action.value.success}
// Segunda opción
{action.value.data}
// Dentro de esto ya a las propiedades que le añadimos como 'email'
Con todo esto, vamos a aplicarlo dentro del apartado del Form
con dos estados, OK
y Pendiente
:
...
export default component$(() => {
...
return (
<div class="formbold-main-wrapper">
<div class="formbold-form-wrapper">
<Form action={action}>
...
<p>¿Enviado? {action.value?.success ? 'OK' : 'Pendiente' }</p>
</Form>
...
);
});
Una vez almacenados los cambios, guardamos y vemos el resultado.
Aquí tenemos el estado cuando hemos cargado esta ruta:
Si introducimos un correo electrónico como contacto@qwik-book.es
y pulsamos en Suscribirme
, aquí debería de cambiar a OK
:
Si os fijáis, en el apartado de Email registrado
no se muestra nada, ya que en estos momentos el valor que se obtiene con useStore
no se modifica. Para visualizar el correo cambiamos a action.value.data.email
:
...
export default component$(() => {
...
return (
<div class="formbold-main-wrapper">
<div class="formbold-form-wrapper">
<Form action={action}>
...
</Form>
<p>Email registrado: {action.value?.data ? JSON.stringify(action.value.data.email) : '-' }</p>
...
);
});
Introduciendo de nuevo el correo y pulsando a Suscribirme
ya se muestra el correo:
Con esto ya hemos terminado este apartado y nos vamos a centrar en el apartado de validar los datos usando zod$()
.
El código que encontráis es el resultado final de todo el proceso realizado durante este punto. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y si lo deseáis, podéis ir comparando.
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/wKt1sSbVAt
zod$
{#validate-input-qwik-zod}Con el objetivo de poder enviar información correcta al servidor, debemos de validar la información desde el cliente.
Tal y como se ha realizado anteriormente mediante la comparación usando regex
vamos a usar una opción más limpia y simplificada que nos va a permitir validar los campos de nuestros formularios sin mucho esfuerzo.
Siguiendo con la documentación de Qwik correspondiente a los Actions
con routeAction$
vamos a validar los campos de nuestros formularios combinándolo con Zod
.
Zod es una biblioteca que proporciona declaración y validación de esquemas. Puede inferir tipos de TypeScript a partir de los esquemas, por lo que no tiene que declarar todo dos veces. También puede validar con los esquemas si tiene formularios en su página, por ejemplo.
Las funciones de Zod son muy parecidas a los tipos familiares de TypeScript, por lo que comenzar es bastante rápido y sencillo.
zod$()
)Antes de comenzar a trabajar con esta forma de validar nuestros formularios en Qwik combinándolo con routeAction$()
os recomiendo que tengáis en cuenta el siguiente bloque.
zod$
En las siguientes referencia tenemos toda la información sobre esta función que nos servirá como apoyo para tener el soporte sobre lo que vamos a trabajar a continuación.
Enlaces:
Creamos un nuevo proyecto con el nombre 13-03-forms-with-qwik-zod
basándonos en el proyecto anterior.
Vamos al apartado donde se ha definido el routeAction$
definido como useNewsletterAddEmail()
y añadimos el apartado de lo que corresponde a la validación del campo email
con zod$
especificando lo siguiente:
string
.Email válido
.Con estos datos, realizamos el siguiente cambio:
...
import {
...
routeAction$,
z,
zod$,
} from '@builder.io/qwik-city';
export const useNewsletterAddEmail = routeAction$(
async (data) => {
...
// SI ES VÁLIDO devuelve esto
return {
success: true,
data,
};
},
// SI NO ES VÁLIDO
// Zod schema es usado para validar datos de un formulario
// En este caso vamos a validar que sea de tipo string, de tipo email
zod$({
email: z.string().email(),
}),
);
Aplicamos la verificación si da error o no, comprobando que la propiedad failed
es verdadera, cuando se de un error de validación con el correo, que no es válido, para mostrar el mensaje de alerta:
export default component$(() => {
...
return (
<div class="formbold-main-wrapper">
<div class="formbold-form-wrapper">
<Form action={action}>
<div class="formbold-email-subscription-form">
...
{action.value?.failed && (
<div class="invalid-message">
El e-mail introducido no es correcto. Debe de seguir el siguiente
formato: contacto@qwik-book.es
</div>
)}
...
</Form>
...
</div>
);
});
Guardamos los cambios y probamos. Recordad que antes siempre nos registraba en la consola el valor introducido en el campo e-mail independientemente si era válido o no.
Es decir, lo que corresponde a este mensaje:
Se van a dar dos nuevos escenarios:
contacto@qwik-book.es
). NO se muestra el mensaje de alerta y en la consola se muestra el correo como lo hacía anteriormente:Lo registrado en la consola:
contacto@qwik-book
). SI se muestra el mensaje de alerta:Y en consola se muestra este mensaje, donde se muestra el error de validación que nos proporciona zod$()
dando toda la información del motivo porque no se valida:
Usando esa información, utilizamos lo que viene en el campo message
para incrustar el mensaje de error y mostrarlo dentro de la alerta.
Cambiamos lo siguiente:
{action.value?.failed && (
<div class="invalid-message">
El e-mail introducido no es correcto. Debe de seguir el siguiente
formato: contacto@qwik-book.es
</div>
)}
Por lo siguiente:
{action.value?.failed && action.value.fieldErrors.email?.length && (
<div class="invalid-message">
{
action.value.fieldErrors.email[0]
}
</div>
)}
Ahora lo que nos muestra, dejando el código JSX más limpio es lo siguiente cuando el EMAIL es INCORRECTO:
Y viendo esto, seguramente os estéis haciendo esta pregunta, ya que ahora queda un mensaje muy general y queremos mostrar el mensaje descriptivo de antes.
¿Podría personalizar el mensaje haciéndolo más descriptivo sin añadirlo en el código JSX?
La respuesta es SI. Mediante la redefinición del mensaje de error (Más información) podemos especificar el mensaje de error que nosotros queremos.
Aplicando lo siguiente en nuestro código, vamos a useNewsletterAddEmail
y en el apartado de la validación del campo email
pasamos del siguiente código:
zod$({
email: z.string().email(),
}),
Y añadimos como primer argumento en la función email()
el objeto con la propiedad message
y el mensaje personalizado, pasando a lo siguiente:
zod$({
email: z.string().email({
message: `
El e-mail introducido no es correcto. Debe de seguir el siguiente formato:
contacto@qwik-book.es
`
}),
}),
Y ahora probando de nuevo introduciendo el correo electrónico incorrecto (p.j: conta@qwik
):
Y en la consola ya aparecería con el mensaje (en message
) personalizado añadido a nuestro gusto:
Ajustar los estilos de validación con el nuevo modo de validar
Seguramente os habéis dado cuenta que cuando introducimos el correo electrónico correcto siempre nos aparece el campo en color rojo indicando que es incorrecto (aunque el mensaje diga que está OK).
Esto es porque no estamos verificando el valor introducido mediante la validación.
La clase invalid-field
está añadida SIEMPRE, por lo que siempre se muestra y eso no sería lo correcto, obviamente.
Teniendo en cuenta lo aprendido con el routeAction$
junto con zod$()
vamos a añadir la condición para que añada esa clase invalid-field
solo cuando se de el caso que el correo no es correcto.
Cambiamos lo que tenemos en el input:
<input
type="email"
name="email"
placeholder="Introduzca su correo electrónico"
class="formbold-form-input invalid-field"
required
/>
A lo siguiente, donde ya se verifica el estado del campo, para ver si hay errores:
<input
type="email"
name="email"
placeholder="Introduzca su correo electrónico"
class={`formbold-form-input ${
action.value?.fieldErrors?.email?.length ? 'invalid-field' : ''
}`}
required
/>
Al guardar y escribir un correo válido, antes de hacer click en el botón, podemos observar que los bordes rojos ya no aparecen, tanto si tenemos el foco sobre el campo como si no:
Pulsamos el botón de Suscribirme
y como es un correo VÁLIDO, NO muestra el borde rojo:
Si modificamos y hacemos que el correo no sea el correcto, al ejecutar Suscribirme
, podemos observar que ya SE MUESTRA el borde rojo:
Llegados a este punto, podemos decir que ya sabemos trabajar perfectamente con las bases de los formularios en Qwik haciendo uso de los Actions
junto con las validaciones con zod$()
.
El código que encontráis es el resultado final de todo el proceso realizado durante este punto donde se han añadido algunas refactorizaciones desacoplando elementos como el botón de Suscribirme, el apartado del elemento Input entre otros elementos. Esto se da por hecho que ya domináis.
Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y si lo deseáis, podéis ir comparando.
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/y3T9SAnSYH
Creamos un nuevo proyecto con el nombre 13-04-forms-bookmarks-db
El objetivo de este proyecto es poner en práctica lo visto anteriormente pero con un ejemplo más completo y aplicando algo más de complejidad a un caso real.
Tendrá las siguientes características a tener en cuenta:
localStorage
como elemento de persistencia de datos ya que el objetivo es practicar con los formularios (lo ideal sería usar una base de datos mediante una API).routeAction$()
para recoger la información y realizar las acciones necesarias.zod$()
para un mayor control de lo que vamos a mandar a guardar.Si queréis ampliar lo visto aquí con algún elemento donde tengamos una Base de datos, podéis seguir la documentación oficial donde se muestra como hacerlo con la integración de Prisma que junto con la documentación oficial de esta, podremos persistirlo en diferentes Bases de Datos, tanto relacionales (PosgreSQL, MySQL) como en NO relacionales como MongoDB,...
Una vez aclarado todo esto, lo que conseguiremos al final de este punto será lo siguiente:
Basándonos en este boceto, vamos a añadir el código base inicial para empezar el proyecto.
Os dejo tanto el contenido del CSS como la estructura principal del componente con el contenido de la ruta con el formulario que se ha propuesto:
Una vez que añadamos en las rutas especificadas, iniciamos el proyecto y podemos observar con la siguiente apariencia que ya está todo correcto para iniciar el proyecto.
Os recomiendo que reviséis el código añadido para ver como está constituido el formulario.
Así se puede ver de que base partimos para ir completando los pasos realizados anteriormente adaptados a este proyecto.
En este código que os he proporcionado ya tenemos encapsulado en el componente Form
junto con la definición del routeAction$
, donde recibimos la información para mostrarla en el registro de la consola del Servidor.
Rellenamos los campos del formulario (1
, 2
y 3
) con la siguiente información (podéis poner lo que queráis) y pulsamos en Guardar
(4
):
Una vez realizados estos pasos, debemos de comprobar que se muestran los datos introducidos en el registro de la consola del Servidor que corresponde al siguiente código:
...
export const useAddUrlToStoreInLocal = routeAction$(async (data) => {
// Para ver en consola como coge los datos
console.log('Title', data.title);
console.log('Description', data.description);
console.log('URL', data.url);
...
});
Esto es lo que se muestra:
zod$()
{#validate-data-form-using-zod}No hemos tenido problemas de validaciones ni nada por el estilo, ya que todavía no hemos especificado como queremos tener los datos.
Tal y como hemos realizado anteriormente, usaremos zod$()
y vamos a validar los datos implementando estas condiciones:
Propiedad | Lo que debe de cumplir | Mensaje de error |
---|---|---|
Título (title ) |
Debe de ser un texto (string ) y su longitud mínima debe de tener 6 caracteres |
Se deben de añadir mínimo 6 caracteres |
Descripción (description ) |
Debe de ser un texto (string ) y su longitud mínima debe de tener 15 caracteres |
Se deben de añadir mínimo 15 caracteres |
Enlace (url ) |
Debe de ser un texto (string ) y el contenido debe de ser un enlace URL |
Debe de añadirse una URL correcta |
Dentro del componente principal, añadimos los elementos que necesitamos para validar como son zod$()
y z
que se usarán para validar en conjunto y los elementos de manera individual respectivamente:
...
import { ..., z, zod$, routeAction$ } from "@builder.io/qwik-city";
...
export const useAddUrlToShort = routeAction$(
async (data) => {
console.log("Title", data.title);
console.log("Description", data.description);
console.log("URL", data.url);
// Devuelve esto si pasa la verificación de zod$()
return {
success: true,
data,
};
},
// Para verificar lo introducido
zod$({
title: z.string().min(6, "Se deben de añadir mínimo 6 caracteres"),
description: z.string().min(15, "Se deben de añadir mínimo 15 caracteres"),
url: z.string().url("Debe de añadirse una URL correcta"),
})
);
...
Guardamos los cambios e introducimos los datos de manera incorrecta:
title
: menos 6 de longitud.description
: menos de 15 de longitud.url
: Contenido que NO sea una URL.Con estos datos, añadimos un ejemplo como el siguiente:
Pulsamos Guardar
y nos debería de saltar el mensaje (en la consola del Servidor) de zod$()
con los errores correspondientes a los 3 campos:
Teniendo esto afianzado, podríamos pensar en añadir la funcionalidad para que se reflejen esos errores tanto en los campos modificándose el color de los bordes y a su vez que se muestre el mensaje (message
) de validación tal y como hemos realizado en el proyecto anterior.
Lo primero que vamos a hacer es añadir un nuevo elemento por input
para mostrar el mensaje personalizado de validación de cada campo:
<input type="text" ... />
<div class="invalid-message">
(MENSAJE DE ERROR)
</div>
Aplicando los cambios, quedaría así:
<Form class="form" action={action}>
...
<label>
<input class="input" type="text" name="title" />
<span>Título</span>
</label>
<div class="invalid-message">
(MENSAJE DE ERROR title)
</div>
<label>
<input class="input" type="text" name="description" />
<span>Descripción</span>
</label>
<div class="invalid-message">
(MENSAJE DE ERROR description)
</div>
<label>
<input class="input" type="text" name="url" />
<span>Enlace</span>
</label>
<div class="invalid-message">
(MENSAJE DE ERROR url)
</div>
<button class="submit">Guardar</button>
</Form>
Si guardamos los cambios, como estaréis imaginando, ocurre lo siguiente, donde es obvio que no deberían de mostrarse los mensajes de error al inicio:
Para mostrar los errores, cuando se requiere debemos de hacer uso del routeAction$()
que tenemos definido para coger los errores tal y como hemos hecho en el apartado anterior.
Añadimos lo siguiente dentro de component$()
sin hacer cambios en otros apartados:
export default component$(() => {
...
const action = useAddUrlToStoreInLocal();
// Obtenemos los errores en general
const fieldErrors = action.value?.fieldErrors!;
// De manera individual
const titleError = fieldErrors?.title;
const descriptionError = fieldErrors?.description;
const urlError = fieldErrors?.url;
return (
...
);
});
Y ahora teniendo esto usamos titleError
, descriptionError
y urlError
para mostrar los mensajes de error siempre y cuando la validación falle:
<Form class="form" action={action}>
...
<label>
<input class="input" type="text" name="title" />
<span>Título</span>
</label>
{titleError && <div class="invalid-message">{titleError[0]}</div>}
<label>
<input
class="input"
type="text"
name="description"
/>
<span>Descripción</span>
</label>
{descriptionError && <div class="invalid-message">{descriptionError[0]}</div>}
<label>
<input class="input" type="text" name="url" />
<span>Enlace</span>
</label>
{urlError && <div class="invalid-message">{urlError[0]}</div>}
<button class="submit">Guardar</button>
</Form>
Al guardar los cambios podemos observar que los mensajes de error NO aparecen.
Solo aparecerán si no cumplen las condiciones especificadas anteriormente y ya aparecerán con los mensajes personalizados que hemos especificado en el routeAction$()
en el apartado de los campos con zod$()
.
Probamos con diferentes situaciones:
Título NO válido.
Título y Descripción NO válidos.
Título y Enlace (url) NO válidos.
Tendríamos alguna combinación más, pero como se puede observar ya funciona correctamente.
Lo que si faltaría ir afinando algunos aspectos:
Fuera del elemento <Form>...</Form>
añadiremos lo siguiente, verificando que el estado de la operación es un 200
y que la propiedad success
dentro de la propiedad value
dentro de action sea true
Lo añadimos de la siguiente forma:
{ action.status === 200 && action.value?.success && <>Preparado para guardar</> }
Quedando de la siguiente forma:
Y si lo probamos, debería de mostrarse Preparado para guardar
si todos los datos son correctos:
Teniendo en cuenta que ya podemos detectar los errores en cada uno de los campos, lo que tenemos que hacer es poner una condición para añadirle una clase en el que caso de que NO sea válido un campo.
label
:Hacemos el ejemplo con el apartado title
verificando si hay un error en ese campo y en el caso afirmativo, le asignamos la clase invalid-class
:
<label>
...
<span class={titleError ? 'invalid-class' : ''}>Título</span>
</label>
Lo probamos, forzando a que nos muestre un error en el campo title
:
Y como se puede apreciar, al tener el error, el color del label se cambia al color rojo aunque no en los demás, ya que habrá que aplicarlo de manera individual como se ha hecho con title
.
Esto lo podéis hacer sin problemas y debería de quedar en algo como lo siguiente al ejecutar y mostrar los errores:
Si el valor es correcto el color del label seguirá estando verde dándolo como válido.
Una vez que ya hemos trabajado con el estado del label de cada campo, debemos de hacerlo con los bordes de los campos.
Añadimos lo siguiente en el archivo src/routes/index.css
para resaltar con borde rojo los errores:
input.invalid-class {
border: 1px solid red !important;
}
Y hacemos el mismo procedimiento, pero añadiéndolo en el input
(por ejemplo en title
):
<label>
<input
class={`input ${titleError ? 'invalid-class' : ''}`}
type="text"
name="title"
/>
<span class={titleError ? 'invalid-class' : ''}>Título</span>
</label>
Añadimos lo que corresponde a los otros campos y probamos haciendo que salten todos los errores:
Ahora sin refrescar si validamos title
y description
por ejemplo:
Y llegados a este punto, ya tenemos el formulario listo para enviar únicamente los datos que van a ser válidos para almacenar los datos introducidos.
Estos datos los podemos usar para almacenarlos, para hacer operaciones o lo que necesitemos.
En este caso particular, lo interesante es almacenar, para tener nuestro propio almacén de enlaces favoritos.
localStorage
{#save-form-data-in-localstorage}Para almacenar en el almacenamiento local de nuestro navegador, usaremos localStorage. Iremos añadiendo los datos del formulario con el que estamos trabajando mediante el hook useLocalStorage
que os proporciono en el siguiente enlace:
Copiamos su contenido y lo añadimos en el fichero que vamos a crear con el nombreuseLocalStorage.tsx
dentro de src/hooks
.
Una vez que lo tenemos añadido, vamos a src/routes/index.tsx
e inicializamos el hook de la siguiente manera, desestructurando los elementos que usaremos para realizar las operaciones de gestión de la información.
Inicializando lo siguiente dentro de component$()
teniendo en cuenta que añadiremos una clave
como argumento. Añadiremos la inicialización con la clave que necesitemos:
const { set, clear, data, loading } = useLocalStorage(`clave-donde-se-almacena-info`);
Partiendo de la base mostrada, usamos la clave my-bookmarks
quedando de la siguiente forma lo que respecta a la inicialización del hook:
...
import { useLocalStorage } from "~/utils/hooks/useLocalStorage";
...
export const useAddUrlToStoreInLocal = routeAction$(
...
);
export default component$(() => {
const { set, clear, data, loading } = useLocalStorage(`my-bookmarks`);
...
});
...
Los elementos que obtenemos desde el hook al desestructurar harán lo siguiente:
set
: función para añadir la información dentro la clave que hemos inicializado con la actualización.clear
: para eliminar (resetear) toda la información asignada a la clave especificada.loading
: Para gestionar el estado de carga para situaciones en los que estemos obteniendo la información dentro de la clave seleccionada con el elemento data
.data
: Información almacenada que mostraremos cuando loading
sea falso, cuando ya haya realizado la carga.Sabiendo lo siguiente, en este mismo fichero src/routes/index.tsx
añadiremos el hook useVisibleTask$
con la función track
para observar los cambios que se dan dentro de action
, concretamente cuando la operación es satisfactoria que lo reflejaremos con una nueva propiedad readyToStorage
que será la que nos habilita la posibilidad de almacenar nuevos datos o no.
Lo primero añadimos la propiedad mencionada readyToStorage
dentro del elemento del routeAction$
llamado useAddUrlToStoreInLocal
:
...
export const useAddUrlToStoreInLocal = routeAction$(
async (data) => {
...
return {
success: true,
// Para especificar que está preparado para guardarlo en local
readyToStorage: true,
data,
};
},
...
);
...
Y con esto, añadimos el hook useVisibleTask$
donde observamos los cambios que se darán en readyToStorage
:
// 1
import {..., useVisibleTask$ } from '@builder.io/qwik';
...
import { useLocalStorage } from '../hooks/useLocalStorage';
...
export default component$(() => {
...
// 2
const { set, clear, data, loading } = useLocalStorage(`my-bookmarks`);
//3
useVisibleTask$(async ({ track }) => {
// 4
track(() => [action.value?.readyToStorage]);
// 5
if (action.value?.readyToStorage) {
console.log("OK");
// 6
const saveData = [...data.value, action.value.data];
// 7
await set(saveData);
// 8
action.value.readyToStorage = false;
}
});
...
});
Funcionando de la siguiente forma:
1
: Importamos los elementos necesarios como el custom hook useLocalStorage
y el hook useVisibleTask$
.2
: Iniciamos el hook con la clave my-bookmarks
donde almacenaremos todos los marcadores favoritos.3
: Añadimos el hook useVisibleTask$
para guardar el nuevo registro siempre y cuando el valor readyToStorage
dentro de action.value
sea verdadero.4
: Observar los cambios en action.value.readyToStorage
para reejecutar el hook.5
: Verifica que action.value.readyToStorage
es verdadero, para realizar el almacenamiento del nuevo registro.6
: Cogemos los datos existentes (data.value
) y añadimos el nuevo registro (action.value.data
) y los añadimos en un array ya que es la estructura que guardaremos en el localStorage
.7
: Enviamos a la función set
todos los datos para actualizarlo en my-bookmarks
.8
: false
en action.value.readyToStorage
para que no entre de nuevo en este apartado mientras no se notifique desde el routeAction$
cuando se validen los datos del nuevo registro.Teniendo claro lo siguiente, lo que vamos a hacer ahora es probarlo con un ejemplo como el siguiente:
Antes de pulsar en Guardar
vamos a abrir las Herramientas de desarrollador
de nuestro navegador y dentro de el accedemos a los datos del localStorage
:
Pulsamos en Guardar
(1
).
Si todo ha ido correctamente, debe de mostrarse el mensaje de que está preparado para guardarse (2
) y posteriormente si nos fijamos en el localStorage, podemos observar que tenemos almacenada (3
) la información que hemos añadido en el formulario dentro de la clave my-bookmarks
:
Si hacemos click sobre el valor my-bookmarks
(1
) podemos observar con más detalle (2
) los datos almacenados y como se puede apreciar, son los mismos que se han añadido en el formulario:
Con esto, ya tenemos lo principal completado.
Para terminar vaomos a añadir los detalles para dejarlo más afinado y esto será lo que haremos:
input
.Realizaremos los tres primeros, lo que corresponde a la refactorización ya lo trabajaís por vuestra cuenta.
Mensaje guardado
Pasamos de lo siguiente:
...
{action.status === 200 && action.value?.success && (
<>Preparado para guardar</>
)}
...
A un mensaje más descriptivo:
...
{action.status === 200 && action.value?.success && (
<>Almacenado correctamente el elemento: {action.value.data.title} ({action.value.data.url})</>
)}
...
Y siguiendo los pasos para añadir un nuevo elemento, se mostrará de la siguiente forma:
Podríamos darle un poco de estilo al mensaje, para resaltarlo pero eso ya lo podéis hacer por vuestra cuenta.
Cargar lista de marcadores
Debajo del formulario, vamos a añadir el apartado donde se visualizan los marcadores que vamos a ir añadiendo.
Esto podríamos hacerlo en otra ruta, pero en este caso lo hacemos en esta misma ruta con una lista.
...
export default component$(() => {
....
return (
<div>
<h1>
Repositorio de Marcadores Favoritos <span class="lightning">⚡️</span>
</h1>
(::FORMULARIO::)
<hr />
<h2>Lista de enlaces favoritos</h2>
<ul>
<li>
<span>{"Hola"}</span> - <span>{"Desc"}</span> /{' '}
<a class='btn-url' href={"url"} target='_blank'>
Ir a enlace
</a>
</li>
</ul>
</div>
);
});
...
Y en el index.css
de este mismo directorio, añadimos el estilo asociado a los elementos de la lista con el selector ul
:
ul {
list-style: disc;
margin-left: 20px;
}
Guardamos los cambios y esto es lo que vamos a observar como cambios:
Con esto, ya tenemos la estructura creada, ahora lo que tenemos que hacer es coger los datos desde el hook useLocalStorage
con la propiedad data
y para tener el estado de carga usamos loading
siendo estos dos elementos de tipo useSignal
.
Aplicamos los cambios para visualizar lo almacenado:
...
export default component$(() => {
....
return (
<div>
<h1>
Repositorio de Marcadores Favoritos <span class="lightning">⚡️</span>
</h1>
(::FORMULARIO::)
<hr />
<h2>Lista de enlaces favoritos</h2>
{ loading.value && <h2>Cargando marcadores favoritos...</h2>}
{
!loading.value && !data.value.length && <h2>No hay marcadores almacenados. Empieza a añadirlos desde el formulario</h2>
}
{
!loading.value && data.value.length && <ul>
{data.value.map((urlItem) => {
const {title, url, description} = urlItem;
return (<li>
<span>{title}</span> - <span>{description}</span> /{' '}
<a class='btn-url' href={url} target='_blank'>
Ir a enlace
</a>
</li>)
})}
</ul>
}
</div>
);
});
...
Y una vez guardado, debe de mostrarse la información que tenemos almacenada (si es el caso).
Para comprobar que se visualiza correctamente, vamos a las Herramientas del desarrollador
de nuestro navegador y observamos lo que está guardado en el Almacenamiento Local
que en mi caso sería lo siguiente:
Como se puede observar, tenemos un elemento guardado y viendo esos datos, se mostrará de la siguiente forma:
Ahora si añadimos otro marcador, directamente sin recargar la página se irá mostrando:
Botón eliminar marcadores
Después de la lista de marcadores, añadimos un botón con estas propiedades CSS:
.delete-btn {
box-shadow:inset 0px 1px 0px 0px #cf866c;
background:linear-gradient(to bottom, #d0451b 5%, #bc3315 100%);
background-color:#d0451b;
border-radius:3px;
border:1px solid #942911;
display:inline-block;
cursor:pointer;
color:#ffffff;
font-family:Arial;
font-size:13px;
padding:6px 24px;
text-decoration:none;
text-shadow:0px 1px 0px #854629;
}
.delete-btn:hover {
background:linear-gradient(to bottom, #bc3315 5%, #d0451b 100%);
background-color:#bc3315;
}
.delete-btn:active {
position:relative;
top:1px;
}
Y el código del botón de eliminar junto con la acción de eliminar será lo siguiente:
<button class="delete-btn" onClick$={() => clear()}>Eliminar</button>
Al guardar los cambios, se verá de la siguiente forma después de la lista de marcadores y en este caso se muestra lo almacenado en local:
Si hacemos click en Eliminar
desaparecerá tanto lo que se visualiza en la lista y también lo que se visualiza almacenado en el Almacenamiento local
:
Y con esto ya tendríamos el funcionamiento completo de nuestra aplicación para guardar marcadores.
Refactorizar los elementos input
Con todo lo que hemos ido aprendiendo, ya deberíamos de ser capaces de hacer este apartado.
Mi propuesta os la dejo a continuación con el resultado final del proyecto.
El código que encontráis es el resultado final de todo el proceso realizado durante este punto. Os recomiendo que vayáis haciendo los pasos poco a poco y paso a paso para ir interiorizando todos los conceptos y si lo deseáis, podéis ir comparando.
El enlace lo tenéis a continuación:
https://shorten-up.vercel.app/9zqiX8fUcI
Llegados a este punto hemos visto todo lo indispensable para trabajar con los formularios Qwik partiendo desde la base:
Form
, ni routeAction$()
ni zod$()
.Form
y routeAction$()
.Form
,routeAction$()
y zod$()
con sus validaciones al detalle.Los formularios son otro de los elementos fundamentales junto con conceptos como Componentes, Layouts, Ciclos de vida, etc. que hemos ido viendo a lo largo del libro.
Estos conocimientos nos permitirán recopilar información en nuestros proyectos web tanto para realizar formulario de tipo contacto, encuestas, generadores en base a unos datos introducidos,...
Con esto terminamos el capítulo y pasamos al siguiente, a trabajar con los Eventos.