ezakto code

Los contenidos de este blog están desactualizados.

Pero estoy pensando en actualizarlos y escribir más.

Si te interesa, dejame tu email (no spam, no newsletters). Si hay suficiente interés me pondré manos a la obra y te lo haré saber!


Tutorial React Extra: Masonry layout

27 de junio 2015

"Apilando" de forma compacta las notas

Una característica muy de moda y que hace a la aplicación más visualmente atractiva es el masonry layout que consiste en ajustar la posición de cada recuadro de forma tal que no queden espacios vacíos entre ellos. Es ampliamente usado, por ejemplo en Pinterest o en el propio Google Keep.

Nota: Voy a dar por hecho que se está utilizando el style.css de ejemplo.

Antes

masonryoff

Después

masonryon

Implementar este diseño en nuestra aplicación es relativamente sencillo y requiere de un combo de JavaScript + CSS.

Utilizaremos javascript para realizar cálculos sobre el posicionamiento de cada nota en la grilla, ya que deberán posicionarse de forma absoluta, en nuestro caso con left y right.

Posicionamiento absoluto

Es necesario que el elemento principal que contiene las notas pueda contener a las notas posicionadas de forma absoluta. En CSS, .grid debe tener position: relative;. Por otro lado, .note debe tener position: absolute;.

Ahora todas las notas apareceran superpuestas en la esquina superior izquierda. Tenemos que calcular la posicion individual de cada una. El método que utilizaremos será el siguiente:

Antes de proceder con este algoritmo, vamos a agregar unos métodos útiles a Note.

Note.js

Añadimos el método setPosition(x, column) que establecerá las los estilos left y top de la nota de forma dinámica.

    setPosition: function(top, column) {
        var element = React.findDOMNode(this);
        element.style.top = top + 'px';
        element.style.left = (column * 25) + '%';
    },

El código es extremadamente sencillo. Primero obtenemos el elemento DOM correspondiente al componente montado y luego le establecemos los estilos. Nótese que left es porcentual. Se espera que el argumento column sea 0, 1, 2 o 3. top es el valor fijo en pixels.

Ahora, por comodidad, añadimos el método getHeight() que nos devolverá la altura del elemento DOM.

    getHeight: function() {
        var element = React.findDOMNode(this);
        var computedStyle = window.getComputedStyle(element);
        var height = computedStyle.getPropertyValue('height');
        return parseInt(height);
    },

Es un poco más escandaloso a simple vista, pero es bastante sencillo también. Primero obtenemos el elemento DOM. Luego obtenemos sus estilos aplicados con window.getComputedStyle() que nos devolverá una colección de reglas. Obtenemos la altura (height) del elemento con getPropertyValue() y lo devolvemos asegurándonos de que sea un entero listo para operar.

Grid.js

Para posicionar y obtener la altura de cada nota con los métodos que acabamos de crear, será necesario acceder a cada componente Note una vez que estén montados, por lo que será necesario obtener referencias a cada uno, usando las refs de React. En el método render, agregamos un atributo ref a los componentes Note que construimos en el ciclo map(), usando la id del componente para asignarle un valor único y predecible:

        var notes = this.props.notes.map(function(note, idx){
            return (
                <Note id={note.id} title={note.title} text={note.text} key={note.id} |||ref={'note-'+note.id}||| />
            );
        });

A continuación agregamos un nuevo método, digamos calculatePositions(), donde implementaremos el algoritmo de determinación de posiciones con el siguiente código (autoexplicado):

    calculatePositions: function() {
        // Array de alturas por columna, inicializadas en 0
        var grid_heights = [0, 0, 0, 0];

        // Recorremos todas las notas
        for (var i = 0, l = this.props.notes.length; i < l; i++) {
            // Obtengo el componente de esta nota por medio de su referencia
            var note = this.refs['note-'+this.props.notes[i].id];

            // Aplicamos un algoritmo sencillo de determinación de menor
            var min = grid_heights[0]; // Acumulador, asumimos que la menos alta es la primera
            var min_col = 0; // Índice de la menos alta

            // Recorremos cada columna
            for (var col = 1; col < grid_heights.length; col++) {
                // Si la columna actual tiene menor altura que la anterior guardada, la guardo
                if (grid_heights[col] < min) {
                    min = grid_heights[col];
                    min_col = col;
                }
            }

            // Posicionamos la nota en el menor altura obtenida, y en su columna correspondiente
            note.setPosition(min, min_col);

            // Sumamos la altura de esta nota a la columna, para la próxima iteración
            grid_heights[min_col] += note.getHeight();
        }
    },

Finalmente, tenemos que ejecutar calculatePositions() cada vez que las notas cambien (por la razón que sea). Para esto, usamos el método especial componentDidUpdate(). Este método de ciclo de vida se ejecutará inmediatamente después de cada ejecución de render() que de como resultado un cambio en el DOM. Por lo tanto, cada vez que las notas se alteren, el state de Grid se actualizará, provocando que render() se ejecute y por consiguiente, este método, componentDidUpdate().

Pero también queremos que se posicionen las notas la primera vez que se ejecuta render() (no "cambios" en el DOM ya que no habría DOM previo). Para eso también ejecutaremos los calculs en el ya conocido componentDidMount().

Entonces, agregamos ambos métodos al componente:

    componentDidMount: function() {
        this.calculatePositions();
    },

    componentDidUpdate: function() {
        this.calculatePositions();
    },

Listo! Masonry layout implementado. Compilamos los archivos y probamos.

Si queremos hacerlo un poco más fancy, podemos agregar transiciones de CSS a las propiedades top y left de .note para que cada vez que dichas propiedades cambien, las notas se reorganizen por medio de una suave animación. Algo como transition: top 0.5s, left 0.5s; en .note bastará. Aquí está el style.css de ejemplo actualizado.

Índice Siguiente: Reestructurando para utilizar Flux