How to Create a Multi-Category Control for the Theme Customizer

This tutorial will explain how to create a custom control for the WordPress theme customizer that will allow multiple categories to be selected via checkbox inputs. Chris Molitor asked me the other day if I knew how to do this, and it sounded like a good challenge. This tutorial assumes that you already know how to work with the theme customizer. If you’re not sure how to use the theme customizer, please see the WordPress Theme Customizer: a Comprehensive Developer’s Guide first.

Update: this control can now be used multiple times within the theme customizer (previously multiple instances caused problems).

1. Create the Custom Control Class

The first thing we’ll want to do is create a new class to hold our custom control. Details on how this works and where it should be placed are covered in the “Building Custom Controls” section of the developer’s guide I just mentioned, so let’s get right into the code:

/**
 * Adds multiple category selection support to the theme customizer via checkboxes
 *
 * The category IDs are saved in the database as a comma separated string.
 */
class Cstmzr_Category_Checkboxes_Control extends WP_Customize_Control {
	public $type = 'category-checkboxes';

	public function render_content() {

		// Loads theme-customizer.js javascript file.
		echo '<script src="' . get_template_directory_uri() . '/js/theme-customizer.js"></script>';

		// Displays checkbox heading
		echo '<span class="customize-control-title">' . esc_html( $this->label ) . '</span>';

		// Displays category checkboxes.
		foreach ( get_categories() as $category ) {
			echo '<label><input type="checkbox" name="category-' . $category->term_id . '" id="category-' . $category->term_id . '" class="cstmzr-category-checkbox"> ' . $category->cat_name . '</label><br>';	
		}

		// Loads the hidden input field that stores the saved category list.
		?><input type="hidden" id="<?php echo $this->id; ?>" class="cstmzr-hidden-categories" <?php $this->link(); ?> value="<?php echo sanitize_text_field( $this->value() ); ?>"><?php
	}
}

Please note: throughout this tutorial I have prefixed function/variable/class names with the abbreviation “cstmzr”. Feel free to change this to whatever prefix you typically use.

Just a few things we’ll want to take note of before moving on. First, this function loads a file named “theme-customizer.js” from the “js” folder within the theme. You may need to modify this line if you plan on placing the javascript file in a different location. Second, we’re looping through all the categories currently being used on the site and automatically generating a checkbox for each category. Third, we’ve created a hidden form field to hold a list of the categories that have been selected. This is the only form field that actually gets saved to the database. The checkboxes only exist on the front end.

2. Add the New Control to the Customizer

This is the code we’ll use to add the new category selection control to the customizer. For the sake of simplicity, I’ve included the code to add the section, the setting, and the control. Once again, please note that all the details of these function calls are explained in the developer’s guide I linked to at the top of this post.

$wp_customize->add_section(
	'cstmzr_category_section',
	array(
		'title' => 'Category Selection',
		'priority' => 35,
	)
);

$wp_customize->add_setting( 'cstmzr_categories' );

$wp_customize->add_control(
	new Cstmzr_Category_Checkboxes_Control(
		$wp_customize,
		'cstmzr_categories',
		array(
			'label' => 'Categories',
			'section' => 'cstmzr_category_section',
			'settings' => 'cstmzr_categories'
		)
	)
);

There’s not really much to notice here, except that the add_control() call is using the Cstmzr_Category_Checkboxes_Control class we just created in step 1.

Please note: While the new control should be visible now, it won’t actually do anything yet. Step 3 will take care of that.

3. Javascript and the Customizer Settings API

Finally we have the javascript file that will listen for any changes to the category checkboxes and modify the hidden field accordingly. As I mentioned previously, this file is called “theme-customizer.js” and should be placed in the “js” folder within the theme (unless you’ve modified the code above to load it from a different location). Here’s the code we’ll want to place inside the file:

// Holds the status of whether or not the rest of the code should be run
var cstmzr_multicat_js_run = true;

