Lecciones del curso
Aprende Remix construyendo un Blog con MongoDB y Netlify
Editando posts | Segunda parte
Como te diste cuenta, esta ruta está tomando tamaño, y eso está bien, pues lo haremos todo en el mismo archivo. 🤯
La edición de un modelo nunca había sido tan sencillo como lo es ahora con Remix. Gracias a que podemos explotar el principio de proximidad.
🪄 Modificando el prisma.schema
Necesitamos incluir un campo para un link de imagen, así mismo necesitamos un campo para agregar un tag que nos ayude a clasificar nuestro post. Esto también será útil para crear un filtro en la vista de lista más adelante.
model Post { id String @id @default(auto()) @map("_id") @db.ObjectId slug String? @unique title String @default("Sin nombre") body String @default("") tags String @default("") cover String? author User @relation(fields: [userId], references: [id]) published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String @db.ObjectId }
El post queda así. Hemos agregado el campo para la imagen: cover
, y también un string para el tag
. Los dos resultan opcionales porque hemos asignado un valor por default.
Vamos a actualizar nuestro cliente de Prisma y subir los cambios en el modelo:
npx prisma db push && npx prisma generate
¡Super! Ahora vamos a guardar. 💾
💾 Guardando la edición en la base de datos
Haremos esto en dos partes.
En la primera, guardaremos directamente utilizando un formulario y la función action de Remix, utilizaremos también a fetcher para administrar mejor las respuestas y poder mostrar mensajes de error, muy básicos pero útiles.
Para la segunda, agregaremos un pequeño hack o truco, 🕹️ que nos permitirá guardar el Markdown que el autor escriba; ¡en cuanto deje de escribir y de forma automática! 🤖
💬 Primera parte | <Form>
El componente <Form> de Remix es especial, contiene todos los atributos y métodos de un <form> nativo. <Form> es un wrapper en torno a la etiqueta <form> que ya evita su comportamiento por default que es el de refrescar la pagina.
const fetcher = useFetcher(); return ( <> <fetcher.Form method="post"> // ... </fetcher.Form> <div className="flex text-red-500"> {fetcher.data?.error && ( <ul> {fetcher.data.error.issues.map((err) => ( <li key={err.path}> {err.path} {err.message} </li> ))} </ul> )} </div> <Toaster /> </> )
Como agregaremos los errores a la vista, necesitaremos de la ayuda de fetcher.
Como fetcher está íntimamente relacionado con el componente <Form>
. Podemos encontrar a este componente como parte del propio fetcher. 🤯
En nuestra action. Observa que estamos validando solo en el backend con Zod.
👀 Gracias a fetcher y a <Form> no hay que interrumpir el request natural del formulario, solo hacerlo en segundo plano. 👤
👨🏻💻 Entendiendo la función action
Nuestra acción es responsable de validar los datos que recibe y actualizar el post adecuadamente. ✅
import slugify from "slugify"; export const action: ActionFunction = async ({ request, params }) => { const formData = await request.formData(); // Obtenemos el formulario desde el formData const form = Object.fromEntries(formData) as Record<string, string>; // Agregamos el slug con slugify form.slug = slugify(form.title + "-" + Date.now()); // Validamos const validated = updatePostSchema.safeParse(form); if (!validated.success) { return json({ ok: false, error: validated.error }, { status: 400 }); } // Actualizamos await db.post.update({ where: { id: params.postId, }, data: validated.data, // usamos siempre los datos ya validados. }); return { ok: true }; };
En este action tomamos los datos que se encuentran en el formData y conseguimos un objeto con el que es más fácil trabajar.
Generamos el slug a partir del titulo y la fecha actual.
Validamos que los datos del objeto coincidan con el tipo esperado y si no, devolvemos un estatus de Bad Request.
Finalmente actualizamos el post correcto en la base de datos.
✅ Esquema de validación con Zod
Este es el schema que estamos usando en esta validación:
// app/utils/zod.ts export const updatePostSchema = z.object({ cover: z.string().optional(), title: z.string().min(5), slug: z.string().min(5), body: z.string().optional(), // Con Zod puedes pre procesar los datos, cual stream en un pipe. published: z.preprocess( (val) => String(val).toLowerCase() === "true" || val === true || String(val).toLowerCase() === "on" ? true : false, z.boolean() ), tags: z.string().optional(), }); export type UpdatePostType = z.infer<typeof updatePostSchema>;
El pequeño map (las <li>
en el JSX) que mostrara los mensajes de error que genera Zod, es muy rústico.
Seguro tú, los puedes embellecer. 🎨 👩🏻🎨
👀
z.preprocess()
Te permite procesar los datos antes de parsearlos, dándote la capacidad de transformarlos.
🤓 Segunda parte | El hack
Finalmente, observa el useEffect que hemos agregado para capturar la respuesta del action y levantar un <Toaster/>
que agregamos al final de nuestro JSX.
import toast, { Toaster } from "react-hot-toast"; useEffect(() => { if (fetcher.data?.ok) { toast.success("Se ha guardado tu post", { id: "exito", }); } else if (fetcher.data && !fetcher.data.ok) { toast.error("No se ha podido guardar", { id: "error", }); console.error(fetcher.data?.error); } }, [fetcher]);
Esto nos permite ofrecer una mejor experiencia a nuestro usuario.
👀 Yo estoy usando esta biblioteca:
react-hot-toast
, pero tú puedes usar la que más te guste.
Bueno, pues creo que ya todo está listo, ¡guardemos el Markdown automáticamente! 🤖
💾 Guardando automáticamente
Vamos a guardar automáticamente los cambios que se hagan en nuestro editor de Markdown, para ello vamos a ayudarnos con el método onChange del editor y un pequeño truco con setTimeout que te voy a enseñar**. 🤓**
<fetcher.Form method="post" ref={formRef}> // ... </fetcher.Form> <MarkdownEditor onChange={handleAutoSave} defaultValue={body} />
En nuestro JSX hemos agregado la función handleAutoSave
al prop onChange de nuestro editor. onChange
detona el guardado cada que el usuario presiona una tecla.
También hemos creado una referencia para el <Form>
y poder sacarle los valores programáticamente.
const formRef = createRef<HTMLFormElement>(); const timeout = signal<ReturnType<typeof setTimeout> | null>(null); const handleAutoSave = (content: string) => { if (timeout.value) { clearTimeout(timeout.value); } timeout.value = setTimeout(() => { if(!formRef.current) return; // Complaciendo a TS const formData = new FormData(formRef.current); formData.append("body", content); fetcher.submit(formData, { method: "post" }); }, 500); };
Esta es la función que contiene la magia. 🪄🎩 🐇
Nos hemos ayudado de una referencia para obtener los datos del formulario y así mandarlos al hacer submit con fetcher. Por eso creamos una señal para guardar nuestro setTimeout.
Este setTimeout es una estrategia conocida para el auto-guardado. En vez de usar onBlur o algo similar, levantamos un setTimeout que tarda 500 ms. Cada que el usuario presiona una tecla, cancelamos el setTimeout con clearTimeout()
, hasta que el usuario pase 500 ms sin escribir, ahí es cuando guardamos. 🤯
Obtenemos los valores de los inputs del formulario, los convertimos en formData (por conveniencia nomás, así no sacamos los inputs uno por uno), agregamos el Markdown (content) y lo enviamos al action. 🏄🏻
Lo que me gusta de esta estrategia:
Es que estamos explotando la colocación de código, es decir, tenemos el código del backend y del frontend en el mismo archivo, lo que hace muy fácil cambiarlos juntos. 💗
¡Y ya está! 🥳
Has trabajado muy duro te mereces un break y una cerveza.🍺 ¡No, yo no tengo, ve tú por ella!
Pero mira, ya tenemos un editor de blog funcional y moderno. 🎉 🍻
Enlaces relacionados
Colocación:
https://kentcdodds.com/blog/colocation