Execute JavaScript code when your custom block is inserted

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. 🙂

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)