March 21, 2016

Implementing Selective Refresh Support for Widgets

WordPress 4.5 includes a new Customizer framework called selective refresh. To recap, selective refresh is a hybrid preview mechanism that has the performance benefit of not having to refresh the entire preview window. This was previously available with JS-applied postMessage previews, but selective refresh also improves the accuracy of the previewed change while reducing the amount of code you have to write; it also just makes possible to do performant previews that would previously been practically impossible. For example, themes often include some variation of the following PHP and JavaScript to enable performant previewing of changes to the site title:

function mytheme_customize_register( WP_Customize_Manager $wp_customize ) {
        $wp_customize->get_option( 'blogname' )->transport = 'postMessage';
}
add_action( 'customize_register', 'mytheme_customize_register' );

function mytheme_customize_preview_js() {
        $handle = 'mytheme-customize-preview';
        $src = get_template_directory_uri() . '/js/customize-preview.js';
        $deps = array( 'customize-preview' );
        $ver = '0.1';
        $in_footer = true;
        wp_enqueue_script( $handle, $src, $deps, $ver, $in_footer );
}
add_action( 'customize_preview_init', 'mytheme_customize_preview_js' );
( function( $, api ) {
        api( 'blogname', function( setting ) {
                setting.bind( function( value ) {
                        $( '.site-title a' ).text( value );
                } );
        } );
} ( jQuery, wp.customize ) );

In 4.5, the core themes now utilize selective refresh for previewing the site title and tagline. This allows the above code to be replaced with the following PHP:

function mytheme_customize_register( WP_Customize_Manager $wp_customize ) {
        $wp_customize->selective_refresh->add_partial( 'blogname', array(
                'selector' => '.site-title a',
                'render_callback' => function() {
                        bloginfo( 'name' );
                },
        ) );
}
add_action( 'customize_register', 'mytheme_customize_register' );

