Dependently-Contextual Customizer Controls

Authored by

In my previous post I discussed how to get the Customizer preview to navigate to a URL when a section is expanded, where this is basically turning the concept of contextual controls on its head: instead of showing a control/section/panel when navigating to a given URL, focusing on the control/section/panel can cause the relevant URL to be actively loaded into the preview. To continue the discussion of contextual controls, I’ve periodically seen questions regarding making controls contextual based on the input for some other control (most recently in #37439 but also in a comment in Otto’s What’s new with the Customizer post). I’ll explore these “dependently-contextual Customizer controls” in this post.

An example of this kind of control can be readily seen in the Static Front Page section of the Customizer. When you initially open this section you see “Front page displays” followed by two radio buttons with “Your latest posts” and “A static page”:

Page On Front: Your Latest Posts

When you select the latter option, then two new options appear to select the static page to use for the front page and the page for posts:

A Static Page

So you can see in this example that the visibility of the “Front page” and “Posts page” controls is dependent on the value you’ve selected for “Front page displays”. This is what I mean by the controls being “conditionally contextual”.

Now, an actual framework for contextual controls was introduced in WordPress 4.0 via #27993, long after the Static Front Page section first made its appearance. As such, the controls in the Static Front Page section actually don’t yet make use of this newer the API for contextual controls (the actual visibility is being directly manipulated with jQuery; see #29948). As can be read in the Customizer handbook page, contextual controls make use of an active_callback property that is defined on the control you register in PHP (or via the customize_control_active filter). If the active_callback returns false then the control is hidden, and if true then it is displayed. The active_callback for each control is invoked every time the Customizer preview loads/refreshes, and each time the returned boolean value is sent in a message from the Customizer preview to the Customizer pane. The activeControls are received by the Customizer pane and are then set on the each respective wp.customize.Control instance’s active state (a wp.customize.Value model), and the control listens to changes to this state in order to toggle its visibility. (Important to note here that if you do set a control’s active state in JS, like via wp.customize.control( 'foo' ).active.set( false ), then the state will be reset to the server’s value the next time the preview refreshes since the return value from the active_callback will be re-synced each time. I’ll note a strategy to work around this below.)

Contextual Site Title and Tagline

To help illustrate how to implement contextual controls it will be helpful to work through an example. The site title and tagline are staple controls in the Customizer:

Site Title and Tagline Controls

The controls usually appear in the Site Identity section but the site title and tagline don’t always appear on the site. There is a checkbox for “Display Site Title and Tagline” which controls whether or not they are displayed in the header. Let’s see how we can make this checkbox make the site title and tagline controls dependently contextual: if the checkbox is checked, show the controls, but if it is unchecked, hide the controls. I’ll be working through the succinctly-named plugin Dependently-Contextual Site Title and Tagline Customizer Controls.

First thing to note is that the checkbox control for “Display Site Title and Tagline” is linked to a setting identified as header_textcolor. When the checkbox is unchecked the setting gets the value of “blank” and the site title and tagline are hidden. Note also that the header_textcolor setting has a refresh transport, so we can rely entirely on the server-side active_callback to manage the visibility of the controls for the site title and tagline. Here’s a minimal code snippet for doing this:

add_action( 'customize_register', function( $wp_customize ) {
	$header_text_controls = array_filter( array(
		$wp_customize->get_control( 'blogname' ),
		$wp_customize->get_control( 'blogdescription' ),
	) );
	foreach ( $header_text_controls as $control ) {
		$control->active_callback = function( $control ) {
			$setting = $control->manager->get_setting( 'header_textcolor' );
			if ( ! $setting ) {
				return true;
			}
			return 'blank' !== $setting->value();
		};
	}
}, 11 );

This does the trick, mostly. If you toggle the checkbox you’ll immediately notice the preview refresh and once it has finished you’ll then notice the site title and tagline controls also toggle their visibility, once the preview syncs the active_callback’s return values to the controls’ active states.

One UX problem with this implementation: the site title and tagline controls toggle their visibility after the preview refreshes. It would be more satisfying to the user if these controls toggled their visibility immediately upon toggling the checkbox. To do this, we need to implement the same logic in JS, which would also be absolutely required if the checkbox had a setting with a postMessage transport. Here’s a minimal example of a JS implementation that does the same as the PHP one:

wp.customize( 'header_textcolor', function( setting ) {
	var setupControl = function( control ) {
		var setActiveState, isDisplayed;
		isDisplayed = function() {
			return 'blank' !== setting.get();
		};
		setActiveState = function() {
			control.active.set( isDisplayed() );
		};
		setActiveState();
		setting.bind( setActiveState );
	};
	wp.customize.control( 'blogname', setupControl );
	wp.customize.control( 'blogdescription', setupControl );
} );

Now that’s a better UX: the controls now toggle their visibility immediately. Nevertheless, we have the downside here of having logic duplicated in PHP and JS (it’s not DRY). We can actually implement it entirely in JS without any PHP at all if we force the control’s active state to ignore whatever is being sent from the server. We can do this by defining the validate method on the active state. To implement this in our example here, we could amend the setupControl function in the above JS with:

control.active.validate = isDisplayed;

With that in place, we can eliminate all of our PHP code. Note that this technique is especially relevant when controls are added dynamically with JS and there isn’t any corresponding control registered in PHP since the Customizer will currently interpret controls missing on the server as being inactive on the client (see #37270 for how we might improve the handling of active state for dynamically-created controls/sections/panels). In the Customize Posts plugin, each post gets its own Customizer section and all of these sections and the controls inside of them are all added dynamically by JavaScript without any corresponding sections or controls registered on the server. So in Customize Posts, you can see both post sections and post controls make use of this active.validate hack.

Hopefully this has helped shed light on how you might implement dependently-contextual controls in the Customizer. See the aforementioned plugin gist for annotated source.

19 thoughts on “Dependently-Contextual Customizer Controls”

  1. Hello Weston,

    Wow this is absolutely amazing. Your scripts are a goldmine! Thank you so much for all your efforts. Wish I would have come across this a few weeks ago. I have been looking for this kind of solution for almost 2 weeks. My current solution is quite ugly. I’m using jQuery with on.change to add css attributes to hide the controls, which for some reason isn’t very stable. Sometimes I have to click twice for jQuery to fire.

    Anyway, one question though. Since Javascript is not my strong side, I was wondering if you have an elegant solution when it comes to hiding multiple controls.

    I try to explain.
    For example:
    In my panel, I have a radio button with 3 options (value: a,b,c) and I also have multiple text boxes.

    When pressing radio button a, I would like to only show text box 1 and 2, when pressing radio button b, show textbox 3… etc. there’s not proper order, as I would like to be flexible on deciding what boxes show when clicking what button.

    Of course I could simply copy the entire setupControl function over and over for each control, but this becomes very big and messy. There has to be a more elegant solution

    As mention JS is not my strong side, and I am still learning.
    I was thinking of a solution where I can pass the values of the radio button via argument, but this is where my javascript knowledge breaks down. I can’t figure out how to create a 2nd argument for the setupControl when “control” seems to be a global variable.

    var setupControl = function ( control, displayVal ) {
    			
    	var isDisplayed = function() {
    		return displayVal === setting.get();
    	};
    	
    	var setActiveState;
    	
    	setActiveState = function() {
    		control.active.set( isDisplayed() );
    	};
    	
    	control.active.validate = isDisplayed;
    	setActiveState(); //Set state on customizer load
    	setting.bind( setActiveState );
    };
    
    wp.customize.control( 'mytheme_textbox1', setupControl('a, b') );
    wp.customize.control( 'mytheme_textbox2', setupControl('b') );
    wp.customize.control( 'mytheme_textbox3', setupControl('c') );
    
    1. @Maria: Thanks for the kind words.

      Good question. The functional concept you’re looking for in JavaScript is called “currying”. This is an approach where you create a function that returns another function. And control isn’t a global variable but rather is the argument that is passed when the setupControl callback is called. So you can change your setupControl to have this general structure:

      var setupControl = function( displayVal ) {
          return function( control ) {
      	var isDisplayed = function() {
      		return displayVal === control.setting.get();
      	};
              /* ... */
          };
      };
      

      You can see that displayVal is then persisted as part of the closure of the enclosed function that is returned, so that when the returned function is called it has access to the displayVal that was originally passed in when setupControl was called.

      Hope this helps!

  2. Thank you so much! It worked 🙂 Also thank you very much for letting me know about the “currying”. Started reading up on it.

    Here is my final code.

    //theme-customizer-panel.js
    wp.customize( 'demo_buttons', function( setting ) {    
    
        var setupControl = function( hideInput ) {
    
            return function( control ) {
                var isDisplayed = function() {
                    return $.inArray(setting.get(), hideInput) === -1;
                };
    
                var setActiveState = function() {
                    control.active.set( isDisplayed() );
                };
    
                control.active.validate = isDisplayed;
                setActiveState();
                setting.bind( setActiveState );
            };
        };
    
        wp.customize.control( 'demo_slider', setupControl(['1', '2']) ); //Hide demo_slider when demo_buttons 1 or 2 is selected
    
    } );

    Except now, when using redux framework for my fields, it doesn’t apply any redux styles to the fields that were hidden on customizer load, and then made visible again. But I this has nothing to do with your code 🙂

    Again, thank you very much for your help, really appreciate it.

  3. Hello,

    Great article, thanks for sharing and explaining this amazing technique.
    I’m actually building a theme on local server (I’ve turned it from HTML/CSS to WP) and using kirki (before kirki i wouldn’t dare to touch the customizer, now i understand a lot more about it).
    My question , hope you can help me solve it, is about hiding a control when a default WP widget is not active.
    I’ve asked Aristeides, https://wordpress.org/support/topic/active_callback-on-is_active_widget/, he explain to me that i’ll have to write a custom js to evaluate if the widget is active using the WP Customizer API.
    Sincerly, I’ve learned by myself HTML/CSS/(english too) and can reproduce almost everything for php in the codex and elswhere, even implementing any sophisticated js plugin… so writing a custom js for an active callback is out of my scope 🙂

    For now i have this color control for the text of a text widget
    I want to be able to hide it if this widget is not active.

    
    Kirki::add_field( 'blogger_config', array(
    	'type'        => 'color',
    	'settings'    => 'blog_page_text_widget_color',
    	'label'       => __( 'Text Widget Color', 'blogger' ),
    	'section'     => 'blog_page_sidebar_colors',
    	'default'     => '#333',
    	'priority'    => 80,
            'transport'=> 'postMessage',
            'js_vars'   => array(
    		array(
                    'element'  => '.banner-right-text .textwidget',
                    'function' => 'css',
                    'property' => 'color',
    		),
    	),
            'output' => array(
                array(
                    'element'  => '.banner-right-text .textwidget',
                    'property' => 'color',
                ),
            ),
            'active_callback' => 'my_custom_callback', //how with custom js?
    	'alpha'       => true,
    ) );
    

    Can you please help/guide me ?
    Thanks a lot in advance for any help (I’ve searched a lot).

    Best regards

    1. @Lebcit Yes, here is an implementation of is_widget_active using the customizer JS API:

      function isWidgetActive( idBase ) {
      	var active = false; 
      	wp.customize.control.each( function( control ) {
      		if ( control.extended( wp.customize.Widgets.WidgetControl ) && idBase === control.params.widget_id_base && control.active.get() ) { 
      			active = true; 
      		} } ); 
      	return active;
      }
  4. Hello Weston,

    First of all, thanks a lot for taking the time for a novice developer like me. I’ve realize after my question that you’re a WordPress core committer & contributor and that I’ve used your Selective Refresh Support for Widgets and readed other articles by you on the core 🙂 So thanks again for your precious time.
    I think that you already have an article about Contextual Customizer Controls on the core, unable to find it…

    I’ve put the code you gave me at the bottom of customizer.js
    Always using kirki, this is what I’ve tried :
    1- created an active callback function in customizer.php and called it in the control… no result
    2- tried a lot (really a lot) of variations of this active callback function… no result
    3- even tried to echo the fuction you gave me in the active callback function 🙂 … no result
    4- tried to put it alone and enqueue it… no result
    5- tried unlogical things… 🙂 of course no result

    is_active_widget is hidding or showing the control when i use
    if(is_active_widget) or if(!is_active_widget) with or without the code you gave me.

    Please, what I’m doing wrong ?
    Thanks again for your patience, guidance and time.

    Best regards

    1. @Lebcit I’m afraid Kirki won’t help you there.

      The active_callback on your control uses The WP active_callback you can see here: https://developer.wordpress.org/themes/advanced-topics/customizer-api/#contextual-controls-sections-and-panels
      That means that they refer to PHP functions and not JS functions.

      To do what you want you’d have to use the Customizer’s Javascript API: https://developer.wordpress.org/themes/advanced-topics/customizer-api/#the-customizer-javascript-api

    2. @Lebcit yeah, @Ari is right, you need to use JS instead of PHP. And my blog post here is all about using JS instead of PHP for managing the active state. To force the active state to only respect what JS says and ignore what PHP says, see the discussion around control.active.validate = isDisplayed above. In your case, the isDisplayed function would incorporate the isActiveWidget sample code I wrote. You’d also want to listen for when any control is added or removed in JavaScript to make sure the active state gets updated, something like the following:

      // ...
      isDisplayed = function() {
      	return isWidgetActive( 'foo' );
      };
      updateActiveState = _.debounce( function() {
      	control.active.set( isDisplayed() );
      } );
      wp.customize.control.bind( 'add', updateActiveState );
      wp.customize.control.bind( 'remove', updateActiveState );
  5. @Ari, @Weston, thank you both very much for all you help, guidance and time.
    I didn’t manage to make it work. I’ll be waiting WP to add the is_active_widget conditonnal tag as a direct php callback for the customizer, since it’s JS api is for now too advanced for me 🙂
    Thank you both again and à un de ces jours !

    1. @Lebcit I don’t think core is going to be adding additional to specifically support toggling the active state of a control based whether or not there is a given widget type active. There can’t be a PHP callback here because widgets are added dynamically with JavaScript and previewed with selective refresh. So unless you disable selective refresh support in a theme to cause the preview to refresh with each widget change (not recommended), you’ll need a JS solution such as I’ve outlined above.

      1. Hello @Weston,

        I think that I’m getting their now 🙂
        As you know, I’ve recently learned jQuery basics while working on a plugin and asked how to get into the DOM of the customizer with jQuery, thanks again for your great explanation that helped me a lot. Learning jQuery allowed me to understand a lot about the customizer JS API.

        This is what I’ve done :
        1- in customizer.php

        function blogger_kirki_sections( $wp_customize ) {
        $wp_customize->add_setting( 'text_widget_color_setting', array(
        'type' => 'theme_mod', // or 'option'
        'capability' => 'edit_theme_options',
        'default' => '#333',
        'transport' => 'postMessage',
        'sanitize_callback' => 'sanitize_hex_color',
        ) );
        $wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'text_widget_color_setting', array(
        'label' => __( 'Text Widget Color', 'blogger' ),
        'section' => 'blog_page_sidebar_colors',
        'settings' => 'text_widget_color_setting',
        'priority' => 80,
        ) ) );
        }
        add_action( 'customize_register', 'blogger_kirki_sections' );
        // AFTER THAT
        function blogger_css_customizer() {
        ?>

        .banner-right-text .textwidget { color: ; }

        <?php
        }
        add_action( 'wp_head', 'blogger_css_customizer' );

        2- in functions.php

        function blogger_customizer_live_preview()
        {
        wp_enqueue_script(
        'blogger-themecustomizer', //Give the script an ID
        get_template_directory_uri().'/assets/js/blogger-customizer.js',//Point to file
        array( 'jquery','customize-preview' ), //Define dependencies
        '', //Define a version (optional)
        true //Put script in footer?
        );
        }
        add_action( 'customize_preview_init', 'blogger_customizer_live_preview' );

        function custom_customize_enqueue() {
        wp_enqueue_script( 'testing', get_template_directory_uri() . '/assets/js/testing.js', array( 'jquery', 'customize-controls' ), false, true );
        }
        add_action( 'customize_controls_enqueue_scripts', 'custom_customize_enqueue' );

        3- in blogger-customizer.js

        jQuery(document).ready(function( $ ) {

        //Update text widget color in real time...
        wp.customize( 'text_widget_color_setting', function( value ) {
        value.bind( function( newval ) {
        $('.banner-right-text .textwidget').css('color', newval );
        } );
        } );

        } );

        4- in testing.js

        jQuery(document).ready(function( $ ){

        function isWidgetActive( idBase ) {
        var active = false;
        wp.customize.control.each( function( control ) {
        if ( control.extended( wp.customize.Widgets.WidgetControl ) && idBase === control.params.widget_id_base && control.active.get() ) {
        active = true;
        } } );
        return active;
        }

        wp.customize( 'text_widget_color_setting', function( setting ) {
        var setupControl = function( control ) {
        var setActiveState, isDisplayed, updateActiveState;
        isDisplayed = function() {
        return isWidgetActive( 'text_widget_color_setting' );
        };
        setActiveState = function() {
        control.active.set( isDisplayed() );
        };
        updateActiveState = _.debounce( function() {
        control.active.set( isDisplayed() );
        } );
        wp.customize.control.bind( 'add', updateActiveState );
        wp.customize.control.bind( 'remove', updateActiveState );
        setActiveState();
        setting.bind( setActiveState );
        };

        wp.customize.control( 'text_widget_color_setting', setupControl );
        } );

        } );

        I’ve put the above code at first in blogger-customizer.js it returned wp.customize.control is not a function, this is why I’ve putted it in testing.js.

        While in the preview, if I remove the widget, the control is removed, but when I refresh the preview, the control gets back !
        What I’m doing wrong ?

        Thanks a lot in advance for your help 🙂

  6. @Weston, Thanks for sharing active.validate hack.

    From the existing comment’s created helper function which helps to toggle the controls.

    /**
     * Helper showControlIfhasValues()
     */
    function showControlIfhasValues( setting, ExpectedValues  ) {
    
        return function( control ) {
    
        	//	Check the current value in the array of ExpectedValues
            var isDisplayed = function() {
                return $.inArray( setting.get(), ExpectedValues ) !== -1;
            };
    
            var setActiveState = function() {
                control.active.set( isDisplayed() );
            };
    
            control.active.validate = isDisplayed;
            setActiveState();
            setting.bind( setActiveState );
        };
    }
    
    /**
     * Test Control Dependency
     */
    wp.customize( 'select-control-1', function( setting ) {
    
        //  Show control 'text-control-1' if control 'select-control-1' value has 'one' or 'two'
    	wp.customize.control( 'text-control-1', showControlIfhasValues( setting, ['one','two']) );
    	
        //  Show control 'text-control-2' if control 'select-control-1' value has 'one'.
        wp.customize.control( 'text-control-2', showControlIfhasValues( setting, ['one']) );
    
        //  Show control 'text-control-2' if control 'select-control-1' value has 'one'.
        wp.customize.control( 'text-control-4', showControlIfhasValues( setting, ['one']) );
    
        //  Show control 'text-control-2' if control 'select-control-1' value has 'one' or 'three'.
        wp.customize.control( 'text-control-3', showControlIfhasValues( setting, ['one', 'three']) );
    } );

    Gist: https://gist.github.com/maheshwaghmare/f4f32bbeb2c79d4767fb30f54c83a8a5

  7. I am having some logic errors in my head, I can’t seem to figure out how I can adjust this script to have 3 ore more dependencies for a controller.

    I have a controller field, that I only want to display if 3 other controller fields have a certain values.

    Below is my poor attempt, and it doesn’t seem to work. Most likely I took a completely wrong approach and with a major logic error. Unfortunately I am new to JavaScript, but WordPress has become my passion and I would like to help push the customizer dependency part.

    Maybe one of you JavaScript wizard people can adjust it, or has an idea on how to create the script from Weston, or better the function from Mahesh Waghmare, so it can have unlimited dependencies from other controllers.

    //Please note, this code does not work :(
    
    function showControlIfhasValues( setting, ExpectedValues, ExternalDependancies ) {
    	
    		ExternalDependancies = typeof ExternalDependancies !== 'undefined' ? ExternalDependancies : '';
    		
    		var isDisplayed2 = function() {
    			if ( ExternalDependancies !== '' ) {
    				var arrayLength = ExternalDependancies.length;
    
    				var output;
    				
    				for ( var i = 0; i < arrayLength; i++ ) {
    					if ( $.inArray( wp.customize.value( ExternalDependancies[i][0] )(), ExternalDependancies[i][1] ) === -1 ){
    						var output = false;
    					}
    				}
    				
    				if (output == undefined) {
    					return true;
    				} else {
    					return false;
    				}
    
    			} else {
    				return true;
    			}
    		}
    		//console.log(isDisplayed2());
    		
    		return function( control ) {
    
    		//	Check the current value in the array of ExpectedValues
    		var isDisplayed = function() {
    				return $.inArray( setting.get(), ExpectedValues ) !== -1 && isDisplayed2() ;
    		};
    
    		var setActiveState = function() {
    			control.active.set( isDisplayed() );
    		};
    
    		control.active.validate = isDisplayed;
    		setActiveState();
    		setting.bind( setActiveState );
    		};
    	}
    
    //Test Control
    wp.customize( 'test_field_toggle', function( setting ) {
    		wp.customize.control( 'test_field_1', showControlIfhasValues3( setting, ['1','2'], [['test_field_2', ['2', '3', '4'], 'test_field_3', ['2', '3']]] ) );
    	} );
    
    1. Maybe to explain it a bit better, my issue is with nested controllers. For example I have a controller called “Show Background” with option “On/Off”.
      When this is on, it will show a controller called “Color”, as well as another field called “Advanced”, with option “On/Off”.
      When that “Advanced” button is “On” it displays a few more options.
      My issue is, when the “Advanced” field is “On” and I switch the “Show Background” to “Off”, they all should disappear, however the “Advanced Options” are still visible.

  8. The only way I got it to work, is by a more “static” approach, where I pre-determine on exactly how many dependencies it should check for.
    Obviously the down side is, that it’s not very flexible, since in my example, ALWAYS exactly 3 dependencies are requires. I am looking for a dynamic approach, where I can enter as many as I would like.

    //Checking for exactly 3 dependencies
    	function showControlIfhasValues3( setting, ExpectedValues, setting2, ExpectedValues2, setting3, ExpectedValues3 ) {
    
    		return function( control ) {
    
    		//	Check the current value in the array of ExpectedValues
    		var isDisplayed = function() {
    			return $.inArray( setting.get(), ExpectedValues ) !== -1 && $.inArray( wp.customize.value( setting2 )(), ExpectedValues2 ) !== -1 && $.inArray( wp.customize.value( setting3 )(), ExpectedValues3 ) !== -1;
    		};
    	//	console.log('status: '+ $.inArray( setting.get(), ExpectedValues ) !== -1);
    		var setActiveState = function() {
    			control.active.set( isDisplayed() );
    		};
    
    		control.active.validate = isDisplayed;
    		setActiveState();
    		setting.bind( setActiveState );
    		};
    	}
    
  9. Hi,

    I have a select option with three choice. When i choose certain option, it should show show some controls and hide some another controls. How can i achieve it?

  10. This is a great article and discussion.
    I’m stuck, wanting to load a background image upload panel to work when a specific custom page template is in use.
    I’m trying
    ‘active_callback’ => ‘is_page_template (‘donations-page.php’)’,
    but getting a php error.
    Everything else about the template is working… just trying to get an uploader for a widget background image working.
    The theme has it working for the front page… but I want to add it to these page templates.

Leave a Reply to Lebcit Cancel reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.