I have created a "Tabbed Panels" (tabbed content) block that is essentially just an InnerBlocks component that only allows the block "Panel". When you create a Panel you must give the Panel a heading which is then used in the Panel as well as the Tab button. So in my render function for Tabbed Panels, I need to pull the heading from the children Panel blocks.
There are a couple approaches I could use, like just using regex in the tabbed-panels-render.php function to search the children html for the proper attributes, but this doesn't seem like the best approach.
I think the simplest solution would be to listen for any changes to the Panel blocks and saves the changes (heading and id in this case) to the parent. My current approach is based off this discussion which uses hooks to listen for changes. That part seems to work fine, but I need to save the output somewhere so I'm saving them as attributes to the Tabbed Panels block. This seems to work fine at first, but putting the "setAttributes" method directly in the edit function leads to issues. If there are too many Tabbed Panel blocks on the page, then React throws a "too many renders" error.
Where should my "setAttributes" function live, or is there a better approach to passing data from child to parent? I thought about using the useDispatch hook in the child, but I need to check a lot of events (the heading changes, the block is reordered, the block is deleted, etc.)
Here are the relevant js and php files. There are some custom elements, but you should be able to parse it.
tabbed-panels.js
import { arraysMatch } from 'Components/utils.js'
const { InnerBlocks } = wp.blockEditor
const { createBlock } = wp.blocks
const { Button } = wp.components
const { useDispatch, useSelect } = wp.data
const { __ } = wp.i18n
export const tabbedPanels = {
name: 'my/tabbed-panels',
args: {
title: __('Tabbed Panels', '_ws'),
description: __('Tabbable panels of content.', '_ws'),
icon: 'table-row-after',
category: 'common',
supports: {
anchor: true
},
attributes: {
headings: {
type: 'array',
default: []
},
uids: {
type: 'array',
default: []
}
},
edit: props => {
const { setAttributes } = props
const { headings, uids } = props.attributes
const { insertBlock } = useDispatch('core/block-editor')
const { panelHeadings, panelUids, blockCount } = useSelect(select => {
const blocks = select('core/block-editor').getBlocks(props.clientId)
return {
panelHeadings: blocks.map(b => b.attributes.heading),
panelUids: blocks.map(b => b.clientId),
blockCount: select('core/block-editor').getBlockOrder(props.clientId).length
}
})
if (!arraysMatch(panelHeadings, headings)) {
setAttributes({ headings: panelHeadings })
}
if (!arraysMatch(panelUids, uids)) {
setAttributes({ uids: panelUids })
}
return (
<div className="block-row">
<InnerBlocks
allowedBlocks={ ['my/panel'] }
templateLock={ false }
renderAppender={ () => (
<Button
isSecondary
onClick={ e => {
insertBlock(createBlock('my/panel'), blockCount, props.clientId)
} }
>
{ __('Add Panel', '_ws') }
</Button>
) }
/>
</div>
)
},
save: props => {
return (
<InnerBlocks.Content />
)
}
}
}
tabbed-panels-render.php
<?php
function block_tabbed_panels($atts, $content) {
$atts['className'] = 'wp-block-ws-tabbed-panels ' . ($atts['className'] ?? '');
$headings = $atts['headings'] ?? '';
$uids = $atts['uids'] ?? '';
ob_start(); ?>
<div class="tabs" role="tablist">
<?php
foreach ($headings as $i=>$heading) : ?>
<button
id="tab-<?= $uids[$i]; ?>"
class="tab"
role="tab"
aria-selected="false"
aria-controls="panel-<?= $uids[$i]; ?>"
tabindex="-1"
>
<?= $heading; ?>
</button>
<?php
endforeach; ?>
</div>
<div class="panels">
<?= $content; ?>
</div>
<?php
return ob_get_clean();
}
panel.js
import ComponentHooks from 'Components/component-hooks.js'
const { InnerBlocks, RichText } = wp.blockEditor
const { __ } = wp.i18n
export const panel = {
name: 'my/panel',
args: {
title: __('Panel', '_ws'),
description: __('Panel with associated tab.', '_ws'),
icon: 'format-aside',
category: 'common',
supports: {
customClassName: false,
html: false,
inserter: false,
reusable: false
},
attributes: {
heading: {
type: 'string'
},
uid: {
type: 'string'
}
},
edit: props => {
const { setAttributes } = props
const { heading } = props.attributes
return (
<>
<ComponentHooks
componentDidMount={ () => setAttributes({ uid: props.clientId }) }
/>
<RichText
label={ __('Tab Name', '_ws') }
placeholder={ __('Tab Name', '_ws') }
tagName="h4"
onChange={ newValue => setAttributes({ heading: newValue }) }
value={ heading }
/>
<InnerBlocks
templateLock={ false }
/>
</>
)
},
save: props => {
return (
<InnerBlocks.Content />
)
}
}
}
panel-render.php
<?php
function block_panel($atts, $content) {
$uid = $atts['uid'] ?? '';
ob_start(); ?>
<div
id="panel-<?= $uid ?>"
class="panel"
role="tabpanel"
aria-labelledby="tab-<?= $uid; ?>"
tabindex="0"
hidden="hidden"
>
<?= $content; ?>
</div>
<?php
return ob_get_clean();
}
setAttributes
is triggeringcomponentDidUpdate
and this leads to an infinite loop. – Drawee