Toward a Complete JavaScript API for the Customizer

The CustomizerCustomizer Tool built into WordPress core that hooks into most modern themes. You can use it to preview and modify many of your site’s appearance settings. is the first true JS-driven feature in core. That’s awesome, especially coming out of WCSF where JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. has been highlighted so prominently between Backbone.js, the WP REST API, Node.js, and socket.io. The Customizer has a great architecture with models for settings, controls, watchable-values, collections, events, and asynchronous callbacks. Nevertheless, the JavaScript APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. in the Customizer is incomplete.

Existing Challenges

When widgets were added to the Customizer in 3.9, adding widgetWidget A WordPress Widget is a small block that performs a specific function. You can add these widgets in sidebars also known as widget-ready areas on your web page. WordPress widgets were originally created to provide a simple and easy-to-use way of giving design and structure control of the WordPress theme to the user. controls to sidebarSidebar A sidebar in WordPress is referred to a widget-ready area used by WordPress themes to display information that is not a part of the main content. It is not always a vertical column on the side. It can be a horizontal rectangle below or above the content area, footer, header, or any where in the theme. sections required direct DOM manipulation. For controls there is at least a Control model to manage state For sections and panels, however, there are no JSJS JavaScript, a web scripting language typically executed in the browser. Often used for advanced user interfaces and behaviors. models at all, and so adding them dynamically is even more of a challenge. And this is the exact challenge that Nick Halsey’s Menu Customizer pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party currently has to struggle through.

When Customizer panels were added in 4.0 to group all widget area sections into a “Widgets” panel, a bug was introduced whereby shift-clicking a widget in the preview no longer revealed the widget control in the Customizer pane because the sections were inside of the collapsed Widgets panel: there were no models to represent the state for whether or not a panel or section were currently expanded. Without models, a fix of this would require more messy DOM traversal to check if parent accordion sections were expanded or not. Storing data in the DOM is bad.

In 4.0 the concept of contextual controls were added to the Customizer. This allowed controls to be registered with an active_callback, such as is_front_page, which the preview would execute and pass back up to the Customizer pane to then show/hide the control based on which URLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org was being previewed. This worked well, except when all controls in a section were inactive: then the result was a Customizer section without any visible controls. Instead, the expected behavior would be for the section to automatically collapse as well when all controls inside become inactive. Again, this problem stems from the fact that there is no JS model to represent a section and to list out the controls associated with it.

For the past three weeks I’ve been focused on fleshing out the Customizer API to address these challenges, and to facilitate doing new dynamic things in the Customizer. The parent ticketticket Created for both bug reports and feature development on the bug tracker. for this is #28709: Improve/introduce Customizer JS models for Controls, Sections, and Panels.

Models for Panels and Sections

As noted above, there is a wp.customize.Control model, and then there is a wp.customize.control collection (yes, it is singular) to store all control instances. So to follow the pattern established by controls, in the patch there is a wp.customize.Panel and wp.customize.Section, along with wp.customize.panel and wp.customize.section collections (both singular again). So just as with controls, you can iterate over panels and sections via:

wp.customize.panel.each( function ( panel ) { /* ... */ } );
wp.customize.section.each( function ( section ) { /* ... */ } );

Relating Controls, Sections, and Panels together

When registering a new control in PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher, you pass in the parent section ID:

$wp_customize->add_control( 'blogname', array(
	'label' => __( 'Site Title' ),
	'section' => 'title_tagline',
) );

In the proposed JavaScript API, a control’s section can be obtained predictably:

id = wp.customize.control( 'blogname' ).section(); // => title_tagline

To get the section object from the ID, you just look up the section by the ID as normal: wp.customize.section( id ).

You can move a control to another section using this section state as well, here moving it to the Navigation section:

wp.customize.control( 'blogname' ).section( 'nav' );

Likewise, you can get a section’s panel ID in the same way:

id = wp.customize.section( 'sidebar-widgets-sidebar-1' ).panel(); // => widgets