So as you can see, not only is the amount of code more than cut in half (also eliminating the JS file altogether), it also ensures that when a site title change is previewed it will appear with all of the PHP filters applied from core and plugins: for example wptexturize will apply so that curly quotes and dashes will appear as expected. In REST API parlance, selective refresh enables the Customizer preview to show title.rendered instead of just title.raw. (For more on this change to previewing the site title and tagline, see #33738. The previous JS-based previewing is retained for an instant low-fidelity preview while the selective refresh request returns.) Selective refresh is also the mechanism used for previewing the new custom logo feature in 4.5, ensuring that the logo image is rendered re-using the image_downsize logic in PHP without having to re-implement it in JS (keeping the code DRY).

With that recap of selective refresh out of the way, let’s turn to the focus of this post: selective refresh for widgets. When widget management was added to the Customizer in 3.9, every change to a widget required a full page refresh to preview. This resulted in a poor user experience since a full refresh can take awhile, especially on weighty pages. So selective refresh of widgets is a key usability experience improvement for widget management in the Customizer in 4.5. However, as live postMessage updates to the site title and tagline have required supporting theme code (see above), so too will theme support be required for widgets, as noted in the Selective Refresh of Widgets section from the previous post on this topic:

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.

So as noted, selective refresh for widgets will be opt-in as of 4.5 RC1 (see #35855).

What do themes and widgets need to do to opt-in for selective refresh support?

Selective refresh will be used for previewing a widget change when both the theme and the widget itself indicate support as follows:

Adding Theme Support

If your theme does not do anything fancy with its sidebars (such as using Masonry to lay out widgets), then all that you need to do is add the following to your theme:

add_theme_support( 'customize-selective-refresh-widgets' );

On the other hand, if the theme is rendering a sidebar in a unique way, then to add a bit of logic to handle the changes properly. For example, as noted previously the footer area in Twenty Thirteen consists of a sidebar that is laid out using Masonry. When a widget is added, removed, or updated, the Masonry logic needs to be re-run to update the positioning of the widgets in the sidebar. The following highlighted code is what handles this:

var widgetArea = $( '#secondary .widget-area' );
widgetArea.masonry( {
        itemSelector: '.widget',
        columnWidth: columnWidth,
        gutterWidth: 20,
        isRTL: body.is( '.rtl' )
} );

if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh  ) {
        wp.customize.selectiveRefresh.bind( 'sidebar-updated', function( sidebarPartial ) {
                if ( 'sidebar-1' === sidebarPartial.sidebarId ) {
                        widgetArea.masonry( 'reloadItems' );
                        widgetArea.masonry( 'layout' );
                }
        } );
}

Note the if statement is there so the code will only run in the Customizer preview and if selective refresh is available (that is, if they are running 4.5 or later).

Adding Widget Support

If your widget lacks any dynamic functionality with JavaScript initialization, adding support just requires adding a customize_selective_refresh flag to the $widget_options param when constructing the WP_Widget subclass. If you are enqueueing any CSS for styling the widget, you’ll also want to enqueue this unconditionally in the Customizer preview (if is_customize_preview()) so that a widget can be added to the preview and be styled properly without doing a full page refresh. For example, note these highlighted lines in a sample widget:

class Example_Widget extends WP_Widget {

        public function __construct() {
                parent::__construct(
                        'example',
                        __( 'Example', 'my-plugin' ),
                        array(
                                'description' => __( 'Selective refreshable widget.', 'my-plugin' ),
                                'customize_selective_refresh' => true,
                        )
                );

                // Enqueue style if widget is active (appears in a sidebar) or if in Customizer preview.
                if ( is_active_widget( false, false, $this->id_base ) || is_customize_preview() ) {
                        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
                }
        }

        public function enqueue_scripts() {
                wp_enqueue_style( 'my-plugin-example-widget', plugins_url( 'example-widget.css', __FILE__ ), array(), '0.1' );
        }

        /* ... */
}

On the other hand, as with sidebars in a theme, if a widget uses JavaScript for initialization, you’ll need to add logic to make sure re-initialization happens when the widget is selectively refreshed. So in addition to the above example, you must:

  1. Enqueue any dependent JS logic if is_customize_preview() just as noted above for enqueueing any CSS stylesheet.
  2. Add a handler for partial-content-rendered selective refresh events to rebuild a widget’s dynamic elements when it is re-rendered.
  3. As needed, add a handler for partial-content-moved selective refresh events to refresh partials if any dynamic elements involve iframes that have dynamically-written documents (such as the Twitter Timeline widget). (Adding this event handler should normally not be required.)

The Twitter Timeline widget in the Jetpack plugin is a good example for how to write these event handlers:

jQuery( function() {
        // Short-circuit selective refresh events if not in customizer preview or pre-4.5.
        if ( 'undefined' === typeof wp || ! wp.customize || ! wp.customize.selectiveRefresh ) {
                return;
        }

        // Re-load Twitter widgets when a partial is rendered.
        wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
                if ( placement.container ) {
                        twttr.widgets.load( placement.container[0] );
                }
        } );

        // Refresh a moved partial containing a Twitter timeline iframe, since it has to be re-built.
        wp.customize.selectiveRefresh.bind( 'partial-content-moved', function( placement ) {
                if ( placement.container && placement.container.find( 'iframe.twitter-timeline:not([src]):first' ).length ) {
                        placement.partial.refresh();
                }
        } );
} );

(This is adapted from a pending PR for Jetpack.)

Conclusion

All core themes and core widgets will have support for selective refresh in 4.5. Now it’s up to theme and plugin authors to also add support to also start taking advantage of these drastic performance improvements for previewing widget changes.



Implementing Selective Refresh Support for Widgets by Weston Ruter was originally posted at https://make.wordpress.org/core/2016/03/22/implementing-selective-refresh-support-for-widgets/

No comments:

Post a Comment