Carrusel con Interactivity API e InnerBlocks

Continuando con esta serie sobre bloques y la Interactivity API, vamos a agregar más complejidad y explorar las opciones que esta nueva herramienta nos ofrece. En este caso, crearemos un carrusel utilizando el bloque core/gallery y la Interactivity API para gestionar la navegación en el frontend.

Creación del bloque

Nuevamente creamos nuestro bloque con @wordpress/create-block, en la carpeta wp-content/plugins:

npx @wordpress/create-block Carousel --template @wordpress/create-block-interactive-template
cd carousel
npm start

Uso de InnerBlocks

Utilizaremos el bloque core/gallery en el editor. Idealmente, no necesitaremos realizar modificaciones en el editor, ya que nos enfocaremos en el frontend con la Interactivity API. El uso de InnerBlocks nos simplifica esta parte, ya que obtenemos un bloque completamente funcional y podemos aprovechar todas las opciones de los bloques de core.

En el archivo src/edit.js, agregamos lo siguiente:

import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
	const blockProps = useBlockProps();
	const innerBlocksProps = useInnerBlocksProps( blockProps, {
		allowedBlocks: [ 'core/gallery' ],
		template: [
			[ 'core/gallery' ],
		],
	} );

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

Aquí importamos useInnerBlocksProps para poder utilizarlo y definimos las propiedades allowedBlocks y template, configurándolas con 'core/gallery'. Luego, lo aplicamos en el return del componente.

Guardado de contenido

Al igual que en el ejemplo anterior, agregamos la función save para almacenar las imágenes seleccionadas en el contenido. En src/save.js:

import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
export default function save() {
	return (
		<div { ...useBlockProps.save() }>
			<InnerBlocks.Content />
		</div>
	);
}

y en src/index.js:

import save from './save';
registerBlockType( metadata.name, {
	/**
	 * @see ./edit.js
	 */
	edit: Edit,

	save,
} );

Hasta acá, nada nuevo.

Frontend

Ahora viene la parte divertida: estructurar el render.php para mostrar nuestro carrusel:

<div
	<?php echo get_block_wrapper_attributes(); ?>
	data-wp-interactive="create-block"
	<?php echo wp_interactivity_data_wp_context( array( "slideIndex" => 0 ) ); ?> >
	<section class="slider-wrapper">
		<button class="slide-arrow prev" data-wp-on--click="actions.prevSlide">
			&#8249;
		</button>
		
		<button class="slide-arrow next" data-wp-on--click="actions.nextSlide">
			&#8250;
		</button>
	
		<ul class="slides-container" data-wp-watch--update="callbacks.updateSliderPosition">
			<?php 
				$gallery = $block->parsed_block['innerBlocks'][0]['innerBlocks'];
				foreach ( $gallery as $i => $slide ) { ?>
					<li class="slide">
						<?php echo $slide['innerHTML']; ?>
					</li>
				<?php
				}
			?>
		</ul>
	</section>
</div>

Acá definimos la estructura del carrusel, los botones de navegación y la lista con las imágenes de la galería. Para acceder a las imágenes, utilizamos la variable expuesta $block y mostramos su innerHTML directamente.

Estilos

Estos son los estilos que aplicaremos al carrusel. No entraremos en detalles, ya que están explicados en el tutorial en el que me basé, con algunas modificaciones menores:

