DÍA 11 / 2016

js(uego)css: Jugando con CSS en JavaScript

Jugaremos a re-crear CSShake, una librería de animaciones CSS vibrantes, en React.


Se está hablando (mucho) de css-in-js y probablemente bajo muchos puntos de vista y con absoluta razón, parezca una cosa inútil, un obstáculo para la _cascada_, una sobrecarga a un lenguaje que ya hace bien lo que tiene que hacer, etc etc.

De todas formas, considero que "css-in-js", ya sean inlineStyles como CSSModules, PostCSS o el approach elegido, sean un gran recurso a la hora de crear, mantener o publicar, un proyecto de mediana/grande escala basado en JavaScript.

No es el objetivo del artículo crear opuestos en absoluto. Toda opinión e implementación en este gran amplio mundo del web son siempre válidos. La idea es simplemente jugar, jugar con css en js.

Componente 1.0

La idea principal es generar un Componente React, para poder aplicar las animaciones con flexibilidad en diversos contextos.

Iniciaremos el proyecto con el fabuloso recurso create-react-app, el cual instala el mínimo indispensable para crear un proyecto React en cuestión de segundos.

Seguramente viendo el código revisado hace ya un tiempo me dará ganas de mejorar (agregar o, sobre todo quitar) cosillas y funcionalidades, pero haremos el esfuerzo de copiar y pegar el código simplemente reescribiéndolo en JavaScript.

En un archivo llamado Shake.js crearemos la base para el componente <Shake /> que aceptará ciertas props, aplicando un valor default en cada una. Usando como base en el mixin @do-shake:

import React from 'react'

const Shake = ({
    h = 5,
    v = 5,
    r = 3,
    dur = 300,
    q = 'infinite',
    tf = 'ease-in-out',
    int = 10,
    max = 100,
    orig = 'center center',
    ...props,
}) => {
    const styles = {};
    return (
        <div style={styles}>
            { props.children }
        </div>
    );
};

export default Shake;

Visto que no necesitamos ningún método de lifecycle, <Shake /> es un Stateless Component con los siguientes parámetros:

Prop Desc Type Default Result unit
h Movimiento máximo en el eje horizontal Number 5 px
v Movimiento máximo en el eje vertical Number 5 px
r Rotación máxima Number 5 deg
dur Duración del ciclo completo de la animación Number 300 ms
q Cantidad de iteraciones Number String 'infinite'
tf CSS animation-timing-function String 'ease-in-out'
int Intervalo entre los porcentajes de los @keyframes. Es una especie de control fino de la animación. Number 10 %
max Valor máximo de @keyframes. Si este valor es diverso de 100, permite generar una pausa en la animación. Number 100 %
tf CSS transform-origin String 'center center'

* En la tabla se expone una breve descripción de cada parámetro, el tipo de valor aceptado, el valor default declarado y, solo en algunos casos, en qué unidad CSS se transformará ese valor.

Aleatorizando

En algún punto, el core de la librería son los valores aleatorios generados en base a los parámetros recibidos. Entonces nos servirá una función helper que devolverá valores random (negativos o positivos) en función del argumento recibido.

const random = (max, min = 0) => {
    return (Math.random() * (max - min) - max / 2);
};

Es hora de comenzar a generar los steps o @keyframes de la animación:

const doKeyframes = () => {
    // el objecto que iremos llenando
    let kf = {};
    const init = 'translate(0,0) rotate(0)';

    // loop con intervalos basados en `int`
    for (let st = int; st <= max; st += int) {
        // Numeros aleatorios en cada keyframe
        const x = random(h);
        const y = random(v);
        const rot = random(r);

        kf[`${st}%`] = {
            transform: `translate(${x}px, ${y}px) rotate(${rot}deg)`,
        }
    }

    // Init de las transformaciones en 0% y 100%
    kf[`0%`] = kf[`100%`] =  {
        transform: init,
    }

    // Init también en caso el `max` < 100
    if (max < 100) {
        kf[`${max}%`] = {
            transform: init,
        }
    }

    return kf;
};

// Creamos los `@keyframes`
const keyframes = doKeyframes();

código completo hasta aquí

Eso es todo. Recibimos los valores de las propiedades del Componente <Shake />, los procesamos con la función doKeyframes() la cuál usa random() para generar números aleatorios en cada step y almacenamos el Objeto con los @keyframes en la variable keyframes (o constante?).

DOMingo

