Selective Refresh in the Customizer

Note: This proposal was merged to core in [36586]. Download the latest nightly build and give it a try!

The Customizer is our framework for live previewing changes. However, settings edited in the Customizer get sent to the preview via the refresh transport by default, meaning that in order for a setting change to be applied, the entire preview has to reload: this refresh can be very slow and is anything but “live”. To remedy this poor user experience of a laggy preview, the Customizer allows settings to opt-in to using a postMessage transport which relies on JavaScript to preview the change instantly without doing any server-side communication at all. This provides for a great user experience.

There is a major issue with settings using the postMessage transport, however: any logic used in PHP to render the setting in the template has to be duplicated in JavaScript. This means that the postMessage previewing logic violates the DRY (Don’t Repeat Yourself) principle, and so it is really only suitable for simple text or style changes that involve almost no logic (e.g. site title or background color). Even in this case, it is often not possible to fully re-implement the PHP logic in JS, since as in the example of #33738 there may be an unknown number of PHP filters that get applied to the setting’s value (such as the site title) when rendering the content on the server.

What the Customizer needs is a middle-ground between refreshing the entire preview to apply changes with PHP and between applying the changes exclusively with JavaScript. The selective refresh is the solution proposed in #27355, and this post is about documenting the feature, as it was approved for core merge in 4.5. Selective refresh also appears on the proposed Customizer roadmap.

Partial preview refreshing was first explored when Customizer widget management was being developed for the 3.9 release. It was added to the Widget Customizer plugin and described in a Make Core post. But it was decided to remove the feature in favor of having a better generalized framework for partial refreshes in a future release (which is now). In 4.3, nav menus were added to the Customizer and with this the first core implementation of selective refresh, as described in Fast previewing changes to Menus in the Customizer. (For more background, see Implementing Selective Refresh in the Customizer.)

With the shipped example of selective refresh of nav menus in the Customizer, and an initial implementation of selective refresh for widgets, work progressed to generalize a selective framework that would support the complicated examples of nav menus and widgets, but also to support simpler use cases such as letting the server render the updated site title so that wptexturize and other filters can apply. The generalized framework has been implemented in the Customize Partial Refresh feature plugin, which also re-implements selective refresh of nav menus and introduces selective refresh of sidebars and widgets.

To see a simple example of selective refresh, see the Site Title Smilies plugin showing wptexturize and emoji applying to site title updates in the preview:

Note in this example that the standard postMessage updating of the site title in the header is still in place and so the change appears immediately. It gets followed, however, by the content being rendered by the partial on the server. This allows for JS to apply a quick low-fidelity preview for a setting change until the server can respond with the high-fidelity preview.

Selective Refresh Architecture

This plugin introduces a selective refresh framework which centers around the concept of the partial. A partial is conceptually very similar to a Customizer control. Both controls and partials are associated with one or more settings. Whereas a control appears in the pane, the partial lives in the preview. The fields in a control update automatically to reflect the values of its associated settings, and a partial refreshes in the preview when one of its settings is changed. The name “partial” is derived from  get_template_part(), which is a natural function to use in a partial’s  render_callback.

In addition to a partial being associated with one or more settings, a partial is also registered with a jQuery selector which identifies the partial’s locations or “placements” on the page, where the rendered content appears. For example, a partial can be registered for the site title to update the header as well as the document title via:

function my_register_blogname_partials( WP_Customize_Manager $wp_customize ) {

	// Abort if selective refresh is not available.
	if ( ! isset( $wp_customize->selective_refresh ) ) {
		return;
	}

	$wp_customize->selective_refresh->add_partial( 'header_site_title', array(
		'selector' => '.site-title a',
		'settings' => array( 'blogname' ),
		'render_callback' => function() {
			return get_bloginfo( 'name', 'display' );
		},
	) );

	$wp_customize->selective_refresh->add_partial( 'document_title', array(
		'selector' => 'head > title',
		'settings' => array( 'blogname' ),
		'render_callback' => 'wp_get_document_title',
	) );
}
add_action( 'customize_register', 'my_register_blogname_partials' );

When a setting (like blogname here) is modified, each registered partial is asked on the client whether it isRelatedSetting(). If that method for a partial returns true, then the partial’s refresh() method is invoked and a request is queued up to render the partial via the requestPartial() function. Calls to this function are debounced and multiple simultaneous partials being changed will be batched together in a single Ajax request to the server.

When the server receives a request to render partials, it will receive the list of partials as well as all of the modified settings in the Customizer. Partials need not be all registered up front: as settings support the customize_dynamic_setting_args and customize_dynamic_setting_class filters, so too partials support the customize_dynamic_partial_args and customize_dynamic_partial_class filters. These filters allow partials to be added on demand to support the partials being requested to render.

If there is no registered partial for the request, or if the partial’s render_callback returns false, then false will be returned in the response as the content for that partial, and the default fallback() behavior in the client-side Partial object is to requestFullRefresh(). On the other hand, if the request recognizes the partial and the render_callback returns content, then this content is passed along to the Partial instance in the client and its renderContent() method then follows through to update the document with the new content.

