This is really a two stage question, for not very relevant context you can look at this question – How to customize a shortcode using the customizer. In brief I got a widget and a shortcode that implement the same functionality and that is customized in the customizer by adapting the form generated for the widget into a customizer section.

Now I would like to use partial refresh when possible, but some ssettings require CSS and/or JS changes and a full refresh is a must to reflect changes (if you want to avoid writting crazy JS code just for that).

So the easier part of the question is the shortcode section – how to have two
settings in the saame section with one doing a partial refresh while the other doing a full one.

The advanced part is to have it working for the widget as well.

To make it less theoretical, here is the code in question – https://github.com/tiptoppress/category-posts-widget/tree/4.7.1

That is a “recent posts” type of widget which lets you specify filtering criteria based on wp_query settings, therefor some settings just require the trip to the server to produce a “preview”. Some other setting control the display by inserting CSS rules, while some other setting – browser side cropping, need JS for implementation and the JS need to be activated per widget, and for this the ids of the widgets are enqueued.

Partials are good solution for the wp_query related part, but not for the CSS. In theory you can enqueue JS in the partial (I assume), but it is not obvious how to dequeue it.

The question here is not really if it is possible to hack something that will make it work, as given enough effort everything can be massaged into a working state, the problem is to do it in a way which will not violate DRY and KISS principals and show the user exactly what he will see on the front end and not just an approximation. In english – I don’t want to fork how the widget’s front end works just so I will be able to claim that I use partial as the difference in actual user UX between a refresh and a partial is not that big to justify forking the front end code.

(yes this is kind of two questions in one, but maybe the answers are not as different as I suspect they will be)

2 Answers
2

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 Reply

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