How to control an elements classes from multiple Gutenberg sidebar controls?

The double NOT operator (!!)

It’s simply a way to convert/type-cast a non-boolean value to a boolean value, and !! <expression> gives us the opposite of ! <expression>.

let foo = 'bar'; // non-empty string
console.log( ! foo, !! foo ); // false, true

foo = ''; // now it's an empty string
console.log( ! foo, !! foo ); // true, false

Issues in your code

  1. Despite that wp.editor works, it’s been deprecated, so you should use wp.blockEditor instead:

    const {
        RichText,
        InspectorControls,
        getColorClassName
    } = wp.blockEditor; // not wp.editor
    
  2. In buildTableClasses(), backgroundClass is not an attribute in the block. So that (the first one) should be backgroundColor and define backgroundClass like so:

    const backgroundClass = getColorClassName(
        'background-color',
        backgroundColor
    );
    
  3. The buildTableClasses() should only be used with the save function, because from the edit function you’d get the “inverted” issue because the function would receive the old attributes.

  4. And I would use a dedicated function to update the “<table> classes” field when a toggle control is updated and only add/remove the associated class with that toggle, e.g. table-hover for the “Hover” toggle:

    function onChangeTableHover() {
        props.setAttributes( {
            // Toggle the state.
            tableHover: ! tableHover,
    
            // Then add/remove only the table-hover class.
            tableClass: classnames( tableClass, {
                'table-hover': ! tableHover,
            } ),
        } );
    }
    
    /* Then in the JSX:
    <ToggleControl
        label={ __( 'Hover' ) }
        checked={ !! tableHover }
        onChange={ onChangeTableHover }
    />
    */
    

    You might be thinking about the background color classes, but they should be controlled (added/removed) by the color picker toggles (in the “Color settings” panel/section).

  5. When the “<table> classes” field is updated, e.g. you typed table-hover in there or you removed it, you should change the toggle states:

    function onChangeTableClass( value ) {
        // User likely typed a whitespace.
        if ( tableClass === value.trim() ) {
            props.setAttributes( { tableClass: value } );
            return;
        }
    
        const list = value.split( / +/ );
    
        props.setAttributes( {
            // Update the value.
            tableClass: classnames( value ),
    
            // Then the toggles.
            tableHover: list.indexOf( 'table-hover' ) >= 0,
            tableBordered: list.indexOf( 'table-bordered' ) >= 0,
            tableStriped: list.indexOf( 'table-striped' ) >= 0,
        } );
    }
    
    /* Then in the JSX:
    <TextControl
        label={ __( '<table> classes' ) }
        type="text"
        value={ tableClass }
        onChange={ onChangeTableClass }
    />
    */
    
  6. The default value for the tableClass attribute should be table table-hover because the default tableHover state is true.

  7. (Just a suggestion) I think you should use attribute as the source for the tableClass attribute.

Block Validation

Yes, Gutenberg does validate the block’s output:

During editor initialization, the saved markup for each block is regenerated using the attributes that were parsed from the post’s content. If the newly-generated markup does not match what was already stored in post content, the block is marked as invalid. This is because we assume that unless the user makes edits, the markup should remain identical to the saved content.

So if the saved markup is <p class="foo">bar baz</p>, but the newly-generated markup is <p class="foo new-class">bar baz</p>, then you’d get an error.

Therefore, because you’re changing the output, then you’d better off copy the block, customize it and register it as a new block…

  • Creating a new block might not be as easy as simply extending the existing block, but a new block is better than having to deal with block validation errors later on, e.g. after your plugin/theme is deactivated.

If you’d rather simply extend the core table block

Then you would want to copy the original edit component, edit the code, and use it as the edit function for the block, similar to the way you did it with the save function.

Why so is because you would want to update the “<table> classes” field whenever a background color is selected/deselected, which you’d need to modify the PanelColorSettings element.

Secondly, copying also allows you to remove/reorder existing sections or add new controls to the sections.

Try my script

You can find it on GitHub (source | build), and I built it using the wp-scripts package.


Update: About the tableHover: ! tableHover or <attribute>: ! <attribute>

It’s equivalent to what the fourth line below does in PHP:

<?php
$attrs = [ 'tableHover' => false ];
$tableHover = $attrs['tableHover'];            // get the current value
$attrs['tableHover'] = ! $tableHover;          // toggle/change the value
var_dump( $tableHover, $attrs['tableHover'] ); // bool(false) bool(true)

So you asked (in the comment):

I am still confused about tableHover: ! tableHover on onChange. … why is the attribute negated when setting the attribute?

And it’s because the attribute is of the boolean type, so the value should only be either true or false.

So if the current value in props.attributes is true, then the tableHover: ! tableHover toggles/changes the value to false.

// The currently saved block attributes.
const { tableHover } = props.attributes;

// When updating the tableHover, we could simply do:
props.setAttributes( { tableHover: ! tableHover } );
// .. which is equivalent to:
props.setAttributes( { tableHover: tableHover ? false : true } );

Or in an onChange callback, you can use the current state of the toggle control element that’s passed as the first parameter to the callback.

<ToggleControl
    checked={ !! tableHover }
    onChange={ ( checked ) => props.setAttributes( { tableHover: checked } ) }
/>

And that’s more understandable, I guess? 🙂

But the tableHover: ! tableHover is a simpler version without having to use the first onChange parameter.

So just use any methods you prefer, but make sure to set the correct value, e.g. if the toggle control element is checked, set the tableHover to true.

And btw, you can also use !! in PHP in place of (bool):

<?php
$foo = 'bar';
var_dump( (bool) $foo, !! $foo ); // bool(true) bool(true)