You can go the other way as well, to get the children of panels and sections:

sections = wp.customize.panel( 'widgets' ).sections();
controls = wp.customize.section( 'title_tagline' ).controls();

You can use these to move all controls from one section to another:

_.each( wp.customize.section( 'title_tagline' ).controls(), function ( control ) {
	control.section( 'nav' );
});

Contextual Panels and Sections

Also just as with controls, when you invoke $wp_customize->add_section() you can pass an active_callback param to indicate whether the section is relevant to the currently-previewed URL; the same goes for panels. A good example of a contextual section is only showing the “Static Front PageStatic Front Page A WordPress website can have a dynamic blog-like front page, or a “static front page” which is used to show customized content. Typically this is the first page you see when you visit a site url, like wordpress.org for example.” section if the preview is currently on the front-page:

function contextual_static_front_page_section( $wp_customize ) {
	$wp_customize->get_section( 'static_front_page' )->active_callback = 'is_front_page';
}
add_action( 'customize_register', 'contextual_static_front_page_section', 11 );

Nevertheless, this will not usually be needed because a section inherits its active state from its control children (and a panel inherits from its section children), via the new isContextuallyActive() method. If all controls within a section become inactive, then the section will automatically become inactive.

As with controls, Panel and Section instances have an active state (a wp.customize.Value instance). When the active state changes, the panel, section, and control instances invoke their respective onChangeActive method, which by default slides the container element up and down, if false and true respectively. There are also activate() and deactivate() methods now for manipulating this active state, for panels, sections, and controls:

wp.customize.section( 'nav' ).deactivate(); // slide up
wp.customize.section( 'nav' ).activate({ duration: 1000 }); // slide down slowly
wp.customize.section( 'colors' ).deactivate({ duration: 0 }); // hide immediately
wp.customize.section( 'nav' ).deactivate({ completeCallback: function () {
	wp.customize.section( 'colors' ).activate(); // show after nav hides completely
} });

Note that manually changing the active state would only stick until the preview refreshes or loads another URL. The activate()/deactivate() methods are designed to follow the pattern of the new expanded state.

Expanded State

As noted above, in 4.0 when panels were introduced, a bug was introduced whereby shift-clicking a widget in the preview fails to show the widget control if the Widgets panel is not already open. With the proposed changes, panels, sections, and (widget) controls have an expanded state (another wp.customize.Value instance). When the state changes, the onChangeExpanded method is called which by will handle Panels sliding in and out, and sections sliding up and down (and widget controls up and down, as they are like sections). So now when a widget control needs to be shown, the control’s section and panel can simply have their expanded state to true in order to reveal the control. Expanding a section automatically expands its parent panel. Expanding a widget control, automatically expands its containing section and that section’s panel.

As with activate()/deactivate() to manage the active state, there are expand() and collapse() methods to manage the expanded state. These methods also take a similar params object, including duration and completeCallback. The params object for Section.expand() accepts an additional parameter “allowMultiple” to facilitate dragging widget controls between sidebar sections. By default expanding one section will automatically collapse all other open sections, and so this param overrides that. You can use this, for instance, to expand all sections at once so you can see all controls without having to click to reveal each accordion section one by one:

wp.customize.section.each(function ( section ) {
	if ( ! section.panel() ) {
		section.expand({ allowMultiple: true });
	}
});

Focusing

Building upon the expand()/collapse() methods for panels, sections, and controls, these models also support a focus() method which not only expands all of the necessary element, but also scrolls the target container into view and puts the browser focus on the first focusable element in the container. For instance, to expand the “Static Front Page” section and focus on select dropdown for the “Front page”:

wp.customize.control( 'page_on_front' ).focus()

This naturally fixes the #29529, mentioned above.

The focus functionality is used to implement autofocus: deep-linking to panels, sections, and controls inside of the customizer. Consider these URLs:

  • …/wp-adminadmin (and super admin)/customize.php?autofocus[panel]=widgets
  • …/wp-admin/customize.php?autofocus[section]=colors
  • …/wp-admin/customize.php?autofocus[control]=blogname

