Can a widget in the Customizer be “single-use” (i.e. disabled after 1 instance has been added)?

I’m on a nightly quest to build a custom single-use widget.

As soon as one instance of it has been added to a sidebar panel in the Customizer, its control on the Available Widgets panel should be displayed as disabled (or alternatively, disappear entirely).

This is how it would look (note the visually “disabled” widget on the right):

…

The custom Widget is registered and all, but I’m stuck at the single-use requirement.

Specs

  • Scale is not a problem. It’s ok to assume only 1 registered sidebar. The approach can be to limit the use of the widget to either 1 per sidebar, or to 1 per all registered sidebars—both would be equally fine for now.
  • The Widgets page on the back-end is not a problem. This is not required to work equally well on the Widgets page under Appearance on the back-end. It only has to work in the Customizer.

Questions

  1. About 250 years ago, all widgets used to be “single-use”. Does anyone know of a legit way to bring those times back and make a custom widget be used only 1x through the Widget API?
  2. If not (which I’m assuming after having dug through a fair amount of core files), I’d probably defer to a CSS-based approach (pointer-events, pseudo element overlay, whatever). Would any kind soul help my very limited Customizer/JavaScript knowledge with a basic approach on how to add/remove a dedicated CSS class to the widget control in the “available” panel (the one on the right) once an instance of said widget has been added/removed to the sidebar panel?

What I tried so far

  • Dug through a fair chunk of core files.
  • Read this, and this, but neither one seems practical.
  • Tinkered with jQuery events widget-added, widget-updated, and widget-synced, but miss an event for “widget deleted”.

Thanks tons in advance!


Update: Haven’t put my proof of concept on a public repo yet, would will do, of course.

Solution

I wrapped the solution kindly shared by kraftner below in a proof of concept plugin on GitHub.

1
1

Approach

So I’ve looked into this and my approach is this:

  1. When launching the customizer go through all sidebars and disable the widget as soon as you find any usage of it
  2. Whenever a sidebar is changed do that again.

Disclaimer

This has the following limitations:

  1. This is the first time I’ve dabbled in playing with the JS API of the Customizer. So I might be doing inefficient stuff here, but hey, it works 😉
  2. Only cares about the Customizer (as stated in the question)
  3. Doesn’t do any kind of server-side validation. It merely hides the UI, so if you’re worried about someone circumventing the UI this is incomplete/insecure.
  4. Disabling the widget is global – any use in any sidebar disables the widget globally.

The code

Now that we have the Disclaimer done let’s look at the code:

(function() {
    wp.customize.bind( 'ready', function() {

        var api = wp.customize,
            widgetId = 'foo_widget',
            widget = wp.customize.Widgets.availableWidgets.findWhere( { id_base: widgetId } );

        /**
         * Counts how often a widget is used based on an array of Widget IDs.
         *
         * @param widgetIds
         * @returns {number}
         */
        var countWidgetUses = function( widgetIds ){

            var widgetUsedCount = 0;

            widgetIds.forEach(function(id){

                if( id.indexOf( widgetId ) == 0 ){
                    widgetUsedCount++;
                }

            });

            return widgetUsedCount;

        };

        var isSidebar = function( setting ) {
            return (
                0 === setting.id.indexOf( 'sidebars_widgets[' )
                &&
                setting.id !== 'sidebars_widgets[wp_inactive_widgets]'
            );
        };

        var updateState = function(){

            //Enable by default...
            widget.set('is_disabled', false );

            api.each( function( setting ) {
                if ( isSidebar( setting ) ) {
                    //...and disable as soon as we encounter any usage of the widget.
                    if( countWidgetUses( setting.get() ) > 0 ) widget.set('is_disabled', true );
                }
            } );

        };

        /**
         * Listen to changes to any sidebar.
         */
        api.each( function( setting ) {
            if ( isSidebar( setting ) ) {
                setting.bind( updateState );
            }
        } );

        updateState();

    });
})( jQuery );

Sidenote: Use the customize_controls_enqueue_scripts action to add the script.


You could probably extend this to limit it to work an a per sidebar base instead of globally. I’d say listen to the activation of a sidebar and then count the widgets in that sidebar. But I didn’t find time to look into that as well.

Leave a Comment