As noted previously, the location in the document where a partial renders its update is called a placement. A selector may match any number of elements, and so too a partial may update multiple placements. A partial’s placements in the document may each have different sets of context data associated with them. This context data is included in the partials request and is passed along to the render_callback so that the content can vary as needed, for example a widget appearing in two different sidebars would have different sidebar_id context data, which would then determine the before_widget and after_widget args used.

Partial placements may also be nested inside other placements. For example, a Custom Menu widget is rendered as a placement which then contains another placement for a nav menu. The nav menu placement is updated independently from the parent widget placement. This being said (and this is advanced usage), a selector does not need to be supplied when registering a partial: alternatively to a top-down selector, the placement for a partial can be declared from the bottom-up by adding a data-customize-partial-id attribute to any container element. Whenever a partial is re-rendered, any nested partials will be automatically recognized. (As noted above, each placement can have a different associated context, and this is indicated by JSON object in a data-customize-partial-placement-context HTML attribute.)

Aside: The architecture of the Infinity Scroll module in Jetpack mirrors Selective Refresh framework in several ways.

Linking Partials to Controls

Since each partial is associated with at least one setting, shift-clicking on the placement for a partial will attempt to focus on the related control in the pane. This was already implemented for widgets since 3.9 and recently for nav menu items as well, but now the functionality is available to partials generally.

Selective Refresh of Widgets

Selective refresh for nav menus was enabled by default in 4.3. While some themes needed to add theme support for any dynamic aspects (such as the expanding submenus in Twenty Fifteen), generally nav menus seem to be fairly static and can be replaced in the document without needing any JS logic to be re-applied. Widgets, on the other hand, have much more variation in what they can display, since they can in fact display anything. For any widget that uses JavaScript to initialize some dynamic elements, such as a map or gallery slideshow, simply replacing the widget’s content with new content from the server will not work since the dynamic elements will not be re-initialized. Additionally, the sidebars (widget areas) in which the widgets are displayed may also have dynamic aspects associated with them, such as the Twenty Thirteen sidebar which displays widgets in a grid using Masonry. For this theme, whenever a widget is changed/added/removed/reordered, the sidebar needs to be reflowed.

In order to allow themes to reflow sidebars and widgets to reinitialize their contents after a refresh, the selective refresh framework introduces widget-updated and sidebar-updated events. Additionally, since re-ordering widgets in a sidebar is instant by just re-ordering the elements in the DOM, some widgets with dynamically-generated iframes (such as a Twitter widget) may need to also be rebuilt, and for this reason there is a partial-content-moved event.

For the above reasons, I believe it will be much more common for widgets (and sidebars) to need special support for selective refresh, and so I think that at least for 4.5 the selective refresh should be opt-in for widgets (and perhaps in themes for sidebars). See theme support for Twenty Thirteen and plugin support for a few widgets in Jetpack (which won’t be part of the merge). At last week’s Core dev meeting, it was suggested that we add the opt-in for widget selective refresh at RC.

Update: See follow-up post on Implementing Selective Refresh Support for Widgets.

Selective Refresh API

What follows is a list of aspects of the API that should be more commonly used. For full documentation, see the inline docs in the plugin source.

PHP

I suggest that Selective Refresh be added as a new Customizer component to be accessed via $wp_customize->selective_refresh which is where the methods for partials(), add_partial(), get_partial(), remove_partial(), and others would be accessed.

New classes:

  • WP_Customize_Selective_Refresh
  • WP_Customize_Partial

New filters:

  • customize_partial_render
  • customize_partial_render_{$partial->id}
  • customize_dynamic_partial_args
  • customize_dynamic_partial_class
  • customize_render_partials_response

New actions:

  • customize_render_partials_before
  • customize_render_partials_after

JS

Namespace: wp.customize.selectiveRefresh

New classes:

  • wp.customize.selectiveRefresh.Partial
  • wp.customize.selectiveRefresh.Placement
  • wp.customize.navMenusPreview.NavMenuInstancePartial
  • wp.customize.widgetsPreview.WidgetPartial
  • wp.customize.widgetsPreview.SidebarPartial

New properties:

  • wp.customize.selectiveRefresh.parital (collection)
  • wp.customize.selectiveRefresh.partialConstructor

Events (triggered on wp.customize.selectiveRefresh):

  • render-partials-response
  • partial-content-rendered
  • partial-placement-moved
  • widget-updated
  • sidebar-updated

The Future

If we can eliminate full-page refreshes from being the norm for the Customizer, we can start to introduce controls inline with the preview. If the entire preview does not reload, then the inline controls won’t get destroyed by the refresh with each change. For example, consider a widget control floating immediately next to the widget in the sidebar it is editing. With selective refresh, it will then also be possible to eliminate the Customizer altogether. The Customizer could be available to anyone who is logged in, with the controls being bootstrapped on demand when a user wants to edit a given element. There would be no need to navigate way from a page on the frontend to enter a unique Customizer state: the Customizer would come to the user. Any controls not relevant to being inline could remain in the Customizer pane, but it could slide in only as needed instead of appearing by default. That is to say, selective refresh makes the Customizer a much better framework for implementing frontend editing.

Feedback

There is just over a week until the first beta for 4.5. I’d like to get any final feedback on the feature as soon as possible before committing it to trunk. The plugin is on GitHub and the plugin directory. Feedback should be left on #27355. Note that the plugin depends on 4.5-alpha-35776 to run.