1

So I've enabled the option to not show controls if the one control is not active, but the issue is that this works only when you refresh the customizer page.

In my example I have a boxed body control, which is a checkbox (although I used the custom control to style it a bit differently, you can find code here). If it's disabled the associated controls that can change the boxed body border width, color, background color etc. are all hidden. If you enable the boxed body you need to refresh the page to see those controls, which is UX disaster. The code is:

INITIAL CODE

add_action( 'customize_register', 'mytheme_customize_register' );
/**
 * Register customizer settings
 *
 * @see add_action('customize_register',$func)
 * @param  \WP_Customize_Manager $wp_customize WP Customize object.
 * @since 1.0.0
 */
function mytheme_customize_register( WP_Customize_Manager $wp_customize ) {

    /**
    Boxed Body
    */
    $wp_customize->add_setting( 'boxed_body', array(
        'default'           => false,
        'transport'         => 'postMessage',
        'sanitize_callback' => 'mytheme_checkbox_sanitization',
    ) );
    $wp_customize->add_control(new Toggle_Checkbox_Custom_Control( $wp_customize, 'boxed_body', array(
        'label'       => esc_html__( 'Boxed Body', 'mytheme' ),
        'description' => esc_html__( 'Check this to enable boxed body layout', 'mytheme' ),
        'type'        => 'checkbox',
        'section'     => 'section_general',
    ) ) );

    /**
     * Callback function for boxed body choice
     *
     * @param  object $control Control object.
     * @return boolean
     */
    function boxed_body_choice( $control ) {
        if ( 1 === $control->manager->get_setting( 'boxed_body' )->value() ) {
            return true;
        } else {
            return false;
        }
    }

    /**
    Boxed Body Border Color
    */
    $wp_customize->add_setting( 'boxed_body_border_color', array(
        'default'           => '',
        'transport'         => 'postMessage',
        'sanitize_callback' => 'sanitize_hex_color',
    ) );
    $wp_customize->add_control(new WP_Customize_Color_Control( $wp_customize, 'boxed_body_border_color', array(
        'label'    => esc_html__( 'Boxed Body Border Color', 'mytheme' ),
        'settings' => 'boxed_body_border_color',
        'section'  => 'section_general',
        'active_callback' => 'boxed_body_choice',
    ) ) );

}

add_action( 'customize_preview_init', 'mytheme_customizer_live_preview' );
/**
 * Live preview script enqueue
 *
 * @since 1.0.0
 */
function mytheme_customizer_live_preview() {
    wp_enqueue_script( 'mytheme-themecustomizer', get_template_directory_uri() . '/inc/customizer/js/customizer.js?v=' . rand(), array( 'jquery', 'customize-preview' ), '', true );
}

I've tried following Weston Ruter's example on contextual controls here and gist, but no luck. I've added in my customizer.js

(function($) {

    // Add callback for when the boxed_body setting is toggled.
    wp.customize( 'boxed_body', function(setting) {
        var isBoxedBodyToggled, linkSettingValueToControlActiveState;

        /**
         * Determine whether the boxed body associated options should be displayed.
         *
         * @returns {boolean} Is toggled?
         */
        isBoxedBodyToggled = function() {
            return '' !== setting.get();
        };

        /**
         * Update a control's active state according to the boxed_body setting's value.
         *
         * @param {wp.customize.Control} control Boxed body control.
         */
        linkSettingValueToControlActiveState = function( control ) {
            var setActiveState = function() {
                control.active.set( isBoxedBodyToggled() );
            };

            // FYI: With the following we can eliminate all of our PHP active_callback code.
            control.active.validate = isBoxedBodyToggled;

            // Set initial active state.
            setActiveState();

            /*
             * Update activate state whenever the setting is changed.
             * Even when the setting does have a refresh transport where the
             * server-side active callback will manage the active state upon
             * refresh, having this JS management of the active state will
             * ensure that controls will have their visibility toggled
             * immediately instead of waiting for the preview to load.
             * This is especially important if the setting has a postMessage
             * transport where changing the setting wouldn't normally cause
             * the preview to refresh and thus the server-side active_callbacks
             * would not get invoked.
             */
            setting.bind( setActiveState );
        };

        // Call linkSettingValueToControlActiveState on the site title and tagline controls when they exist.
        wp.customize.control( 'boxed_body_border_width', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_border_color', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_border_style', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_bg_image', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_bg_image_repeat', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_bg_image_size', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_bg_image_position', linkSettingValueToControlActiveState );
        wp.customize.control( 'boxed_body_bg_image_attachment', linkSettingValueToControlActiveState );

    });

})(jQuery);

