How to mix partial and full page refresh in the same section of the customizer?

tl;dr See standalone example plugin that demonstrates initiating a full refresh when a selective refresh request enqueues different scripts or styles and also which re-initializes JS behaviors for selectively-refreshed partial placement containers.


I understand the goal to be to minimize the amount of logic required in the customizer to preview changes to updated partials that enqueue different scripts and styles based on a setting change. The solution here has been accounted for in the design of selective refresh in the customizer. In particular, when a partial is rendered it does so in the context of the URL on which it will be displayed. This means that all of the scripts and styles enqueued for that given URL should be enqueued when the partial is rendered. To this end, the customize_render_partials_before action has the following docs:

Fires immediately before partials are rendered.

Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts
and styles which may get enqueued in the response.

And eventually core may go ahead and do wp_enqueue_scripts() itself by default. For the initial implementation of selective refresh, however, the enqueueing was left to plugins.

Once the scripts and styles have been enqueued, the need is then to communicate back to the JS in the page which assets have been enqueued in the response. This is what the customize_render_partials_response filter is for, as its docs indicate:

Filters the response from rendering the partials.

Plugins may use this filter to inject $scripts and $styles, which are dependencies
for the partials being rendered. The response data will be available to the client via
the render-partials-response JS event, so the client can then inject the scripts and
styles into the DOM if they have not already been enqueued there.

If plugins do this, they’ll need to take care for any scripts that do document.write()
and make sure that these are not injected, or else to override the function to no-op,
or else the page will be destroyed.

Plugins should be aware that $scripts and $styles may eventually be included by
default in the response.

The filtered $response array is exposed in JavaScript via a render-partials-response event triggered on wp.customize.selectiveRefresh. So to initiate a full refresh when a selective refresh request enqueues assets not already on the page, what is needed is to first export the handles for enqueued scripts and styles for the initial page load, and then in the render-partials-response JS event check to see if the enqueued script and style handles during the selective refresh request are the same that were initially enqueued on the page. If they differ, then the JS just needs to call wp.customize.selectiveRefresh.requestFullRefresh(). The following JS would be enqueued only in the customizer preview:

/* global wp, _ */
/* exported WPSE_247251 */
var WPSE_247251 = (function( api ) {

    var component = {
        data: {
            enqueued_styles: {},
            enqueued_scripts: {}
        }
    };

    /**
     * Init.
     *
     * @param {object} data Data exported from PHP.
     * @returns {void}
     */
    component.init = function init( data ) {
        if ( data ) {
            _.extend( component.data, data );
        }
        api.selectiveRefresh.bind( 'render-partials-response', component.handleRenderPartialsResponse );
    };

    /**
     * Handle render-partials-response event, requesting full refresh if newly-enqueued styles/styles differ.
     *
     * @param {object} response Response.
     * @returns {void}
     */
    component.handleRenderPartialsResponse = function handleRenderPartialsResponse( response ) {
        if ( ! response.wpse_247251 ) {
            return;
        }
        if ( ! _.isEqual( component.data.enqueued_styles, response.wpse_247251.enqueued_styles ) ) {
            api.selectiveRefresh.requestFullRefresh();
        }
        if ( ! _.isEqual( component.data.enqueued_scripts, response.wpse_247251.enqueued_scripts ) ) {
            api.selectiveRefresh.requestFullRefresh();
        }
    };

    return component;
})( wp.customize );

An inline script can be added in PHP to export the enqueued_scripts and enqueued_styles by then calling WPSE_247251.init( data ).

When a partial is selectively refreshed, a bit more is needed to ensure that any JS behaviors associated with the container are re-initialized. Specifically, the JS should be engineered so that there is a function that is responsible for finding elements to set up in the DOM. By default this function should look at the entire body. But then the JS can check to see if it is running in the context of the customizer preview, and if so, listen for the partial-content-rendered event triggered on wp.customize.selectiveRefresh to then call that function passing in placement.container as the scope for initializing rather than the entire body:

if ( 'undefined' !== typeof wp && 'undefined' !== typeof wp.customize && 'undefined' !== typeof wp.customize.selectiveRefresh ) {
    wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
        initializeAllElements( $( placement.container ) );
    } );
}

So again, see a standalone example plugin that demonstrates initiating a full refresh when when a selective refresh request enqueues different scripts or styles and also which re-initializes JS behaviors for selectively-refreshed partial placement containers.

In closing, if WordPress themes could just be implemented with something like React we could avoid adding special support for selectively refreshing elements on the page since the changes to the state would automatically cause the desired changes in the DOM. Since we’re not there yet (although prototypes are being worked on for this) we have to settle for the next best experience for quickly refreshing parts of a page with the minimal extra effort for developers: selective refresh.

Leave a Comment