I’m sharing my solution here in case it can help someone, as I believe this type of shortcode transform is a common use case.
As fetching data inside a transform is not possible right now, I’m creating the block inside the transform
by passing just the carousel id
attribute from the carousel
shortcode. I then fill the rest of the attributes afterwards inside the edit
function by querying the WP data store there. Finally, I set the temporary id
block attribute to undefined
so it’s not stored in the block markup as this attribute is useless after the transform.
I know this is a bit convoluted and cumbersome, but it seems to be the only way to do this right now. If anyone finds a better way to do this, I’m all ears.
Thanks to Tom J Nowell for the help.
block.json:
{
"$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": {
"id": {
"type": "integer"
},
"images": {
"type": "array",
"items": {
"type": "integer"
},
"default": []
},
"caption": {
"type": "string",
"default": ""
},
"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"
}
index.js:
/**
* WordPress dependencies
*/
import {
G,
Path,
SVG,
} from '@wordpress/components';
import {
registerBlockType
} from '@wordpress/blocks';
/**
* Internal dependencies
*/
import metadata from './block.json';
import Edit from './edit';
import transforms from './transforms';
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: Edit,
transforms,
};
registerBlockType(name, settings);
edit.js:
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
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 { useEffect } from '@wordpress/element';
import { useRefEffect } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { Carousel } from './carousel.js';
const Edit = (props) => {
const blockProps = useBlockProps();
const {
attributes: { id, 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 },
});
// Get the carousel post meta data using the temporary id attribute.
const { post, hasPostResolved } = useSelect(select => {
return {
post: select('core').getEntityRecord('postType', 'carousel', id, {
context: 'edit',
_fields: ['id', 'meta']
}),
hasPostResolved: select('core').hasFinishedResolution('getEntityRecord', ['postType', 'carousel', id, {
context: 'edit',
_fields: ['id', 'meta']
}]),
}
}, [id]);
// Fill the missing attributes after a shortcode-to-block transform.
useEffect(() => {
if (hasPostResolved && post) {
if (images.length === 0) {
const postImages = post.meta._rsc_carousel_items.split(',');
setAttributes({ images: postImages });
}
if (!caption) {
const postCaption = post.meta._rsc_carousel_caption;
setAttributes({ caption: postCaption });
}
if (max_height === '300px') {
const postMaxHeight = post.meta._rsc_carousel_max_height;
setAttributes({ max_height: postMaxHeight + 'px' });
}
// Unset the id attribute as it's no longer necessary.
setAttributes({ id: undefined });
}
}, [hasPostResolved, post]);
const { attachments } = useSelect(select => {
const query = {
include: images.join(','),
per_page: images.length,
orderby: 'include'
};
return {
attachments: select('core').getMediaItems(query, { context: 'view' }),
}
}, [images]);
const displayImages = (images) => {
return (
images.map((image, index) => {
return (
<div className="carousel-item" key={index}>
<figure>
<img src={image.source_url} style={{ maxHeight: max_height }} alt={image.alt_text} key={image.id} />
{image.caption && (
<figcaption>{image.caption.rendered}</figcaption>
)}
</figure>
</div>
)
})
)
};
function setImages(media) {
const imageIDs = media.map(image => image.id);
setAttributes({ images: imageIDs })
}
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}
onSelect={setImages}
render={({ open }) => (
<ToolbarButton onClick={open}>
{__('Edit images', 'rather-simple-carousel')}
</ToolbarButton>)}
/>
</MediaUploadCheck>
</ToolbarGroup>
)}
</BlockControls>
<MediaUploadCheck>
{attachments && attachments.length > 0 ?
<figure {...blockProps}>
<div className="carousel-wrapper" ref={ref}>
<div className="carousel-frame">
<div className="carousel-items">
{
displayImages(attachments)
}
</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>
:
<MediaPlaceholder
accept="image/*"
allowedTypes={['image']}
onSelect={setImages}
multiple={true}
gallery={true}
addToGallery={true}
handleUpload={true}
labels={
{ title: __('Rather Simple Carousel', 'rather-simple-carousel') }
}
/>
}
</MediaUploadCheck>
</>
);
}
export default Edit;
transforms.js
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
const transforms = {
from: [
{
type: 'shortcode',
tag: 'carousel',
transform({ named: { id } }) {
return createBlock('my/rather-simple-carousel', {
id: id
});
}
},
],
}
export default transforms;