Ok, todo procesado pero el elemento en el DOM no recibe aún los estilos. Atención! Las próximas lineas de código que escribiremos se autodestruirán en 5 segundos. O sea, escribiremos un poco de código que sirve exclusivamente para hacer funcionar la animación base sin dependencias. Luego, nos ayudará en varios aspectos la librería aphrodite.

Comenzamos transformando el Object keyframes en un String con la ayuda del gran amigo reduce():

const keyframesString = Object.keys(keyframes).reduce((acc, next) {
    return `${acc} ${next} {
        transform: ${keyframes[next].transform};
    }`;
}, '');

Necesitaremos un nombre único para la animación y así evitar conflictos en caso de tener varias conviviendo en el mismo documento:

const name = `anim-${Date.now()}`;

Por último creamos la regla CSS y la inyectamos en el DOM:

const styles = `@keyframes ${name} {
    ${keyframesString}
}`;

let styleSheet = document.styleSheets[0];

styleSheet.insertRule(styles, styleSheet.cssRules.length);

Queda nomás aplicar los estilos al <div /> que devuelve el Componente <Shake />:

//...

    return (
        <div style={{
            display: 'inline-block',
            animationName: name,
            animationDuration: `${dur}ms`,
            animationIterationCount: q,
            transformOrigin: orig,
        }}>
            { props.children }
        </div>
    );

//...

Para testearlo creamos un Componente en App.js:

import Shake from './Shake';

// ...

<Shake>
    &lt;Shake /&gt;
</Shake>
<Shake h={0} v={100} r={0} dur={1000} int={3}>
    &lt;Shake custom /&gt;
</Shake>

aquí el código completo

See the Pen <Shake /> [1] by Lionel T (@elrumordelaluz) on CodePen.

Componente 2.0

Están faltando una serie de parámetros que ayudarán a rendir más interactivo el Componente. Comenzamos por agregar estas props junto con el resto:

