Creando un Bloque con InnerBlocks de Antes y Después para WordPress

En este post, voy a documentar la creación de un miniplugin o «plugin monobloque» para WordPress. El objetivo es practicar habilidades de desarrollo creando un bloque con dos imágenes y un slider para mostrar un antes y después. Aunque existen soluciones similares, aquí busco hacerlo de forma rápida y simple utilizando InnerBlocks.

Instalación y Configuración Inicial

Para empezar, creamos nuestro plugin utilizando @wordpress/create-block en un entorno de pruebas dentro del directorio wp-content/plugins:

npx @wordpress/create-block@latest beforeAfterImages
cd beforeafterimages 
npm start

Este comando genera la estructura base del plugin con un bloque estático. Luego, habilitamos el plugin en WordPress, creamos un post o página y agregamos el nuevo bloque que se llama «Before After Images» y se va a ver mas o menos así:

Configuración del Bloque en el Editor

En src/beforeafterimages/edit.js, importamos useInnerBlocksProps y definimos nuestro bloque:

import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';

export default function Edit() {
  const blockProps = useBlockProps();
  const innerBlocksProps = useInnerBlocksProps(blockProps, {
    allowedBlocks: ['core/image'],
    template: [['core/image'], ['core/image']],
    templateLock: 'all',
  });

  return <div {...innerBlocksProps}></div>;
}

Aquí definimos que bloques permite: allowedBlocks: ['core/image'] y que contendrá dos imágenes fijas: template: [['core/image'], ['core/image']], y bloqueamos el template con: templateLock: 'all' para evitar la posibilidad de agregar más imágenes o eliminar alguna.

Nuestro bloque en el editor se va a ver de la siguiente manera:

Guardando el Bloque

En src/beforeafterimages/save.js, usamos InnerBlocks.Content para guardar el contenido del bloque:`

import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';

export default function save() {
  return (
    <div {...useBlockProps.save()}>
      <InnerBlocks.Content />
    </div>
  );
}

Esto garantiza que las imágenes seleccionadas en el editor se guarden en el contenido y sean mostradas en el frontend.

Implementando el Slider

Para estructurar el slider, modificamos src/beforeafterimages/save.js:

export default function save() {
  return (
    <div {...useBlockProps.save()}>
      <div className="container">
        <div className="image-container">
          <InnerBlocks.Content />
        </div>
        <input type="range" min="0" max="100" value="50" className="slider" />
        <div className="slider-line"></div>
        <div className="slider-button"></div>
      </div>
    </div>
  );
}

Estilos en src/beforeafterimages/style.scss

.wp-block-create-block-beforeafterimages {
    display: grid;
    place-items: center;
    *,
    *::after,
    *::before {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    .container {
        display: grid;
        place-content: center;
        position: relative;
        overflow: hidden;
        border-radius: 1rem;
        --position: 50%;
    }

    .image-container {
        max-width: 800px;
        max-height: 90vh;
        aspect-ratio: 1/1;

        figure {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            display: flex;
            img{
                width: 100%;
                height: 100%;
                object-fit: cover;
                object-position: left;
            }
            figcaption{
                position: absolute;
                bottom: 0;
                background-color: rgb(210, 210, 210);
                width: 100%;
                display: block;
                overflow: hidden;
                text-align: left;
		text-indent: 10px;
            }

            &:first-child {
                position: absolute;
                inset: 0;
                width: var(--position);
                figcaption{
                    z-index: 10;
                }
            }
        }
    }
    .slider {
        position: absolute;
        inset: 0;
        cursor: pointer;
        opacity: 0;
        /* for Firefox */
        width: 100%;
        height: 100%;
    }

    .slider:focus-visible ~ .slider-button {
        outline: 5px solid black;
        outline-offset: 3px;
    }

    .slider-line {
        position: absolute;
        inset: 0;
        width: .2rem;
        height: 100%;
        background-color: #fff;
        left: var(--position);
        transform: translateX(-50%);
        pointer-events: none;
    }

    .slider-button {
        position: absolute;
        background-color: #fff;
        color: black;
        padding: .5rem;
        border-radius: 100vw;
        display: grid;
        place-items: center;
        top: 50%;
        left: var(--position);
        transform: translate(-50%, -50%);
        pointer-events: none;
        box-shadow: 1px 1px 1px hsl(0, 50%, 2%, .5);
    }
}

JavaScript para el Frontend en src/beforeafterimages/view.js

const container = document.querySelector('.container');
document.querySelector('.slider').addEventListener('input', (e) => {
  container.style.setProperty('--position', `${e.target.value}%`);
})

Mejorando la Vista en el Editor

Creamos src/beforeafterimages/slider.js para reutilizar la estructura del slider:

import React from 'react';

const Slider = () => (
    <>
        <div class="slider-line" aria-hidden="true"></div>
        <div class="slider-button" aria-hidden="true">
            <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" viewBox="0 0 256 256">
                <rect width="256" height="256" fill="none"></rect>
                <line x1="128" y1="40" x2="128" y2="216"   fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" ></line>
                <line x1="96" y1="128" x2="16" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" ></line>
                <polyline points="48 160 16 128 48 96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" ></polyline>
                <line x1="160" y1="128" x2="240" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" ></line>
                <polyline points="208 96 240 128 208 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" ></polyline>
            </svg>
        </div>
    </>
);

export default Slider;

Luego, importamos y usamos <Slider /> en src/beforeafterimages/edit.js y en src/beforeafterimages/save.js:

import Slider from './slider';

return (
  <div {...blockProps}>
    <div className="container">
      <div {...innerBlocksProps} className="image-container"></div>
      <Slider />
    </div>
  </div>
);

Finalmente, ajustamos src/beforeafterimageseditor.scss para mejorar la vista en el editor.

.wp-block-create-block-beforeafterimages {
  display: block;
  min-height: auto;
  .container {
    display: block;
    position: static;
    overflow: hidden;
    border-radius: 0;
    --position: 0;
    width: 100%;
    .image-container {
      display: grid;
      grid-template-columns: 1fr 1fr;
      max-width: 100%;
      max-height: 100%;
      aspect-ratio: auto;
      figure {
        display:block;
        &:first-child {
          position: relative;
          inset: 0;
          width:100%;
        }
      }
    }
  }
  .slider-button,
  .slider-line {
    left:50%;
  }
}

Con esto, hemos creado un bloque funcional con un slider de antes y después. ¡Ahora podemos seguir refinando y optimizando el código!

Por ejemplo podemos modificar el javascript para poder insertar mas de un slide en la misma pagina:

document.addEventListener('DOMContentLoaded', () => {
    const containers = document.querySelectorAll('.container');
    const sliders = document.querySelectorAll('.slider');

    containers.forEach((container, index) => {
        const slider = sliders[index];
        if (slider) {
            slider.addEventListener('input', (e) => {
            container.style.setProperty('--position', `${e.target.value}%`);
            });
        }
    });
});

Acá lo pueden ver funcionando:

Antes
Después

Fuentes