cover

Tabla DragAndDrop ordenable con React y Framer Motion

author photo

Héctorbliss

@hectorbliss


Mira el video si prefieres:

robot logo saying hello

No te quedes atrás: Actualízate

Suscríbete para recibir información sobre nuevos frameworks, updates, eventos, tips, hacks y más.

Formmy.app es una aplicación No-Code que te permite colocar un formulario en tu sitio web solo copiando y pegando una línea de HTML. Así puedes administrar todos tus mensajes en el mismo lugar. 🤩

Pero para que los usuarios de Formmy puedan disfrutar de una solución No-Code, yo necesito andar full-code. 👨🏻‍💻 🤓

Por eso quise arreglar la animación de ordenamiento que no terminaba de gustarme, sobre todo porque estoy utilizando una biblioteca muy limitada para el drag and drop. Mismo, que está funcionando solo en el axis x. Así que voy a sustituir esta animación por una mucho mejor. Una que haremos a manita solo con Framer Motion. 🪬

animated grid, tabla animada drag and drop

Aquí te dejo el link al resultado final. Pero, ¡ya es hora! Esto no se va a programar solo (aún) y nos tomará un buen ratito de estar pensando. Así que, si quieres practicar con TypeScript, React y Framer Motion un rato, ¡pues comencemos de una vez! 🎮

🧠 Definiendo la estrategia

Tenemos que organizarnos un poquito de principio, pues este tutorial nos tomaría mucho si lo ejecutamos sin un plan, igual que tomar un empleo sin un plan, nomás pa terminar endeudado con un coche. 🚗 Mejor dividamos nuestro trabajo en cuatro partes:

  1. Primero vamos a colocar el código inicial y las utilidades
  2. Luego nos ocuparemos de la estructura básica de los componentes y los datos iniciales
  3. En la tercera parte agregaremos la configuración necesaria para el <motion.div>
  4. Finalmente, activaremos el drag and drop y nos concentraremos en lograr las funciones de la animación 🔥

Creo que con estos cuatro simples pasos podremos lograrlo. 👊🏼

🥡 Código inicial y utilidades

He necesitado de algunas funciones para mover dos elementos dentro de un array o para lograr reordenarlo automáticamente, así como para detectar la colisión entre los cubitos con un algoritmo que he reciclado de cuando practicaba JavaScript creando videojuegos: te dejo uno de esos videos por si nunca lo has intentado, es muy divertido.

Pondremos estas utilidades en su propio archivo utils.tsx

// utils export const shuffle = (a: Color[]) => { const array = [...a]; for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; }; export const arrayMove = <T,>(a: T[], oldIndex: number, newIndex: number): T[] => { const array = [...a]; if (newIndex >= array.length) return array; const old = array.splice(oldIndex, 1)[0]; array.splice(newIndex, 0, old); return array; }; // A clasic collider detector 🥰 Checa mi curso de Tetris con JS 😉 export function isColliding(source: Position, sample: Position, threshold = 0.5) { return ( source.x < sample.x + sample.width - threshold * sample.width && source.x + source.width > sample.x + threshold * sample.width && source.y < sample.y + sample.height - threshold * sample.height && source.y + source.height > sample.y + threshold * sample.height ); } export const getInitialColors = () => { return [...Array(72).keys()].map((index) => ({ color: `hsl(${360 * (index * 0.01)}, 50%, 50%)`, id: index + 1, })) }; //

Si estas funciones te dan curiosidad, puedes leerlas con calma, por ahora, para nosotros serán solo abstracción. 📦

🍱 Maquetando la estructura básica

Crearemos dos componentes, uno llamado <Grid> y otro <Item>. Yo luego no soy suficientemente semántico con los nombres, tú puedes ponerles nombres que te hagan más sentido a ti, como <Table> y <Cell> o algo así, tú decide. Continuemos pues.

export const Grid = () => { const [showInts, setShowInts] = useState(true); const [colors, setColors] = useState<Color[]>(getInitialColors()); return ( <article > <h2 >Color grid by blissmo</h2> <div> <button> Reset </button> <button> Shuffle </button> <button> {showInts ? "Clear" : "Show"} numbers </button> </div> <section> {colors.map((i, index) => ( <Item key={i.color} /> ))} </section> </article> ) } export const Item = () => { return <motion.div /> }

Simple ¿verdad? Ocuparemos TailwindCSS para no invertir tiempo en los estilos, todo el código fuente está en el link, recuérdalo. Observa que hemos definido la utilidad getInitialColors para generar los colores. ¡Vamos bien! 🆗

⚙️ Configurando el <motion.div>

Bueno, por fin vamos a configurar el <motion.div> para que lo podamos arrastrar (dnd).