jQuery(window).load(function() {

	// Prevents code from running twice due to live preview window.load firing in addition to the main customizer window.
	if( true == cstmzr_multicat_js_run ) {
		cstmzr_multicat_js_run = false;
	} else {
		return;
	}

	var api = wp.customize;

	// Loops through each instance of the category checkboxes control.
	jQuery('.cstmzr-hidden-categories').each(function(){

		var id = jQuery(this).prop('id');
		var categoryString = api.instance(id).get();
		var categoryArray = categoryString.split(',');

		// Checks/unchecks category checkboxes based on saved data.
		jQuery('#' + id).closest('li').find('.cstmzr-category-checkbox').each(function() {

			var elementID = jQuery(this).prop('id').split('-');

			if( jQuery.inArray( elementID[1], categoryArray ) < 0 ) {
				jQuery(this).prop('checked', false);
			} else {
				jQuery(this).prop('checked', true);
			}

		});		

	});

	// Sets listeners for checkboxes
	jQuery('.cstmzr-category-checkbox').live('change', function(){

		var id = jQuery(this).closest('li').find('.cstmzr-hidden-categories').prop('id');
		var elementID = jQuery(this).prop('id').split('-');

		if( jQuery(this).prop('checked' ) == true ) {
			addCategory(elementID[1], id);
		} else {
			removeCategory(elementID[1], id);
		}

	});

	// Adds category ID to hidden input.
	function addCategory( catID, controlID ) {

		var categoryString = api.instance(controlID).get();
		var categoryArray = categoryString.split(',');

		if ( '' == categoryString ) {
			var delimiter = '';
		} else {
			var delimiter = ',';
		}

		// Updates hidden field value.
		if( jQuery.inArray( catID, categoryArray ) < 0 ) {
			api.instance(controlID).set( categoryString + delimiter + catID );
		}
	}

	// Removes category ID from hidden input.
	function removeCategory( catID, controlID ) {

		var categoryString = api.instance(controlID).get();
		var categoryArray = categoryString.split(',');
		var catIndex = jQuery.inArray( catID, categoryArray );

		if( catIndex >= 0 ) {

			// Removes element from array.
			categoryArray.splice(catIndex, 1);

			// Creates new category string based on remaining array elements.
			var newCategoryString = '';
			jQuery.each( categoryArray, function() {
				if ( '' == newCategoryString ) {
					var delimiter = '';
				} else {
					var delimiter = ',';
				}
			 	newCategoryString = newCategoryString + delimiter + this;
			});

			// Updates hidden field value.
			api.instance(controlID).set( newCategoryString );
		}
	}
});

I tried to heavily comment the code above, so I won’t try to explain every line here. However, I do want to specifically point out the way the value in the hidden field is being updated. This was the trickiest part for me to figure out. I originally tried to modify the value of the hidden field directly, but that didn’t work as expected. Notice the following line near the beginning of the file:

var api = wp.customize;

The wp.customize javascript object is what we will use to interact with our hidden field. As you can see in this line, we use it to get the current value of our hidden field:

var categoryString = api.instance(id).get();

I won’t continue explaining this code line by line, but I will quickly cover how it works. After the check to make sure that the code doesn’t run twice (this can be an issue, but I’ll space you the details) and the lines we already covered, there is a function that sets the initial state of each checkbox based on the previously saved data. Next, a listener is attached to all the category checkboxes (using the “cstmzr-category-checkbox” class). Every time a checkbox is checked or unchecked, the addCategory or removeCategory function is called. These functions simply add or remove the category ID from the comma separated list in the corresponding hidden field.

4. Use the Saved Data

The saved data can be used just like the data from any other customizer field. Since we named the setting “prefix_categories” earlier in this tutorial, we will use that same name to retrieve it from the database:

get_theme_mod( 'cstmzr_categories' )

This will return a comma separated list of the selected category IDs for use in your theme.

As always, if you have any questions, comments, or suggestions, let me know via twitter.