But on customizer refresh I get

Uncaught TypeError: wp.customize.control is not a function(…)

On the

wp.customize.control( 'boxed_body_border_width', linkSettingValueToControlActiveState );

And the toggle won't hide or show the controls. I've made some changes to fit the custom toggle control, so I'm not checking against 'blank' but rather '' in the isBoxedBodyToggled function.

When I fiddle in DevTools I can see that wp.customize is a function, not an object. In the example this should be an object (I guess).

Any help would be appreciated.

NEW CODE

So I've noticed that I don't need that many controls since I have added

add_theme_support( 'custom-background' );

which gives me built in control for the body background, so I've made it like this:

add_action( 'customize_register', 'mytheme_customize_register' );
/**
 * Register customizer settings
 *
 * @see add_action('customize_register',$func)
 * @param  \WP_Customize_Manager $wp_customize WP Customize object.
 * @since 1.0.0
 */
function mytheme_customize_register( WP_Customize_Manager $wp_customize ) {

    /**
    ------------------------------------------------------------
    SECTION: Body Settings
    ------------------------------------------------------------
    */
    $wp_customize->add_section( 'background_image', array(
        'title'    => esc_html__( 'Body Settings', 'mytheme' ),
        'priority' => 0,
    ) );

    /**
    Boxed Body
    */
    $wp_customize->add_setting( 'boxed_body', array(
        'default'           => false,
        'transport'         => 'postMessage',
        'sanitize_callback' => 'mytheme_checkbox_sanitization',
    ) );
    $wp_customize->add_control(new Toggle_Checkbox_Custom_Control( $wp_customize, 'boxed_body', array(
        'label'       => esc_html__( 'Boxed Body', 'mytheme' ),
        'description' => esc_html__( 'Check this to enable boxed body layout', 'mytheme' ),
        'type'        => 'checkbox',
        'section'     => 'background_image',
        'priority'    => 0,
    ) ) );

    /**
     * Callback function for boxed body choice
     *
     * @param  object $control Control object.
     * @return boolean
     */
    function boxed_body_choice( $control ) {
        if ( 1 === $control->manager->get_setting( 'boxed_body' )->value() ) {
            return true;
        } else {
            return false;
        }
    }

    /**
    Boxed Body Border Color
    */
    $wp_customize->add_setting( 'boxed_body_border_color', array(
        'default'           => '',
        'transport'         => 'postMessage',
        'sanitize_callback' => 'sanitize_hex_color',
    ) );
    $wp_customize->add_control(new WP_Customize_Color_Control( $wp_customize, 'boxed_body_border_color', array(
        'label'           => esc_html__( 'Boxed Body Border Color', 'mytheme' ),
        'settings'        => 'boxed_body_border_color',
        'section'         => 'background_image',
        'active_callback' => 'boxed_body_choice',
        'priority'        => 0,
    ) ) );

}

add_action( 'customize_preview_init', 'mytheme_customizer_live_preview' );
/**
 * Live preview script enqueue
 *
 * @since 1.0.0
 */
function mytheme_customizer_live_preview() {
    wp_enqueue_script( 'mytheme-themecustomizer', get_template_directory_uri() . '/inc/customizer/js/customizer.js?v=' . rand(), array( 'jquery', 'customize-preview' ), '', true );
}

And the customizer.js looks like this