.wp-block-create-block-carrousel {
	.slider-wrapper {
		margin: 1rem;
		position: relative;
		overflow: hidden;
	}
	.slides-container {
		height: calc(100vh - 2rem);
		width: 100%;
		display: flex;
		list-style: none;
		margin: 0;
		padding: 0;
		overflow-x: scroll;
		overflow-y: hidden;
		scroll-behavior: smooth;
		scrollbar-width: none; /* Firefox */
		-ms-overflow-style: none;  /* Internet Explorer 10+ */
		&::-webkit-scrollbar { 
			width: 0;
			height: 0;
		}
	}
	.slide {
		width: 100%;
		height: 100%;
		flex: 1 0 100%;
		display: flex;
    		align-items: center;
		background-color: beige;
		figure{
			width: 100%;
			img {
				width: 100%;
			}
		}
	}
	.slide-arrow {
		position: absolute;
		display: flex;
		top: 0;
		bottom: 0;
		margin: auto;
		height: 4rem;
		background-color: white;
		border: none;
		width: 2rem;
		font-size: 3rem;
		padding: 0;
		cursor: pointer;
		opacity: 0.5;
		transition: opacity 100ms;
		&:hover,
		&:focus {
			opacity: 1;
		}
		&.next {
			right: 0;
			padding-left: 0.75rem;
			border-radius: 2rem 0 0 2rem;
		}
		&.prev {
			left: 0;
			padding-left: 0.25rem;
			border-radius: 0 2rem 2rem 0;
		}
	}
}

En el div inicial, agregamos la directiva data-wp-interactive="create-block" para indicar que activamos la Interactivity API en el contenido. El namespace por defecto es "create-block", aunque podemos cambiarlo para identificar mejor nuestro plugin.

Para los botones de navegación, asignamos acciones con la directiva data-wp-on:

data-wp-on--click="actions.prevSlide"

data-wp-on--click="actions.nextSlide"

En el contenedor de la lista de imágenes, agregamos:

data-wp-watch--update="callbacks.updateSliderPosition"

Esto ejecutará el callback al crear el nodo y cada vez que se actualice el contexto declarado en:

wp_interactivity_data_wp_context( array( "slideIndex" => 0 ) )

Acciones y callbacks

Agregamos las acciones y callbacks al src/view.js: Incrementamos y decrementamos el slideIndex en cada acción de los botones de navegación y en el callback usamos la propiedad de javascript scrollLeft del contenedor para mover el carrousel según el slideIndex.

import { store, getContext, getElement } from '@wordpress/interactivity';

const { state } = store( 'create-block', {
	actions: {
		nextSlide: () =>{
			const context = getContext();
			context.slideIndex += 1
		},
		prevSlide: () => {
			const context = getContext();
			context.slideIndex -= 1
		},
	},
	callbacks: {
		updateSliderPosition: () => {
			const context = getContext();
			const { ref } = getElement();
			ref.scrollLeft = context.slideIndex * ref.children[context.slideIndex].clientWidth;
		},
	},
} );

La función getContext() nos permite acceder a las variables locales del bloque, mientras que getElement() nos permite acceder al nodo que está vinculado a ese callback.

Inicio y fin de los botones

Por último, vamos a agregar la lógica a los botones para deshabilitarlos cuando no haya más imágenes para seguir navegando.

En render.php agregamos la directiva data-wp-bind a los botones y un nuevo atributo para poder indicar que llego a la ultima imagen, la primera ya la tenemos:

<?php echo wp_interactivity_data_wp_context( array( "slideIndex" => 0, "lastSlide" => 0 ) ); ?> >
	<section class="slider-wrapper">
		<button class="slide-arrow prev" data-wp-on--click="actions.prevSlide" data-wp-bind--disabled="!context.slideIndex">
			&#8249;
		</button>
		
		<button class="slide-arrow next" data-wp-on--click="actions.nextSlide" data-wp-bind--disabled="context.lastSlide">	
			&#8250;
		</button>

por otro lado en view.js actualizaos el atributo de context lastSlide para que devuelva true cuando slideIndex tiene el mismo valor de scrollWidth menos un ancho de slide clientWidth

updateSliderPosition: () => {
	const context = getContext();
	const { ref } = getElement();
	ref.scrollLeft = context.slideIndex * ref.children[context.slideIndex].clientWidth;
	context.lastSlide = ref.children.length - 1 === context.slideIndex;
}

Generar el plugin

Finalmente, para generar el archivo ZIP del plugin e instalarlo en otro sitio, ejecutamos:

Fuentes

How To Build a Simple Carousel With Vanilla JavaScript

Interactivity API Reference

Repositorio con código completo