Remove theme mod after button-click in the customizer

In the upcoming version of my Hannover theme, there should be an improved user experience in the Customizer besides a reworked design and code base. Among other things, the user should be able to remove sections with a button click (like known from removing a menu in the customizer). After that, not only the section should be removed from the customizer, but also the corresponding theme mods need to be removed from the database. This post shows my solution for that.

Procedure summarized

Of course, it would be cool if we could use the JS API to remove theme mods after a button click. I cannot find a solution for that so we will use PHP for this part. The steps we will take:

  1. We add a customize control for the delete button.
  2. We add a hidden customize control with a setting, which is used later in the PHP part to determine if the delete button was pressed.
  3. We listen to the clicks on the delete button and change the value of the hidden control on click.
  4. After saving the customize settings, we check via PHP if the button was clicked and remove the theme mods.

1. Creating the delete button

For the delete button, I used core’s approach for the button that removes a menu. For that, we create a small control template and include it into the customizer via PHP:

<?php
// Include customize control templates into the customizer.
add_action( 'customize_controls_print_footer_scripts', 'hannover_customize_control_templates' );

/**
 * Prints control templates into the customizer.
 */
function hannover_customize_control_templates() { ?>
	<script type="text/html" id="tmpl-hannover-delete-portfolio-category-page-button">
		<div class="hannover-customize-control-text-wrapper delete-portfolio-category-page">
			<button type="button" class="button-link button-link-delete">
				<?php _e( 'Delete category page', 'hannover' ); ?>
			</button>
		</div>
	</script>
<?php }

Now we can use the template for a control in the Customize JS API, and here is the code:

/**
 * Custom JavaScript functions for the customizer controls.
 *
 * @version 2.0.0
 *
 * @package Hannover
 */
;(function (api) {
	api.bind('ready', function () {
		// Create remove button control.
		api.control.add(
			new api.Control('portfolio_category_page[1][delete]', {
				section: 'hannover_portfolio_category_page_section[1]',
				templateId: 'hannover-delete-portfolio-category-page-button'
			})
		);
	});
})(wp.customize);

Important is the value for templateId, that corresponds to the id attribute of the script element in hannover_customize_control_templates() – minus the tmpl- prefix. The number in the name of control and setting is for identification because the user can add new sections and its controls and settings dynamically.

With that, we created the delete button and added it to a section. To make that work, we need a section of course, but that is not part of this article. I described the creation of panels, sections, and controls in my post »Creating panels, sections, and controls with the Customize JS API«.

2. Creating hidden control and setting as removal flag

To check if a delete button was clicked in the PHP part later, we create a hidden control with a setting – its value will be changed after a click on the delete button. This is the JS part:

// Add setting for removal flag.
api.add(new api.Setting('portfolio_category_page[1][deleted]'));

// Add control for removal flag.
api.control.add(
	new api.Control('portfolio_category_page[1][deleted]', {
		setting: 'portfolio_category_page[1][deleted]',
		type: 'number',
		section: 'hannover_portfolio_category_page_section[1]',
		active: false,
	})
);

I described how to create a setting with the JS API in my article »Creating settings with the Customize JS API«. We connect the control with the setting via the setting key, declare the number type, set the same value for section like for the delete button and set active to false. With that, the control is not visible.

Like said in the linked article, we need a small part of PHP to save settings that are created with the JS API:

// Filter the dynamically created customizer settings.
add_filter( 'customize_dynamic_setting_args', 'hannover_filter_dynamic_setting_args', 10, 2 );

/**
 * Filters a dynamic setting's constructor args.
 *
 * For a dynamic setting to be registered, this filter must be employed
 * to override the default false value with an array of args to pass to
 * the WP_Customize_Setting constructor.
 *
 * @link https://wordpress.stackexchange.com/a/286503/112824
 *
 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
 * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
 *
 * @return array|false
 */
function hannover_filter_dynamic_setting_args( $setting_args, $setting_id ) {
	// Create array of ID patterns.
	$id_patterns = [
		'portfolio_category_page_deleted' => '/^portfolio_category_page\[(\d+)\]\[deleted\]/',
	];

	// Match for the deleted portfolio category page setting.
	if ( preg_match( $id_patterns['portfolio_category_page_deleted'], $setting_id, $matches ) ) {
		$setting_args = [
			'type' => 'theme_mod',
		];
	}

	return $setting_args;
}

3. Listening to clicks on the delete button and update the hidden control

The complete JS code for listening to clicks and running the actions after that:

// Add click event listener to the delete button.
var deleteButton = document.querySelector('.delete-portfolio-category-page button');
deleteButton.addEventListener('click', function () {
	// Close the section.
	api.section('hannover_portfolio_category_page_section[1]').collapse();

	// Set flag that the setting needs to be removed.
	api.control('portfolio_category_page[1][deleted]', function (control) {
		control.setting.set(-1);
	});

	// Remove the section.
	api.section('hannover_portfolio_category_page_section[1]').active(false);
});

After a click, we close the section first. After that, we set the value of the hidden control to -1 and hide the section, so the user cannot see and open it in his current customizer setting.

4. Removing theme mods

The PHP code for removing the theme mods:

// Remove theme mods after portfolio category page was removed.
add_action( 'customize_save_after', 'hannover_customize_save_after' );

/**
 * Fires after customize settings are saved.
 *
 * @param WP_Customize_Manager $manager Instance of WP_Customize_Manager.
 */
function hannover_customize_save_after( $manager ) {
	// Get the theme mods.
	$theme_mods = get_theme_mods();

	// Loop them.
	foreach ( $theme_mods['portfolio_category_page'] as $id => $portfolio_category_page_theme_mod ) {
		// Check if the delete flag is set.
		if ( isset( $portfolio_category_page_theme_mod['deleted'] ) && - 1 === $portfolio_category_page_theme_mod['deleted'] ) {
			// Code inspired by the remove_theme_mod() function.
			unset( $theme_mods['portfolio_category_page'][ $id ] );

			// Check if we have no more portfolio category pages and if so, unset the array index.
			if ( empty( $theme_mods['portfolio_category_page'] ) ) {
				unset( $theme_mods['portfolio_category_page'] );
			} // End if().

			// Check if that was the last theme mod and if so, remove the entry from the database.
			if ( empty ( $theme_mods ) ) {
				remove_theme_mods();
			} else {
				// Get theme slug and update the theme mod option.
				$theme = get_option( 'stylesheet' );
				update_option( "theme_mods_$theme", $theme_mods );
			} // End if().
		} // End if().
	} // End foreach().
}

We use the customize_save_after action, which fires after saving the customizer settings. With get_theme_mods() we fetch all theme mods and loop the $theme_mods['portfolio_category_page'] entries (because of my naming with the square brackets, the theme mods are stored in a multidimensional array in the database).

Inside the loop, we check if the deleted key exists and if its value is -1. If so, we remove the theme mods from the $theme_mods array. After that, we check if $theme_mods['portfolio_category_page'] is empty now and remove it if that is the case. If this was also the last theme mod (that is the case, if $theme_mods is empty at that point), we remove the theme mod entry from the database with remove_theme_mods().

Otherwise, we get the slug of the theme and update the theme mod field with an update_option() call, that gets the option name as the first param and the modified $theme_mods array as the second.

And with that, we are at the end ? Not exactly easy and probably not the most elegant solution, but it does what I want 🙂 If any of you have suggestions for improvements to this relatively complex procedure, please write a comment!

Related posts

Leave a Comment

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