(function($, api) {
    'use strict';

    //Boxed Body Toggle
    api( 'boxed_body', function(value) {
        value.bind(function(newval) {
            if (newval) {
                $('body').wrapInner('<div class="boxed_body_wrapper" />');
            } else {
                $('.boxed_body_wrapper').contents().unwrap();
            }
        });
    });

    // Add callback for when the boxed_body setting is toggled.
    api( 'boxed_body', function(value) {
        value.bind(function(setting){
            var isBoxedBodyToggled, linkSettingValueToControlActiveState;

            /**
             * Determine whether the boxed body associated options should be displayed.
             *
             * @returns {boolean} Is toggled?
             */
            isBoxedBodyToggled = function() {
                return '' !== setting.get();
            };

            /**
             * Update a control's active state according to the boxed_body setting's value.
             *
             * @param {api.Control} control Boxed body control.
             */
            linkSettingValueToControlActiveState = function( control ) {
                var setActiveState = function() {
                    control.active.set( isBoxedBodyToggled() );
                };

                // FYI: With the following we can eliminate all of our PHP active_callback code.
                control.active.validate = isBoxedBodyToggled;
                // Set initial active state.
                setActiveState();

                /*
                 * Update activate state whenever the setting is changed.
                 * Even when the setting does have a refresh transport where the
                 * server-side active callback will manage the active state upon
                 * refresh, having this JS management of the active state will
                 * ensure that controls will have their visibility toggled
                 * immediately instead of waiting for the preview to load.
                 * This is especially important if the setting has a postMessage
                 * transport where changing the setting wouldn't normally cause
                 * the preview to refresh and thus the server-side active_callbacks
                 * would not get invoked.
                 */

                setting.bind( setActiveState );
            };

            // Call linkSettingValueToControlActiveState on the site title and tagline controls when they exist.
            api.control( 'boxed_body_border_color', linkSettingValueToControlActiveState );
        });
    });

})(jQuery, wp.customize);

I still get

customizer.js?v=1256189197&ver=4.7:111 Uncaught TypeError: api.control is not a function

at i (jquery.js?ver=1.12.4:2)

at Object.fireWith (jquery.js?ver=1.12.4:2)

at Function.set (customize-base.min.js?ver=4.7:1)

at Function.d [as set] (jquery.js?ver=1.12.4:2)

at c (customize-preview.min.js?ver=4.7:1)

at f. (customize-preview.min.js?ver=4.7:1)

at i (jquery.js?ver=1.12.4:2)

at Object.fireWith (jquery.js?ver=1.12.4:2)

at f.trigger (customize-base.min.js?ver=4.7:1)

If I add 'customize-controls' as a dependency then I get the error:

Uncaught TypeError: Cannot read property 'dirty' of undefined

at Function.initialize (customize-controls.min.js?ver=4.7:1)

at f.e.Class (customize-base.min.js?ver=4.7:1)

at f [as constructor] (customize-base.min.js?ver=4.7:1)

at new f (customize-base.min.js?ver=4.7:1)

at Function.create (customize-base.min.js?ver=4.7:1)

at String.c (customize-preview.min.js?ver=4.7:1)

at Function.each (jquery.js?ver=1.12.4:2)

at f. (customize-preview.min.js?ver=4.7:1)

at i (jquery.js?ver=1.12.4:2)

at Object.fireWith (jquery.js?ver=1.12.4:2)

Still not sure what I'm doing wrong :\

EDIT

I used the wrong action hook when enqueuing. I created another file in which I added:

add_action( 'customize_controls_enqueue_scripts', 'utter_customizer_control_toggle' );

/**
 * Custom contextual controls
 *
 * @since 1.0.0
 */
function utter_customizer_control_toggle() {
    wp_enqueue_script( 'utter-contextualcontrols', UTTER_TEMPPATH . '/inc/customizer/js/customizer-contextual.js?v=' . rand(), array( 'customize-controls' ), false );
}

This gets rid of all errors. And toggle works the first time, but not when I toggle back, but I guess that has to do with the custom control I'm using instead of regular checkbox. I'll probably fix this soon :D

2
  • You should wrap any controls logic in wp.customize.bind( 'ready', function(){ ... } ); to ensure everything is available. Also, you need to make sure that you add customize-controls as a dependency for the script you are enqueueing. Commented Dec 20, 2016 at 6:36
  • Adding customize-controls as a dependency produced the error: Uncaught TypeError: Cannot read property 'dirty' of undefined with a bunch of functions. I tried with the use of binding but that didn't work as well. The odd thing is that when I fiddle in the console, I can get the code that is in my js script. It almost looks like #31320 ticket. Which is also weird because I have other controls that are working fine...
    – dingo_d
    Commented Dec 20, 2016 at 7:05

1 Answer 1

2

The answer was to 'decouple' live preview and contextual control. If I place them together, I cannot use the same action hook - live won't work on customize_controls_enqueue_scripts and contextual control won't work on customize_preview_init.

So the answer is:

add_action( 'customize_controls_enqueue_scripts', 'mytheme_customizer_control_toggle' );
add_action( 'customize_preview_init', 'mytheme_customizer_live_preview' );
/**
 * Live preview script enqueue
 *
 * @since 1.0.0
 */