const Shake = ({
    // la `props` anteriores
    fixed = false,
    freez = false,
    active = true,
    trigger = ':hover',
    fixedStop = false,
    ...props,
}) => {
// el resto de la función
Prop Desc Type Default
fixed Animación fija Boolean false
freez Pausa en la animación al interactuar Boolean false
active Habilitación general de las animaciones Boolean true
trigger CSS pseudo-class que lanza/frenta la animación String true
fixedStop Permite frenar la animación con el trigger cuando la animación es fixed String false

Es hora de instalar aphrodite desde el Terminal:

npm i --save aphrodite

Importamos el Objeto StyleSheet y la función css en el archivo Shake.js. Esta librería agrega por default la declaración !important en cada regla para evitar conflictos con código ya existente. A nosotros esta cosa no nos interesa por lo cuál utilizaremos la versión /no-important:

import { StyleSheet, css } from 'aphrodite/no-important';

A esta altura podemos eliminar keyframesString, name, document.styleSheets[0] e insertRule.

Creamos una variable que contiene los atributos base para lanzar la animación:

const animAttrs = {
    animationName: keyframes,
    animationDuration: `${dur}ms`,
    animationIterationCount: q,
};

Y a la variable styles le asignamos la hoja de estilo creada mediante StyleSheet.create():

const styles = StyleSheet.create({
    base: {
        display: 'inline-block',
        transformOrigin: orig,
    },
    shake: {
        ...animAttrs,
    },
    triggered: {
        [trigger]: {
            ...animAttrs,
        },
    },
    freez: {
        animationPlayState: !fixed ? 'paused' : 'running',
        [trigger]: {
            animationPlayState: !fixed ? 'running' : 'paused',
        },
    },
    init: {
        [trigger]: {
            animation: 'initial',
        },
    },
});

Resta solo aplicar la declaración en el elemento, mediante la función css(). Notese que no nos preocupamos más por el nombre de la animación, ni por agregar la palabra @keyframes a la regla CSS, de eso y otras cosas se está encargando Afrodita.

Para simplificar las próximas condiciones nos creamos un par de variables:

const shouldShakeDefault = fixed || (!fixed && freez);
const shouldShakeWhenTriggered = !fixed && !freez;

Y las aplicamos a la variable className:

const className = css(
    shouldShakeWhenTriggered && styles.triggered,
    shouldShakeDefault && styles.shake,
    freez && styles.freez,
    fixed && fixedStop && styles.init,
);

En base a las condiciones, se aplicará la clase correspondiente (con un nombre único): triggered, shake, freez o init.

Asignamos las clases generadas en el elemento:

// ...el resto de la función
return (
    <div className={className}>
        { props.children }
    </div>
);

Como en la versión anterior del componente, lo podremos declarar con sus defaults o pasarle valores específicos a las propiedades:

<Shake>
    &lt;Shake /&gt;
</Shake>
<Shake fixed>
    &lt;Shake fixed /&gt;
</Shake>
<Shake freez>
    &lt;Shake freez /&gt;
</Shake>
<Shake fixed freez>
    &lt;Shake fixed freez /&gt;
</Shake>
<Shake fixed fixedStop>
    &lt;Shake fixed fixedStop /&gt;
</Shake>

aquí el código completo

Detalles

Agregaremos solo unos últimos detalles. Usaremos classnames para asignar las eventuales classes pasadas al Componente a través de la propiedad className y para asignar las propiedades de animaciones solo en el caso la propiedad active sea true. Instalamos el paquete:

npm i --save classnames

Luego lo incluimos y lo utilizamos en Shake.js . También agregamos style={props.style} en el caso se pasen estilos en línea al Componente.

import classNames from 'classnames';

// el resto de la función
return (
    <div
        style={props.style}
        className={classNames(props.className, css(styles.base), {
            [className]: active
        })}>
        { props.children }
    </div>
);

Así podremos tener un Componente con los siguientes atributos, además de los relativos al Shake:

<Shake
    className="my-custom-class"
    style={{ color: 'deepskyblue' }}>
        &lt;Shake /&gt;
</Shake>

aquí el código actualizado

See the Pen <Shake /> [2] by Lionel T (@elrumordelaluz) on CodePen.

Un último detalle podría ser el de agregar elem como propiedad, la cual va a definir en qué elemento se renderiza el Componente. Esta cosa puede ser útil por ejemplo, si se quiere lanzar la animación on :focus de un <input />.

const Shake = ({
    //...
    elem = 'div',
    ...props,
}) => {
// ...
    const Elem = elem;
    <Elem
      style={props.style}
      className={...}
      { ...props }>
      { props.children }
    </Elem>
}

Dos avisos a tener en cuenta:

  • No estamos haciendo ningún check en la propiedad elem. Será responsabilidad de quien usa el Component, agregar children solo en los elementos que lo permitan, al menos por ahora (pull requests bienvenidas).

    Así se evitarán errores de React y logs como Uncaught Invariant Violation: input is a void element tag and must neither have children

  • Estamos pasando absolutamente todas las propiedades { ...props } ya que no sabemos qué Elemento se elegirá y los atributos deseados. Simplemente, no es ideal pero en este caso sería necesario.
<Shake
    elem="input"
    trigger=":focus"
    type="email"
    placeholder="lorem@ipsum.com" />

Aunque no logro imaginarme un posible uso real, aquí el pen con la implementación en funcionamiento:

See the Pen <Shake /> [3] by Lionel T (@elrumordelaluz) on CodePen.

Playground

Ahora podemos usar el Componente <Shake /> con las propiedades que queramos o con aquellas default, pero también (y aquí empieza lo divertido) podemos controlarlas desde un Componente parent. Juguemos pues con los atributos en nuestro Playground.
Todas los valores que van cambiando en los inputs pasan directamente al Componente <Shake />, cuando React detecta el cambio realiza un re-rendering.

Shaking as...

Por último, como se ofrece en CSShake, podemos crear mini Componentes que se limitan a hacer solo una animación, sin demasiadas props en las que pensar.

Crearemos un Componente llamado <Shaking /> el cual nos servirá para generar los otros:

import React from 'react';
import Shake from './Shake';
import shakes from './shakes';

const Shaking = ({
  type = null,
  ...props,
}) => {
  const attrs = type && shakes[type];
  return (
    <Shake { ...attrs } { ...props }>
      { props.children }
    </Shake>
  );
};

export default Shaking;

Cargamos los defaults de cada animación de un archivo shakes.js que contiene un Objeto similar a esto:

const shakes = {
  little: {
    h: 2,
    v: 2,
    r: 2,
  },
  slow: {
    h: 20,
    v: 20,
    r: 7,
    dur: 5000,
  },
  hard: {
    h: 20,
    v: 20,
    r: 7,
  },
  // ...
};

export default shakes;

Luego creamos cada Componente específico así:

export const ShakeLittle = props => <Shaking {...props} type="little" />
export const ShakeSlow = props => <Shaking {...props} type="slow" />
export const ShakeHard = props => <Shaking {...props} type="hard" />
// ...

Reshake

Todo esto se puede consultar en el su repositorio y asi mismo usarlo instalando el paquete reshake:

npm install --save reshake

El resto, de cómo usarlo y personalizarlo ya lo sabéis...

Aviso importante: usar con moderación. Buenas vibraciones!