Hey visitors! If you’re a beginner in WordPress and WP-CLI development, then you should check out my dear friend Bhargav’s Youtube channel! @BuntyWP

Detecting completion of DOM updates in Gutenberg blocks using MutationObserver

This article is not just limited to Gutenberg. The solution can be used outside of it as well.

Recently I had a task to automate inserting blocks to the editor and saving the post. This worked fine for most of the blocks – except the gallery block.

After inserting a gallery and then programmatically saving the post, I could see the images. But when I reloaded the editor page, the images disappeared and all that remained was an empty gallery with placeholders.

The logic which I initially implemented was very simple:

wp.data
    .dispatch( 'core/block-editor' )
    .insertBlocks( blocks ); // insert the blocks.

wp.data
    .dispatch( 'core/editor' ).savePost(); // save the post.Code language: JavaScript (javascript)

But this did not work as expected.

Failure due to programmatically inserting the Gallery block immediately followed by post saving.

Why did it fail?

The Gallery block is actually a collection of Image inner blocks and when you insert a Gallery block, it first inserts the Gallery block to the editor DOM, and then inserts all the Image inner blocks to the Gallery block.

For those who don’t know, DOM manipulations/updates are asynchronous in nature. When you call savePost() immediately after insertBlocks() , the post saving begins whether or not insertBlocks() is done finishing the DOM updates.

In our case, saving of the post began before the inner blocks finished adding the src attribute to the image elements. To solve this, we used MutationObserver.

The solution:

/**
 * Inserts the array of blocks to the editor and saves the post.
 *
 * @param {Array} blocks Array of blocks to be inserted.
 */
async function insertBlocksAndSave( blocks ) {
    wp.data
        .dispatch( 'core/block-editor' )
        .insertBlocks( blocks );

        if ( Array.isArray( blocks ) ) {
            const promises = blocks.map( ( block ) => waitForDOMManipulation( block ) );
            await Promise.all( promises );
        }

    wp.data
        .dispatch( 'core/editor' ).savePost();
}

// Blocks insertion begins here:
insertBlocksAndSave( arrayOfblocksToInsert );

/**
 * Waits for a block's DOM manipulation to finish.
 *
 * @param {Object} block The block being inserted.
 *
 * @returns {Promise<void>} A promise that resolves when DOM manipulation is detected
 * or when no DOM manipulation happens.
 */
async function waitForDOMManipulation( block = '' ) {
    // @type {HTMLIFrameElement} iframeElement - The editor iframe element.
    let iframeElement;

    // @type {HTMLElement} blockEl - The block element within the iframe.
    let blockEl;

    // @type {string} clientId - The unique identifier for the block.
    const { clientId } = block;

    // Resolves after the editor iframe canvas is found.
    await new Promise( ( resolve ) => {
        const intervalId = setInterval( () => {
            iframeElement = document.getElementsByName( 'editor-canvas' );

            if ( iframeElement.length > 0 ) {
                iframeElement = iframeElement[0];
                clearInterval(intervalId);
                resolve();
            }
        }, 100 );
    });

    // Resolves after the block is inserted to the iframe.
    await new Promise( ( resolve ) => {
        const intervalId = setInterval( () => {

            // @type {Document} iframeDocument - The document object of the iframe.
            const iframeDocument =
                iframeElement.contentDocument || iframeElement.contentWindow.document;

            if ( iframeDocument.body ) {
                blockEl = iframeDocument.getElementById( `block-${clientId}` );

                if ( blockEl ) {
                    clearInterval( intervalId );
                    resolve();
                }
            }
        }, 100 );
    } );

    await new Promise( ( resolve ) => {
        const observer = new MutationObserver( ( mutationList, observer ) => {
            if ( observer.timeoutId ) {
                clearTimeout( observer.timeoutId );
            }

            observer.timeoutId = setTimeout( () => {
                observer.disconnect();
                resolve();
            }, 100 );
        });

        observer.timeoutId = null;
        observer.observe( blockEl, { childList: true, subtree: true } );

        // We resolve if there is no DOM manipulations happening.
        setTimeout( () => {
            observer.disconnect();
            resolve();
        }, 100 );
    } );

    // Recursively call waitForDOMManipulation on inner blocks if they exist.
    if ( block.innerBlocks && block.innerBlocks.length > 0 ) {
        await Promise.all(
            block.innerBlocks.map( ( innerBlock ) => waitForDOMManipulation( innerBlock ) ),
        );
    }

    // Return a resolved promise to ensure function returns a promise.
    return Promise.resolve();
}

Code language: TypeScript (typescript)

Solution in action:



Leave a Reply

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

Powered by WordPress