Simple, free resources to help you build a better WordPress website.

Sign up for our newsletter.

Exploring the Theme Customizer Code in WordPress 3.4

Being a theme developer means staying up-to-date on the latest WordPress features and changes to ensure our themes remain compatible and enjoy enhanced features that live in harmony with core WordPress code and utilities. The new Theme Customizer is certainly one of the most exciting features for theme developers in quite some time, allowing us to add our theme customization controls into a live customization panel that makes it super easy to view and update your theme’s design quickly and easily.

I want to give big props to Otto for providing a thorough explanation of the implementation details for developers (which is currently subject to change, since it is beta code, after all). It’s a helpful post so I’d recommend reading that first and coming back here to dive a little deeper into the code. I’m going to walk you through a quick overview of what the Theme Customizer code is actually doing and how it can be extended in your own themes to provide custom controls.

We’re currently working on implementing controls that already exist in the UpThemes Framework as a part of the new Theme Customizer so all UpThemes customers can have access to these features shortly after WordPress 3.4 is released. We just recently implemented support for the Settings API and now, with the new Theme Customizer, we’re a few short steps away from being able to quickly add support for that.

The PHP Stuffs

The new Theme Customizer code actually lives in a few files inside your wp-includes folder that we’re going to look at today:

First, we’re going to take a look at class-wp-customize.php. This is where the Theme Customizer class is defined and does the bulk of the work. One area I want to point out is where the Theme Customizer checks to see that it is active:


public function setup_theme() {
    if ( ! isset( $_REQUEST['customize'] ) || 'on' != $_REQUEST['customize'] )
        return;

    $this->start_previewing_theme();
    show_admin_bar( false );
}

This function essentially tells the customizer to load on the front-end by checking for a query string variable of “customize” that is currently set to “on” and then the magic happens.

The next part to take note of is the start_previewing_theme() function. This function sits right below the setup_theme() function:


public function start_previewing_theme() {
    if ( $this->is_preview() || false === $this->theme || ( $this->theme && ! $this->theme->exists() ) )
        return;

    // Initialize $theme and $original_stylesheet if they do not yet exist.
    if ( ! isset( $this->theme ) ) {
        $this->theme = wp_get_theme( $_REQUEST['theme'] );
        if ( ! $this->theme->exists() ) {
            $this->theme = false;
            return;
        }
    }

    $this->original_stylesheet = get_stylesheet();

    $this->previewing = true;

    add_filter( 'template', array( $this, 'get_template' ) );
    add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
    add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );

    // @link: http://core.trac.wordpress.org/ticket/20027
    add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
    add_filter( 'pre_option_template', array( $this, 'get_template' ) );

    // Handle custom theme roots.
    add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
    add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );

    do_action( 'start_previewing_theme', $this );
}

As you can see inside this function, there are a number of filters added as well as an action that is fired to officially start previewing the theme.

There are a few functions here that are important to note for adding settings, sections, and controls. I’ve dropped them in below for reference, just so you can see exactly how WordPress is building out the theme customization panel. This is very similar to how the Settings API works.


public function add_setting( $id, $args = array() ) {
    if ( is_a( $id, 'WP_Customize_Setting' ) )
        $setting = $id;
    else
        $setting = new WP_Customize_Setting( $this, $id, $args );

    $this->settings[ $setting->id ] = $setting;
}

public function add_section( $id, $args = array() ) {
    if ( is_a( $id, 'WP_Customize_Section' ) )
        $section = $id;
    else
        $section = new WP_Customize_Section( $this, $id, $args );

    $this->sections[ $section->id ] = $section;
}

public function add_control( $id, $args = array() ) {
    if ( is_a( $id, 'WP_Customize_Control' ) )
        $control = $id;
    else
        $control = new WP_Customize_Control( $this, $id, $args );

    $this->controls[ $control->id ] = $control;
}

Putting it all together

Now, I realize that what I’ve shared above isn’t all that helpful until you see some code that you can actually use as an example for registering your own settings. One of the largest functions in the entire file is the one that registers your default settings, like so:



