🖖 Inicio

SERVICIOS

Salir

nochedía

Desarrollo con css-flex

2000 palabras
8 minutos
May 18, 2020

JavaScript con css-flex

Para implementar el masonry con el css-flex lo que haremos será reproducir la lógica de este pen

Y esta estrategia se basa en fabricar una suerte de VIRTUAL VIRTUAL DOM para nosotros

Es decir

  • Tenemos el DOM, que es lo que vemos en el browser
  • Luego el VIRTUAL DOM, que es una representación del DOM y que es lo que modificamos con React (lo cual es mucho más eficiente)
  • Y aquí haremos un VIRTUAL VIRTUAL DOM, en pequeñito

O lo que es lo mismo, fabricaremos el grid en una estructura JavaScript, allí ordenaremos las celdas como tocan, y con esto modificaremos el VIRTUAL DOM

La ventaja? Que está todo muy ordenado

La desventaja? Que necesitamos leer el DOM y la función que utilizamos tiene un coste

En la época donde estamos este coste es perfectamente asumible, pero al final dependerá del tipo de aplicación que queramos construir

Otra ventaja es que al basarnos en flex, el responsive nos será mucho más sencillo

Bien

Para construir la estructura

  1. Primero tendremos que leer la altura de cada ítem
  2. Luego establecer el número de columnas
  3. Luego ir añadiendo cada ítem a la columna que en ese momento tenga menos altura
  4. Luego traspasar ese orden de elementos al css-flex con la propiedad order
  5. Y luego establecer el max-height que es lo que forzará al flex a generar las columnas

Y todo esto, claro, en React

Si quieres ir practicando con el código que voy poniendo, copia y pega el código en un code sandbox

Pero recuerda que necesitamos el css-in-js, y usaremos el @emotion

Por lo tanto, en el codesandbox:

  1. Crea una aplicación REACT

  2. Y una vez allí ves al botón de añadir dependencias Add Dependency y buscas emotion/core y emotion/styled, añades las dos y ya lo tendremos

Y con esto ya tenemos el envoltorio React

Empiezo por escribir una estructura de elementos para ordenarlos después

Pero claro, escribirlo uno por uno ... esto puedo hacerlo más programáticamente, esto es generar el html a partir de una estructura de datos que idealmente sacaré de algún archivo, pero que aquí simplemente lo genero y listos

Aprovecho para añadir el atributo alt en la imagen, que lo necesitamos para indicar a los programas que leen en voz alta de qué trata esa imagen (aunque en este caso lo dejo vacío, lo cual se recomienda si la imagen es meramente decorativa)

Mucho más conciso

Ahora me faltan las referencias, pero para qué las necesito?

Las necesito porque después necesitaré poder acceder a cada elemento, y cuando estamos en modo react necesitamos las referencias para acceder al VIRTUAL DOM

Si utilizáramos el DOM no necesitaría estas referencias, utilizaría alguna función de la API del DOM y ya lo tendria (que es lo que puedes ver en el pen de antes)

Pero como aquí no tocamos el DOM, pues eso

Y cómo lo hago?

Con el hook useRef()

Y Cómo se utiliza?

El funcionamiento simple es muy sencillo

useRef() te devuelve una referencia, y ésta la pones en el elemento html con ref={miRef} y listos

Pero esto funciona cuando tenemos sólo una referencia, pero y cuando queremos más?

Puede parecer una pregunta fácil, pero tiene su qué

Aparentemente ningún problema, si quieres 20 referencias le pasas un const refs = useRef([20]) y listos

Pero claro, cómo asignamos las referencias?

Con algo como esto

Así manual no es ni práctico ni nada, pero el problema no es ese, el problema es qué pasa cuando no sabemos cuántas referencias necesitaremos

20? 30? Es algo que depende del archivo que leemos?

Pero eso importa? (te podrías preguntar)

Sí, importa, y esto es porque useRef() es un hook

  • La primera regla de los hooks es no los puedes ejecutar cuando te dé la gana, sólo al principio

  • Y la segunda regla es no te olvides de la primera regla

No pasa nada, que no cunda el pánico

Es algo tan sencillo como asignar un array vacío al principio, y después cuando queramos asignarle dimensiones a ese array, pues se las asignamos, porque el hook ya lo habremos inicializado

Y con esto ya lo tendremos

Quizá estés tentado de probar lo siguiente:

Pero aquí estamos infringiendo la primera norma de los hooks, así que no

Aclarado esto, añado

Por qué no utilizamos un useState?

Esto podría ser equivalente?

No del todo, porque no tendríamos las referencias, pero podríamos crearlas con el createRef de React (de antes de que tuviésemos los hooks)