function mytheme_customizer_live_preview() {
    wp_enqueue_script( 'mytheme-themecustomizer', get_template_directory_uri() . '/inc/customizer/js/customizer.js?v=' . rand(), array( 'jquery', 'customize-preview' ), false );
}

/**
 * Custom contextual controls
 *
 * @since 1.0.0
 */
function mytheme_customizer_control_toggle() {
    wp_enqueue_script( 'mytheme-contextualcontrols', get_template_directory_uri() . '/inc/customizer/js/customizer-contextual.js?v=' . rand(), array( 'customize-controls' ), false );
}

The contextual control code is

( function( api ) {
    'use strict';

    api( 'boxed_body', function(setting) {
        var isBoxedBodyToggled, linkSettingValueToControlActiveState;

        /**
         * Determine whether the boxed body associated options should be displayed.
         *
         * @returns {boolean} Is toggled?
         */
        isBoxedBodyToggled = function() {
            return '' !== setting.get();
        };

        /**
         * Update a control's active state according to the boxed_body setting's value.
         *
         * @param {api.Control} control Boxed body control.
         */
        linkSettingValueToControlActiveState = function( control ) {
            var setActiveState = function() {
                control.active.set( isBoxedBodyToggled() );
            };

            // FYI: With the following we can eliminate all of our PHP active_callback code.
            control.active.validate = isBoxedBodyToggled;
            // Set initial active state.
            setActiveState();

            /*
             * Update activate state whenever the setting is changed.
             * Even when the setting does have a refresh transport where the
             * server-side active callback will manage the active state upon
             * refresh, having this JS management of the active state will
             * ensure that controls will have their visibility toggled
             * immediately instead of waiting for the preview to load.
             * This is especially important if the setting has a postMessage
             * transport where changing the setting wouldn't normally cause
             * the preview to refresh and thus the server-side active_callbacks
             * would not get invoked.
             */

            setting.bind( setActiveState );
        };

        // Call linkSettingValueToControlActiveState on the site title and tagline controls when they exist.
        api.control( 'boxed_body_border_width', linkSettingValueToControlActiveState );
        api.control( 'boxed_body_border_color', linkSettingValueToControlActiveState );
        api.control( 'boxed_body_border_style', linkSettingValueToControlActiveState );
    });

}( wp.customize ) );

The issue I have now is that my custom control doesn't work properly, but this is a different issue that I can easily solve.

Thanks to Weston Ruter for pointing me in the right direction :)

WORKING EDIT

For some reason, the above code only toggled the controls once, so I rewrote it like this

( function( api ) {
    'use strict';

    api.bind( 'ready', function() {

        api( 'boxed_body', function(setting) {
            var linkSettingValueToControlActiveState;

            /**
             * Update a control's active state according to the boxed_body setting's value.
             *
             * @param {api.Control} control Boxed body control.
             */
            linkSettingValueToControlActiveState = function( control ) {
                var visibility = function() {
                    if ( true === setting.get() || 1 === setting.get() ) {
                        control.container.slideDown( 180 );
                    } else {
                        control.container.slideUp( 180 );
                    }
                };

                // Set initial active state.
                visibility();
                //Update activate state whenever the setting is changed.
                setting.bind( visibility );
            };

            // Call linkSettingValueToControlActiveState on the border controls when they exist.
            api.control( 'boxed_body_border_width', linkSettingValueToControlActiveState );
            api.control( 'boxed_body_border_color', linkSettingValueToControlActiveState );
            api.control( 'boxed_body_border_style', linkSettingValueToControlActiveState );
        });

    });

}( wp.customize ) );

And this seems to be working fine. I used the Twenty Seventeen as a guide, they toggle the custom color scheme in a similar way.

2
  • 4
    Good. Yes, you cannot normally have the JS for both the controls pane and the preview in the same JS, so so you need to enqueue them as separate files, the first with customize-controls as a dependency and enqueued at customize_controls_enqueue_scripts and the second with customize-preview as the script dependency and enqueued in wp_enqueue_scripts via customize_preview_init. Commented Dec 20, 2016 at 14:58
  • 1
    This is great! Thanks, I was looking for something similar and this really helped ;)
    – WPExplorer
    Commented Jul 31, 2017 at 2:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.