This can be used to add a link on the widgets admin page to link directly to the widgets panel within the Customizer.

Priorities

When registering a panel, section, or control in PHP, you can supply a priority parameter. This value is now stored in a wp.customize.Value instance for each respective Panel, Section, and Control instance. For example, you can obtain the priority for the widgets panel via:

priority = wp.customize.panel( 'widgets' ).priority(); // => 110

You can then dynamically change the priority and the Customizer panel will automatically re-arrange to reflect the new priorities:

wp.customize.panel( 'widgets' ).priority( 1 ); // move Widgets to the top

Custom types for Panels and Sections

Just as Customizer controls can have custom types (ColorControlImageControlHeaderControl…) which have custom behaviors in JS:

wp.customize.controlConstructor.FooControl = wp.customize.Control.extend({ /*...*/ });

So too can Panels and Sections have custom behaviors in the proposed changes. A type parameter can be passed when creating a Panel or Section, and then in JavaScript a constructor corresponding to that type can be registered. For instance:

PHP:

add_action( 'customize_register', function ( $wp_customize ) {
	class WP_Customize_Harlem_Shake_Section extends WP_Customize_Section {
		public $type = 'HarlemShake';
	}
	$section = new WP_Customize_Harlem_Shake_Section(
		$wp_customize,
		'harlem_shake',
		array( 'title' => __( 'Harlem Shake' ) )
	);
	$wp_customize->add_section( $section );
	$wp_customize->add_setting( 'harlem_shake_countdown', array(
		'default' => 15,
	));
	$wp_customize->add_control( 'harlem_shake_countdown', array(
		'label' => __( 'Countdown' ),
		'section' => 'harlem_shake',
		'setting' => 'harlem_shake_countdown',
		'type' => 'number',
	));
});

JS:

wp.customize.sectionConstructor.HarlemShake = wp.customize.Section.extend({
	shake: function () {
		// This can be invoked via wp.customize.section( 'harlem_shake' ).shake();
		console.info( 'SHAKE!!' );
	}
});

Next Steps

  • Continue discussion on parent ticket #28709: Improve/introduce Customizer JS models for Controls, Sections, and Panels.
  • Review JavaScript changes in pull request. Anyone is free to open a PR to onto the existing branchbranch A directory in Subversion. WordPress uses branches to store the latest development code for each major release (3.9, 4.0, etc.). Branches are then updated with code for any minor releases of that branch. Sometimes, a major version of WordPress and its minor versions are collectively referred to as a "branch", such as "the 4.0 branch". on GitHubGitHub GitHub is a website that offers online implementation of git repositories that can easily be shared, copied and modified by other developers. Public repositories are free to host, private repositories require a paid subscription. GitHub introduced the concept of the ‘pull request’ where code changes done in branches by contributors can be reviewed and discussed before being merged be the repository owner. https://github.com/ to make changes. Props to Ryan Kienstra (@ryankienstra) and Nick Halsey (@celloexpressions) for their contributions.
  • Update logic for adding widget controls to use new API (adding widgets is using the old pseudo-API and it is currently broken); allow controls to be added manually.
  • Work with Nick Halsey to make sure that dynamically-created sections and controls suit the needs of Menu Customizer, and make sure that it works for other plugins like Customize Posts.
  • Build upon the initial QUnit tests to add coverage for existing JS API and newly added API (#28579).
  • Harden the logic for animating the Customizer panel into view.
  • Get feedback from other CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. devs and get initial patchpatch A special text file that describes changes to code, by identifying the files and lines which are added, removed, and altered. It may also be referred to as a diff. A patch can be applied to a codebase for testing. committed.

Thanks to Nick Halsey (@celloexpressions) for his proofreading and feedback on the drafts of this blogblog (versus network, site) post.

#customize, #menu-customizer, #menus, #widget-customizer