Entonces básicamente sí que serían equivalentes

*Aunque con useRef el valor se almacena en variable.current y con useState no hay .current que valga

Pues entonces qué? Si son equivalentes qué?

Son lo mismo excepto en una cosa

  • useState provoca un re-renderizado

  • useRef NO provoca ese re-renderizado

Y no queremos que cuando asignemos las referencias se nos repinte todo de nuevo, no lo necesitamos

Así que nos quedamos con el useRef, que además ya nos devuelve las referencias directamente

Total, que el código nos queda así

Y esta es la parte donde asignamos las referencias

Y qué es eso? No deberíamos tener algo tipo ref={miRef}?

Sí, pero también podemos pasarle una función, a lo que se le llama callback ref (documentación)

O podemos escribir la función allí mismo

o lo que es lo mismo, con ES6

Y esta función, cuando la asignamos a la propiedad ref pues recibe un argumento que es el propio elemento html (donde está el ref)

Y lo que hago aquí, aunque en lugar del console.log() de arriba le asigno el elemento html que recibo como argumento a la posición del array de referencias de antes

Para hacerlo me sirvo de i que es el índice del map() de antes, que me va de perlas

Con esto ya tenemos los elementos y las referencias listas para interactuar con ellas

Ahora nos falta la parte del VIRTUAL VIRTUAL DOM que decía antes

Es decir, crear la estructura de las celdas ya ordenadas, algo como esto

Y para cada celda almacenaremos el height

Para esto podemos aprovechar que ya tenemos nuestros elementos almacenados como referencias

Por lo tanto, lo primero es poner todas las celdas en la primera columna

El problema? Que nuestras imágenes tardan un tiempo en cargarse, con lo que cuando ejecutamos este código, nuestro height es de 2px que representan el border que le hemos puesto

Es decir, no tenemos imágenes, y por lo tanto el .getBoundingClientRect().height nos da cero

Solución?

Poner un evento onLoad a cada imagen, y que esta llame a la función createStructure

Y si tenemos 300 imágenes? Tendremos 300 llamadas?

Podemos esperar a la número 300 y entonces llamar a la función

Y si hay un error y se cargan sólo 299? Y si tardan 10 minutos en cargarse?

Vale, pues podemos hacer 300 llamadas pero implementar un buffer para que las llamadas que estén muy juntas en el tiempo se descarten

Bien, y qué mas?

Pues una vez tengamos la estructura creada, lo suyo es ir poniendo los ítems en función de la altura de las columnas

Pero para eso necesitamos saber el número de columnas

Y para saber esto necesitamos saber la anchura de la columna

Y cómo definimos esa anchura? Pues necesitamos ponerle un número, y luego con el width del contenedor podemos calcular cuantas columnas nos caben

Todo esto lo tienes en el siguiente código

Fíjate que también incluyo el maxHeight, que es una variable (con useState) que definirá eso mismo (el max-height) pero que de momento no está implementado (más allá de definir la variable y utilizarla aunque esté vacía)

Bien

Lo único que no he explicado es esto

Aquí utilizo el useMemo (otro hook)

Para qué?

El problema es que a cada repaint se vuelven a pedir nuevas imágenes, por lo que estas van cambiando constantemente

Lo digo con otras palabras

Cada vez que una imagen se carga, llama a la función updateGrid

Pues esta función provoca un repaint, con lo que vuelven a renderizarse los componentes <Image>, y con esto vuelven a pedirse nuevas imágenes, con lo que vuelven a llamarse el updateGrid otra vez creando un bucle infinito

Solución rápida?

Con el useMemo() lo que hago es forzar a que React recuerde esa función y nos devuelva siempre el mismo elemento

Solución mejor?

Pues desde mover el Math.random fuera y calcularlo sólo una vez dentro del useEffect, hasta generar el array de imágenes al principio

De momento lo dejamos así básicamente porque en una aplicación real lo suyo es que las imágenes no sean random, con lo que este problema no existirá

Y qué toca ahora?

Pues ahora ya tenemos la estructura de las columnas con las celdas ordenadas

Ahora nos falta traspasar esto al css-flex

Cómo?

Si no utilizáramos el css-flex, ahora lo que podríamos hacer es simplemente rellenar estructuras tipo esto

Y si virtual_structure es un useState, ya tendríamos una aproximación no basada en css-flex, con sus ventajas e inconvenientes

Pero si aquí queremos utilizar el css-flex entonces tenemos que establecer un max-height y también ajustar el orden, lo cual hacemos con la propiedad order

Y falta una última cosa

