I've built the following Gutenberg block. It's a carousel of images:
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
G,
Path,
SVG,
ToolbarGroup,
ToolbarButton,
PanelBody,
__experimentalUnitControl as UnitControl,
__experimentalUseCustomUnits as useCustomUnits,
} from '@wordpress/components';
import {
InspectorControls,
BlockControls,
MediaUploadCheck,
MediaUpload,
MediaPlaceholder,
RichText,
useBlockProps,
useSettings
} from '@wordpress/block-editor';
import {
useRefEffect
} from '@wordpress/compose';
import { registerBlockType } from '@wordpress/blocks';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import metadata from './block.json';
import { Carousel } from './carousel.js';
import './editor.scss';
import './style.scss';
const { name } = metadata;
export const settings = {
icon: {
src: <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<Path fill="none" d="M0 0h24v24H0V0z" />
<G><Path d="M20 4v12H8V4h12m0-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8.5 9.67l1.69 2.26 2.48-3.1L19 15H9zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6H2z" /></G>
</SVG>,
foreground: '#ff8a00'
},
edit: (props => {
const blockProps = useBlockProps();
const {
attributes: { images, caption, max_height },
setAttributes,
} = props;
const units = useCustomUnits({
availableUnits: useSettings('spacing.units') || [
'px',
'em',
'rem',
'vw',
'vh',
],
defaultValues: { px: 100, em: 10, rem: 10, vw: 10, vh: 25 },
});
const displayImages = (images) => {
return (
images.map((image, index) => {
return (
<div className="carousel-item" key={index}>
<figure>
<img src={image.url} style={{ maxHeight: max_height }} alt={image.alt} key={image.id} />
{image.caption && (
<figcaption>{image.caption}</figcaption>
)}
</figure>
</div>
)
})
)
};
function setImages(media) {
setAttributes({ images: media })
}
function setCaption(value) {
setAttributes({ caption: value })
}
function setMaxHeight(value) {
setAttributes({ max_height: value })
}
const ref = useRefEffect((element) => {
Carousel(element, { speed: 10 });
}, []);
return (
<>
<InspectorControls>
<PanelBody
title={__('Settings', 'rather-simple-carousel')}
>
<UnitControl
label={__('Max height of elements', 'rather-simple-carousel')}
min="1"
onChange={setMaxHeight}
value={max_height}
units={units}
/>
</PanelBody>
</InspectorControls>
<BlockControls>
{images.length > 0 && (
<ToolbarGroup>
<MediaUploadCheck>
<MediaUpload
allowedTypes={['image']}
multiple={true}
gallery={true}
value={images.map((image) => image.id)}
onSelect={setImages}
render={({ open }) => (
<ToolbarButton onClick={open}>
{__('Edit images', 'rather-simple-carousel')}
</ToolbarButton>)}
/>
</MediaUploadCheck>
</ToolbarGroup>
)}
</BlockControls>
<MediaUploadCheck>
{images.length > 0 &&
<figure {...blockProps}>
<div className="carousel-wrapper" ref={ref}>
<div className="carousel-frame">
<div className="carousel-items">
{displayImages(images)}
</div>
</div>
<div className="carousel-arrow left"><span className="icon"><svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m17.5 5v14l-11-7z" /></svg></span></div>
<div className="carousel-arrow right"><span className="icon"><svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m6.5 5v14l11-7z" /></svg></span></div>
</div>
<RichText
className="carousel-caption"
tagName="figcaption"
placeholder={__('Enter a caption', 'rather-simple-carousel')}
value={caption}
onChange={setCaption}
/>
</figure>
}
{images.length === 0 &&
<MediaPlaceholder
accept="image/*"
allowedTypes={['image']}
onSelect={setImages}
multiple={true}
gallery={true}
addToGallery={true}
handleUpload={true}
labels={
{ title: __('Rather Simple Carousel', 'rather-simple-carousel') }
}
/>
}
</MediaUploadCheck>
</>
);
}),
};
registerBlockType(name, settings);
This is the corresponding block.json
file:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my/rather-simple-carousel",
"title": "Rather Simple Carousel",
"description": "Display a carousel.",
"textdomain": "rather-simple-carousel",
"category": "media",
"attributes": {
"images": {
"type": "array",
"default": []
},
"caption": {
"type": "string"
},
"max_height": {
"type": "string",
"default": "300px"
}
},
"supports": {
"spacing": {
"margin": true,
"padding": true
}
},
"editorScript": "file:./index.js",
"viewScript": "file:./view.js",
"style": "file:./style-index.css",
"editorStyle": "file:./index.css"
}
The block works as expected with no issues. I need to add a transform to migrate a shortcode with the following syntax: [carousel id="n"]
, where id
is a post id representing a custom post type with the following meta fields:
_rsc_carousel_items
contains a string with attachment ids separated with commas (ex: 2220,2241,3256)
_rsc_carousel_max_height
contains a string with a height value in pixels (ex: 200px)
_rsc_carousel_caption
contains a string with a caption text
I added the following transform function to the block, but it's not working:
transforms: {
from: [
{
type: 'shortcode',
tag: 'carousel',
attributes: {
images: {
type: 'array',
source: async ({ id }) => {
const response = await apiFetch({ path: `/wp/v2/posts/${id}` });
const post = await response.json();
const imageIdsString = post.meta._rsc_carousel_items;
const imageIds = imageIdsString.split(',');
const images = await Promise.all(
imageIds.map(async (imageId) => {
const imageResponse = await apiFetch({
path: `/wp/v2/media/${imageId}`,
});
const image = await imageResponse.json();
return {
id: image.id,
url: image.source_url,
alt: image.alt_text,
caption: image.caption.rendered,
};
})
);
return images;
},
query: {
id: {
type: 'string',
shortcode: (attrs) => attrs.id,
},
},
},
caption: {
type: 'string',
source: async ({ id }) => {
const response = await apiFetch({ path: `/wp/v2/posts/${id}` });
const post = await response.json();
return post.meta._rsc_carousel_caption;
},
query: {
id: {
type: 'string',
shortcode: (attrs) => attrs.id,
},
},
},
max_height: {
type: 'string',
source: async ({ id }) => {
const response = await apiFetch({ path: `/wp/v2/posts/${id}` });
const post = await response.json();
return post.meta._rsc_carousel_max_height;
},
query: {
id: {
type: 'string',
shortcode: (attrs) => attrs.id,
},
},
}
},
},
],
},
Can anyone help me troubleshoot this? Thanks
apiFetch
it's going to make a brand new request even if the entity you need has already been fetched and stored in the editors data store, causing slowdowns and possibly other problems. It would be much safer and faster to query the WP Data store for this information, and if it isn't available it would fetch it for you.wp.data.select('core').getEntityRecord()
to get the information but apparently one can't use hooks such asuseSelect
inside a transform. It throws aInvalid hook call. Hooks can only be called inside of the body of a function component
error.useSelect
isn't the only way to interact with WP Data, and is actually a somewhat recent introduction meant to replacewithSelect
, neither of which should be needed here, but if you refer to my other suggestions then there would be no need at all to fetch the posts, only the IDs would be necessary which you already have. Similarly you already have a function component to calluseSelect
from, the edit componentuseEffect
if they're missing. Though storing the URL at all is a mistake that complicates things, better to store attachment IDs then generate the associated URLs on render instead of storing the URLs. ThoseapiFetch
calls are also going to yield bad performance