So is there some hook I can attach to, whenever my custom block is
inserted / appended in the visual editor?
At the time of writing, none, as far as I know.
But there is a function, namely wp.data.subscribe()
, which we can use to listen to (state) changes in the editor, e.g. when the post is published, when the title or permalink/slug is changed, when a new block is added to or removed from the post, etc.
Therefore, you could use that function to run HVT.initializeTabbedBlock()
whenever a new block of your block type is added to the post. Here’s an example which uses _.difference()
(which means underscore
needs to be added to your script’s dependencies) and an IIFE:
( ( blockName ) => {
const { subscribe, select } = wp.data;
// Used for storing the current target blocks.
let _previousBlockClientIds = [];
subscribe( () => {
const currentBlocks = select( 'core/block-editor' ).getBlocks();
const targetBlockClientIds = [];
// A recursive function which searches for the clientId of all the target
// blocks, including inner blocks.
const findTargetBlocks = ( blocks ) => {
blocks.forEach( block => {
if ( block.innerBlocks.length ) {
findTargetBlocks( block.innerBlocks );
} else if ( blockName === block.name ) {
targetBlockClientIds.push( block.clientId );
}
} );
};
findTargetBlocks( currentBlocks );
const addedBlockClientId = _.difference( targetBlockClientIds, _previousBlockClientIds )?.[0];
if ( addedBlockClientId ) {
console.log( `New ${ blockName } added. Current total: ${ targetBlockClientIds.length }` );
// A new block was added, but we should wait until the block node is attached to the DOM.
const unsubscribe = subscribe( () => {
const tabbedBlock = document.querySelector( `[data-block="${ addedBlockClientId }"]` );
if ( tabbedBlock ) {
HVT.initializeTabbedBlock( tabbedBlock );
unsubscribe();
}
} );
}
_previousBlockClientIds = targetBlockClientIds;
} );
} )( 'hvt/tabbed-block' );
However, if it’s really just about attaching a click
listener or another event handler, then I would just attach it directly on the corresponding elements. E.g.
Note: The HTML markup was based on the Gist here. I also used JSX because the code would be simpler, i.e. easier to read.
// In the `edit` function:
return (
<div { ...blockProps }>
<div className="hvt-tabbed-block-tabs">
<a className="hvt-tabbed-block-tab" onClick={ HVT.onTabbedBlockTabSelect }>Tab 1</a>
<a className="hvt-tabbed-block-tab" onClick={ HVT.onTabbedBlockTabSelect }>Tab 2</a>
...
</div>
...
</div>
);
Or if you don’t like that, or that the initialization code is too “long” or not as simple as attaching event handlers, then you can use a ref with useEffect
, like so:
// In the `edit` function:
// Create a ref for .hvt-tabbed-block-tabs.
const blockTabsRef = useRef( null );
// Run the initialization code via useEffect.
useEffect( () => {
HVT.initializeTabbedBlock( blockTabsRef.current );
}, [] );
return (
<div { ...blockProps }>
<div className="hvt-tabbed-block-tabs" ref={ blockTabsRef }>
<a className="hvt-tabbed-block-tab">Tab 1</a>
<a className="hvt-tabbed-block-tab">Tab 2</a>
...
</div>
...
</div>
);
What I would really do
After looking at your Gist, I thought that I’d probably just “copy” the same behaviors of the HVT
object into the block’s edit
function.
Here’s an example which uses a local state named activeTab
to control the tabs and panes, e.g. to add/remove the active tab class which then activates the pane for the clicked tab:
// In the `edit` function:
const blockProps = useBlockProps( {
className: 'hvt-tabbed-block',
} );
// This is otherwise `attributes.tabs`, and the `id` property is just an example
// of setting a unique tab identifier.
const tabs = [
{ title: 'Tab 1', id: 'tab-1' },
{ title: 'Tab 2', id: 'tab-2' },
{ title: 'Tab 3', id: 'tab-3' },
];
// Create a state for the active tab, with the first tab in the tabs array being
// the default active tab.
const [ activeTab, setActiveTab ] = useState( tabs?.[0]?.id );
function onTabClick( tabId ) {
setActiveTab( tabId );
return false; // Equivalent or alternative to calling event.preventDefault().
}
return (
<div { ...blockProps }>
<div className="hvt-tabbed-block-tabs">
{
tabs.map( ( tab, i ) => (
<a
className={ 'hvt-tabbed-block-tab' + // wrapped for brevity
( activeTab === tab.id ? ' hvt-tabbed-block-tab-active' : '' ) }
data-hvt-tabbed-block-id={ tab.id }
onClick={ () => onTabClick( tab.id ) }
key={ 'tab_' + ( tab.id || i ) }
>
{ tab.title }
</a>
) )
}
</div>
<div className="hvt-tabbed-block-panes">
{
tabs.map( ( tab, i ) => (
<div
className="hvt-tabbed-block-pane"
data-hvt-tabbed-block-pane-target={ tab.id }
style={ { display: ( tab.id === activeTab ? 'block' : 'none' ) } }
key={ 'pane_' + ( tab.id || i ) }
>
Pane content for <b>{ tab.title }</b>
</div>
) )
}
</div>
</div>
);
So, it’s not actually that much of work to make one for Gutenberg, and particularly when you also use JSX, the code becomes easier to read. 🙂