Y es que para que esto funcione (con css-flex) necesitamos que la altura del último elemento de cada columna ocupe todo el espacio posible

Por qué?

Porque si allí cabe un elemento (via css) pero en nuestra virtual_structure ese elemento no tiene que ir allí, no podremos evitarlo y todo se descuadrará

Para evitarlo necesitamos extender la altura del último elemento para asegurarnos que esa columna está en efecto llena

Y eso lo hacemos con el flex-basis, calculamos el espacio vacío que queda en esa columna y se lo pasamos con flex-basis

Casi lo tenemos, pero de momento el resultado es bastante caótico

Qué está fallando?

Pues el típico (tipiquísimo) error de no tener en cuenta de que el array empieza por 0, y por lo tanto con

Está creando un array con un elemento de más

Si lo corrijo

Entonces ya me cuadran las columnas

Eso sí, la numeración sigue siendo un caos

Por qué?

Pues porque para leer el height de cada elemento, no estoy teniendo en cuenta los espacios margin

Y los padding? Pues depende

Depende de la propiedad css box-sizing

Por defecto es box-sizing: content-box; y esto implica que cuando leamos el height y el width esto incluirá border y padding

Con box-sizing: border-box; sería lo mismo, pero hay diferencias, aquí no aplican pero puedes leerlas aquí

Total, que tenemos que incluir el margin

Y además le añado un par de cosas

  • Añado un evento para que cuando se redimensione la ventana se vuelva a ejecutar la función para recalcularlo todo (si el número de columnas ha cambiado)

  • Cambio el useEffect por useLayoutEffect, que hace lo mismo con la única diferencia de que el código se ejecuta antes de que el DOM se haya actualizado, por lo que ganamos algo de eficiencia

Y ahora sí, ya nos funciona 🙌🙌🙌👏

Y si quieres que la imagen ocupe todo el ancho posible, puedes hacer dos cosas

O añadir width: -webkit-fill-available; en la imagen, y así se te alargará

O puedes modificar el código para tener la imagen como background-image con lo que tendrás más margen

O puedes poner en el container align-content: center; con lo que el espacio entre ítems no variará (pero te quedará un margen en los extremos derecho y izquierdo)

Estamos ya? No, hay un último error

Dónde?

Cada vez que se calcula la estructura, se establece el flex-basis del último elemento de la columna

Esto provoca que esa columna quede ya cerrada y no reciba ya más ítems con lo que en función de la suerte pueden quedar estructuras extrañas, con una distribución que no está bien (aunque puede gustar igual)

image

Para solucionarlo, podemos simplemente resetear ese flexBasis al principio ya que al final lo volveremos a establecer

Y listos, pero aún podemos mejorarlo ...

En hook

Ahora ya estaríamos, sólo que podemos mejorar el código haciéndolo más funcional

Y esto en REACT quiere decir convertirlo en un hook

Esto no es más que encapsularlo para hacer que su uso sea fácil en el sentido de poderse olvidar de casi todo y simplemente disfrutar de su funcionalidad

Dicho y hecho

Y ahora sí, terminado 💪

Bueno, faltaría separar el hook en un nuevo archivo, pero esto ya es trivial

Qué es lo que no me gusta de esta solución?

  • Que el control de los espacios es complicado
  • Que en general el control de la estética es complicado
  • Que el tamaño de la columna va fijado y no se adapta
  • Que cuando añado las imágenes en lazy loading, el efecto no es especialmente estético (lo puedes mirar en la demo del theme que te comentaba al principio)

Por todo esto yo personalmente me quedo con la opción que vemos en el siguiente capítulo

Acabamos de ver cómo implementar la técnica masonry aplicando el css-flex y ajustando el alto del contenedor y cambiando el orden de los elementos con JavaScript

Lo bueno de esta técnica es que utiliza la distribución flex aprovechando por lo tanto la rapidez del procesador css

Dicho esto, yo prefiero la estrategia que vemos en el siguiente capítulo, la que utiliza el css-grid

Nos vemos en el tercer capítulo, en Diseño Grid

Qué tal este capítulo?
👌 Bien 🙌🙌
👍 Bien, pero algunas cosas podrían explicarse mejor 😬
🤷‍♂️ Da por sentadas demasiadas cosas 😒
🤷‍♂️ A ver, hay poca chicha 😬
🤷‍♂️ Los ejemplos no son muy claros 🙇‍♂️
🤷‍♂️ No se entiende, está mal escrito 👎
✍️ Hay errores, revísalo en cuanto puedas 🙏

Newsletter de kuworking, un correo de tanto en cuanto

Quizá te interese

Privacidad
by kuworking.com
[ 2020 >> kuworking ]