// Esto lo haremos en el map de <Grid> {colors.map((i, index) => ( <Item id={showInts ? i.id : undefined} savePosition={saveItemPosition} // ahora definimos esta función moveItem={moveItem} // y esta index={index} color={i.color} key={i.color} /> ))} // Y esto lo haremos dentro de Item return ( <motion.button layout children={id} ref={ref} drag dragSnapToOrigin style={{ backgroundColor: color }} transition={{ type: "spring" }} onDrag={(_, { point }) => { // Así obtenemos la posición del mouse moveItem(index, point); // moveItem es un prop }} tabIndex={0} whileHover={{ // esto nomás pa se mantenga encima al arrastrar zIndex: 10, scale: 1.1, boxShadow: "0px 3px 3px rgba(0,0,0,0.15)", }} /> );

Trabajar con Framer Motion es muy cómodo, pues en nuestro caso, solo tenemos que configurar el componente Item, gracias a las Layout animations de Framer Motion, podremos animar declarativa y automáticamente. 🦾🤖

🤯 Programando la lógica

Este paso puede ser un poco duro si algo no está bien de principio, pon mucha atención. Pero también en este paso es donde se encuentra la diversión, 🎭 y eso es a lo que vinimos: a divertirnos programando. 🤩

Así que vamos paso a pasito:

  1. Primero necesitamos un useEffect en el componente <Item> que nos permita guardar su posición en x, y de la pantalla, así como su width y height. ✅
const ref = useRef<HTMLButtonElement>(null); useEffect(() => { invariant(ref.current); const bounds = ref.current.getBoundingClientRect(); savePosition(index, { width: bounds.width, height: bounds.height, x: bounds.x, y: bounds.y, });

Recuerda que savePosition llega como prop.

  1. Ahora vamos a definir savePosition. Usaremos un ref para lograr almacenar todos estos datos de cada cubo sin renderizar ni una sola vez. 😎
// un tipo útil 🤤 así me decían en mi chamba... ok no, ya me despidieron U_U type Position = { width: number; height: number; x: number; y: number }; // Aquí guardaremos la referencia de las posiciones del <Grid> const positions = useRef<Position[]>([]); // Esta función vive en <Grid> y se le pasa como prop a <Item> const saveItemPosition = (index: number, position: Position) => { positions.current[index] = position; };
  1. Es hora de definir moveItem. Esta función será llamada en cuanto se hace grab del cubito y se invocará múltiples veces, buscando colisiones, hasta encontrar una o hasta que se suelte el cubo. 🧊
// pasan muchas cosas en esta función pero a grandes rasgos: // se checa la colisión con cada uno de los cubos usando // los datos width y height del cubo sujetado y el punto (x,y) del evento grab (mouse). const moveItem = (fromIndex: number, point: Point) => { for (let i = 0; i < positions.current.length; i++) { const targetPosition = positions.current[i]; if ( !isColliding( { ...positions.current[fromIndex], ...point }, targetPosition ) ) { continue; } if (fromIndex === i) return; swapColors(fromIndex, i); } };

Observa cómo aquí usamos nuestra clásica función isColliding e ignoramos si es el mismo cubo: (fromIndex===i) return;, para luego ¡hacer el swap con **swapColors. Veamos qué hace swapColors.

  1. Vamos entonces a intercambiar el cubo de color con el que ha chocado, todo esto mientras aún se está haciendo grab.
// moveArray está en utils.tsx: const moveArray = <T,>(a: T[], oldIndex: number, newIndex: number): T[] => { const array = [...a]; if (newIndex >= array.length) return array; const old = array.splice(oldIndex, 1)[0]; array.splice(newIndex, 0, old); return array; }; // const swapColors = (fromIndex: number, toIndex: number) => setColors(moveArray<Color>(colors, fromIndex, toIndex));

👀 Yo estoy programando mi propia función moveArray, por mera curiosidad intelectual. Pero, tú puedes usar una librería genial llamada array-move que nos ayuda a abstraer está lógica algo complicada para un principiante (es genial ser principiante).

¡Chingón! ¿Apoco no? Y “pus” ya. 🤯

Lo demás son solo estilos que puedes consultar en el código fuente que te he dejado en los enlaces. No lo he puesto aquí para no saturar los bloques de código. Espero esto te sea útil para practicar e intentar esta animación por tu cuenta.

🪅 Reto final:

No olvides usar la utilidad shuffle para mezclar al presionar el botón. ¿Cómo la usarías?

No dejes de decirme qué te parece este ejercicio en los comentarios, así me animo a crear más contenido similar. 💐

Abrazo. Bliss. 🤗

Enlaces relacionados

Link al CodePen

Biblioteca npm para mutar arrays

Video con colisiones en Tetris

banner

¿Quieres mantenerte al día sobre los próximos cursos y eventos?

Suscríbete a nuestro newsletter

Jamás te enviaremos spam, nunca compartiremos tus datos y puedes cancelar tu suscripción en cualquier momento 😉

robot logo saying hello
facebook icontwitter iconlinkedin iconinstagram iconyoutube icon

© 2016 - 2023 Fixtergeek