public function register_controls() {

    /* Custom Header */

    $this->add_section( 'header', array(
        'title'          => __( 'Header' ),
        'theme_supports' => 'custom-header',
        'priority'       => 20,
    ) );

    $this->add_setting( 'header_textcolor', array(
        // @todo: replace with a new accept() setting method
        // 'sanitize_callback' => 'sanitize_hexcolor',
        'theme_supports' => array( 'custom-header', 'header-text' ),
        'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
    ) );

    $this->add_control( 'display_header_text', array(
        'settings' => 'header_textcolor',
        'label'    => __( 'Display Header Text' ),
        'section'  => 'header',
        'type'     => 'checkbox',
    ) );

    $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
        'label'   => __( 'Text Color' ),
        'section' => 'header',
    ) ) );

    // Input type: checkbox
    // With custom value
    $this->add_setting( 'header_image', array(
        'default'        => get_theme_support( 'custom-header', 'default-image' ),
        'theme_supports' => 'custom-header',
    ) );

    $this->add_control( new WP_Customize_Header_Image_Control( $this ) );

    /* Custom Background */

    $this->add_section( 'background', array(
        'title'          => __( 'Background' ),
        'theme_supports' => 'custom-background',
        'priority'       => 30,
    ) );

    // Input type: Color
    // With sanitize_callback
    $this->add_setting( 'background_color', array(
        'default'           => get_theme_support( 'custom-background', 'default-color' ),
        'sanitize_callback' => 'sanitize_hexcolor',
        'theme_supports'    => 'custom-background',
        'transport'         => 'postMessage',
    ) );

    $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
        'label'   => __( 'Background Color' ),
        'section' => 'background',
    ) ) );

    $this->add_setting( 'background_image', array(
        'default'        => get_theme_support( 'custom-background', 'default-image' ),
        'theme_supports' => 'custom-background',
    ) );

    $this->add_control( new WP_Customize_Image_Control( $this, 'background_image', array(
        'label'          => __( 'Background Image' ),
        'section'        => 'background',
        'context'        => 'custom-background',
    ) ) );

    $this->add_setting( 'background_repeat', array(
        'default'        => 'repeat',
        'theme_supports' => 'custom-background',
    ) );

    $this->add_control( 'background_repeat', array(
        'label'      => __( 'Background Repeat' ),
        'section'    => 'background',
        'type'       => 'radio',
        'choices'    => array(
            'no-repeat'  => __('No Repeat'),
            'repeat'     => __('Tile'),
            'repeat-x'   => __('Tile Horizontally'),
            'repeat-y'   => __('Tile Vertically'),
        ),
    ) );

    $this->add_setting( 'background_position_x', array(
        'default'        => 'left',
        'theme_supports' => 'custom-background',
    ) );

    $this->add_control( 'background_position_x', array(
        'label'      => __( 'Background Position' ),
        'section'    => 'background',
        'type'       => 'radio',
        'choices'    => array(
            'left'       => __('Left'),
            'center'     => __('Center'),
            'right'      => __('Right'),
        ),
    ) );

    $this->add_setting( 'background_attachment', array(
        'default'        => 'fixed',
        'theme_supports' => 'custom-background',
    ) );

    $this->add_control( 'background_attachment', array(
        'label'      => __( 'Background Attachment' ),
        'section'    => 'background',
        'type'       => 'radio',
        'choices'    => array(
            'fixed'      => __('Fixed'),
            'scroll'     => __('Scroll'),
        ),
    ) );

    /* Nav Menus */

    $locations      = get_registered_nav_menus();
    $menus          = wp_get_nav_menus();
    $menu_locations = get_nav_menu_locations();
    $num_locations  = count( array_keys( $locations ) );

    $this->add_section( 'nav', array(
        'title'          => __( 'Navigation' ),
        'theme_supports' => 'menus',
        'priority'       => 40,
        'description'    => sprintf( _n('Your theme supports %s menu. Select which menu you would like to use.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ) . "\n\n" . __('You can edit your menu content on the Menus screen in the Appearance section.'),
    ) );

    if ( $menus ) {
        $choices = array( 0 => __( '— Select —' ) );
        foreach ( $menus as $menu ) {
            $truncated_name = wp_html_excerpt( $menu->name, 40 );
            $truncated_name = ( $truncated_name == $menu->name ) ? $menu->name : trim( $truncated_name ) . '…';
            $choices[ $menu->term_id ] = $truncated_name;
        }

        foreach ( $locations as $location => $description ) {
            $menu_setting_id = "nav_menu_locations[{$location}]";

            $this->add_setting( $menu_setting_id, array(
                'sanitize_callback' => 'absint',
                'theme_supports'    => 'menus',
            ) );

            $this->add_control( $menu_setting_id, array(
                'label'   => $description,
                'section' => 'nav',
                'type'    => 'select',
                'choices' => $choices,
            ) );
        }
    }

    /* Static Front Page */
    // #WP19627

    $this->add_section( 'static_front_page', array(
        'title'          => __( 'Static Front Page' ),
    //  'theme_supports' => 'static-front-page',
        'priority'       => 50,
        'description'    => __( 'Your theme supports a static front page.' ),
    ) );

    $this->add_setting( 'show_on_front', array(
        'default'        => get_option( 'show_on_front' ),
        'capability'     => 'manage_options',
        'type'           => 'option',
    //  'theme_supports' => 'static-front-page',
    ) );

    $this->add_control( 'show_on_front', array(
        'label'   => __( 'Front page displays' ),
        'section' => 'static_front_page',
        'type'    => 'radio',
        'choices' => array(
            'posts' => __( 'Your latest posts' ),
            'page'  => __( 'A static page' ),
        ),
    ) );

    $this->add_setting( 'page_on_front', array(
        'type'       => 'option',
        'capability' => 'manage_options',
    //  'theme_supports' => 'static-front-page',
    ) );

    $this->add_control( 'page_on_front', array(
        'label'      => __( 'Front page' ),
        'section'    => 'static_front_page',
        'type'       => 'dropdown-pages',
    ) );

    $this->add_setting( 'page_for_posts', array(
        'type'           => 'option',
        'capability'     => 'manage_options',
    //  'theme_supports' => 'static-front-page',
    ) );

    $this->add_control( 'page_for_posts', array(
        'label'      => __( 'Posts page' ),
        'section'    => 'static_front_page',
        'type'       => 'dropdown-pages',
    ) );

    /* Site Title & Tagline */

    $this->add_section( 'strings', array(
        'title'    => __( 'Site Title & Tagline' ),
        'priority' => 5,
    ) );

    $this->add_setting( 'blogname', array(
        'default'    => get_option( 'blogname' ),
        'type'       => 'option',
        'capability' => 'manage_options',
    ) );

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

    $this->add_setting( 'blogdescription', array(
        'default'    => get_option( 'blogdescription' ),
        'type'       => 'option',
        'capability' => 'manage_options',
    ) );

    $this->add_control( 'blogdescription', array(
        'label'      => __( 'Tagline' ),
        'section'    => 'strings',
    ) );
}

If you pay close attention to this section, you can see how each and every default preview setting is registered. Each “option” is actually defined by a pair of definitions, with an add_setting and add_control function being fired for each one:


    $this->add_setting( 'blogdescription', array(
        'default'    => get_option( 'blogdescription' ),
        'type'       => 'option',
        'capability' => 'manage_options',
    ) );

    $this->add_control( 'blogdescription', array(
        'label'      => __( 'Tagline' ),
        'section'    => 'strings',
    ) );

The important pieces of these settings to note are the ‘type’ and the ‘capability’ bits. These pieces allow you to define what control type it is and which users can access it.

This is where we can jump over to the class-wp-customize-control.php file and take a peek at the different types of default controls we have access to:

  • text
  • checkbox
  • radio
  • select
  • dropdown-pages

protected function render_content() {
    switch( $this->type ) {
        case 'text':
            ?>
            <label>
                <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
                <input type="text" value="<?php echo esc_attr( $this->value() ); ?>" <?php $this->link(); ?> />
            </label>
            
            <label>
                <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
                <input type="checkbox" value="<?php echo esc_attr( $this->value() ); ?>" <?php $this->link(); checked( $this->value() ); ?> />
            </label>
            
            <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
            
                <label>
                    <input type="radio" value="<?php echo esc_attr( $value ); ?>" name="<?php echo esc_attr( $name ); ?>" <?php $this->link(); checked( $this->value(), $value ); ?> />
                    <?php echo esc_html( $label ); ?><br/>
                </label>
                
            <label>
                <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
                <select <?php $this->link(); ?>>
                    <?php
                    foreach ( $this->choices as $value => $label )
                        echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . $label . '</option>';
                    ?>
                </select>
            </label>
            
        <label>
            <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
            <div>
                <a href="#" class="button-secondary upload"><?php _e( 'Upload' ); ?></a>
                <a href="#" class="remove"><?php _e( 'Remove' ); ?></a>
            </div>
        </label>
        
        <label class="customize-image-picker">
            <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>

            <div class="customize-control-content">
                <div class="dropdown preview-thumbnail">
                    <div class="dropdown-content">
                        <?php if ( empty( $src ) ): ?>
                            <img style="display:none;" />
                        <?php else: ?>
                            <img src="<?php echo esc_url( $src ); ?>" />
                        <?php endif; ?>
                        <div class="dropdown-status"></div>
                    </div>
                    <div class="dropdown-arrow"></div>
                </div>
            </div>

            <div class="library">
                <ul>
                    <?php foreach ( $this->tabs as $id => $tab ): ?>
                        <li data-customize-tab='<?php echo esc_attr( $id ); ?>'>
                            <?php echo esc_html( $tab['label'] ); ?>
                        </li>
                    <?php endforeach; ?>
                </ul>
                <?php foreach ( $this->tabs as $id => $tab ): ?>
                    <div class="library-content" data-customize-tab='<?php echo esc_attr( $id ); ?>'>
                        <?php call_user_func( $tab['callback'] ); ?>
                    </div>
                <?php endforeach; ?>
            </div>

            <div class="actions">
                <a href="#" class="remove"><?php _e( 'Remove Image' ); ?></a>
            </div>
        </label>
        
        <div class="upload-dropzone">
            <?php _e('Drop a file here or <a href="#" class="upload">select a file</a>.'); ?>
        </div>
        
        <div class="uploaded-target"></div>
        <div class="uploaded-target"></div>
            <a href="#" class="thumbnail" data-customize-image-value="<?php echo esc_url( $header['url'] ); ?>">
                <img src="<?php echo esc_url( $header['thumbnail_url'] ); ?>" />
            </a>
        
            <a href="#" class="thumbnail" data-customize-image-value="<?php echo esc_url( $header['url'] ); ?>">
                <img src="<?php echo esc_url( $header['thumbnail_url'] ); ?>" />
            </a>
        <?php endforeach;
    }
}

The Difference Between Using a Default and Custom Control

If you compare the two control declarations below, you’ll see one of them uses a default setting control of 'type' => 'checkbox' and the other actually instantiates a new object for WP_Customize_Color_Control(). Notice the array of arguments passed in are quite a bit different.


        $this->add_control( 'display_header_text', array(
            'settings' => 'header_textcolor',
            'label'    => __( 'Display Header Text' ),
            'section'  => 'header',
            'type'     => 'checkbox',
        ) );

        $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
            'label'   => __( 'Text Color' ),
            'section' => 'header',
        ) ) );


Make sure to browse the files I linked earlier in the post and learn about how the Theme Customizer works and it’ll make life much easier when implementing your custom options. Also note that these functions will all change at some point, so please reference the current version of the code in trunk (which is what I linked above). We are certainly excited about the future of the UpThemes Framework with the new Theme Customizer and we’ll definitely be posting our progress in implementing these features in the near future so make sure to subscribe to the RSS feed to stay updated.

2 Comments

  1. Avatar
    devinsays says:

    I’ll be curious how you implement it.  I’m also trying to decide how to best incorporate it into the Options Framework.
     
    Also, FYI, class-wp-customize.php is a broken link.

Leave a Reply

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