#StackBounty: #plugin-development #javascript #block-editor How to contol an elments classes from multiple Gutenberg sidebar controls?

Bounty: 50

I am trying to extend the core/table block and copy-pasted, trail and errored me into something that is close to what I am trying to do. But I it’s not quite working as I like.

I like to extend the Block to have toggle elements that control the classes on the <table> the block already does this for two base classes but I want it to work with Bootstrap CSS tables.

The original Block code can be found here.

What I like to do is something Gutenberg also does great and for the table block is does so for the style. When you set it to ‘Striped’ it will add a is-style-striped to the block. It will live update the field and as soon as you manually remove the class it will automatically switch the style to ‘Default’. I like to link a few class toggles to a text field like that. I searched a little in GB code but could not find where or how it’s done. The buildTableClasses function that uses classnames/dedupe could be a way but there are a few issues here.

                        <ToggleControl
                            label={ __( 'Hover' ) }
                            checked={ !! tableHover }
                            onChange={ () => props.setAttributes( {
                                tableHover: ! tableHover,
                                tableClass: buildTableClasses( props.attributes ),
                            } ) }
                        />
  1. I do not really understand the negation and double negation used here. This code is based off some tutorial I found. Currently, is behaves weird. Like inverted, classes get added when the toggles are off, but also when toggling another toggle that last class sticks, some weird. I experimented with it but it broke completely, so I posted it this way.
  2. The background class for the table that the block already has originally does not end up in the ‘table classes’ text control field.
  3. When I remove a class manually from the field the changes are not bound to the toggles. So I like to find a way to make this work. I could write a function for it but I like to know if there is maybe a better way to do all this. Probably better to reuse some GB core code or model the code after it. If you can point be into the right direction that would be great.
  4. I like to know how I can extend an already existing section, like adding new controls to it. Or removing a section like the style section that makes. Because Bootstrap has so many classes that control style that can be combined the combination of styles would be huge so class toggles is probably a better way to do it.
  5. During playing around with this I noticed that it sometimes broke the block and GB seems to do some checks of the output and was actually unable to restore the block. So I wonder if I should rather fork the block but it would be annoying to maintain. But if an extended block breaks users blocks when the filters come in or get removed later breaks this solution it not great.

The complete code I have currently. Ready to try plugin on Github. The save function is copy-pasted from GB and modified for the table classes.

import classnames from 'classnames/dedupe';

const wp = window.wp;
const { __ } = wp.i18n;
const { addFilter } = wp.hooks;
const { assign } = window.lodash;
const {
    createHigherOrderComponent,
} = wp.compose;

const {
    Fragment,
} = wp.element;

const {
    RichText,
    InspectorControls,
    getColorClassName,
} = wp.editor;

const {
    PanelBody,
    TextControl,
    ToggleControl,
} = wp.components;

const filterBlocks = ( settings ) => {
    console.log( settings );

    if ( settings.name !== 'core/table' ) {
        return settings;
    }

    const newSettings = {
        ...settings,
        attributes: {
            ...settings.attributes, // spread in old attributes so we don't lose them!
            tableClass: { // here is our new attribute
                type: 'string',
                default: 'table ',
            },
            tableBordered: { // here is our new attribute
                type: 'boolean',
                default: false,
            },
            tableStriped: { // here is our new attribute
                type: 'boolean',
                default: false,
            },
            tableHover: { // here is our new attribute
                type: 'boolean',
                default: true,
            },
        },
        save( { attributes } ) {
            const {
                hasFixedLayout,
                head,
                body,
                foot,
                backgroundColor,
                caption,
                tableBordered,
                tableStriped,
                tableHover,
                tableClass,
            } = attributes;
            const isEmpty = ! head.length && ! body.length && ! foot.length;

            if ( isEmpty ) {
                return null;
            }

            const classes = buildTableClasses( attributes );

            const hasCaption = ! RichText.isEmpty( caption );

            const Section = ( { type, rows } ) => {
                if ( ! rows.length ) {
                    return null;
                }

                const Tag = `t${ type }`;

                return (
                    <Tag>
                        { rows.map( ( { cells }, rowIndex ) => (
                            <tr key={ rowIndex }>
                                { cells.map(
                                    ( { content, tag, scope, align }, cellIndex ) => {
                                        const cellClasses = classnames( {
                                            [ `has-text-align-${ align }` ]: align,
                                        } );

                                        return (
                                            <RichText.Content
                                                className={
                                                    cellClasses ?
                                                        cellClasses :
                                                        undefined
                                                }
                                                data-align={ align }
                                                tagName={ tag }
                                                value={ content }
                                                key={ cellIndex }
                                                scope={
                                                    tag === 'th' ? scope : undefined
                                                }
                                            />
                                        );
                                    }
                                ) }
                            </tr>
                        ) ) }
                    </Tag>
                );
            };

            return (
                <figure>
                    <table className={ classes === '' ? undefined : classes }>
                        <Section type="head" rows={ head } />
                        <Section type="body" rows={ body } />
                        <Section type="foot" rows={ foot } />
                    </table>
                    { hasCaption && (
                        <RichText.Content tagName="figcaption" value={ caption } />
                    ) }
                </figure>
            );
        },
    };

    return newSettings;
};

addFilter(
    'blocks.registerBlockType',
    'example/filter-blocks',
    filterBlocks
);

function buildTableClasses( attributes ) {
    const {
        hasFixedLayout,
        backgroundClass,
        tableStriped,
        tableBordered,
        tableHover,
        tableClass,
    } = attributes;

    const classes = classnames(
        tableClass.split( ' ' ),
        backgroundClass,
        {
            table: true,
            'has-fixed-layout': hasFixedLayout,
            'has-background': !! backgroundClass,
            'table-bordered': tableBordered,
            'table-striped': tableStriped,
            'table-hover': tableHover,
        }
    );

    return classes;
}

const tableClassControl = createHigherOrderComponent( ( BlockEdit ) => {
    return ( props ) => {
        if ( 'core/table' !== props.name ) {
            return (
                <BlockEdit { ...props } />
            );
        }

        const {
            tableStriped,
            tableBordered,
            tableHover,
            tableClass,
        } = props.attributes;

        return (
            <Fragment>
                <BlockEdit { ...props } />
                <InspectorControls>
                    <PanelBody
                        title={ __( 'Table classes' ) }
                        initialOpen={ true }
                    >
                        <ToggleControl
                            label={ __( 'Striped' ) }
                            checked={ !! tableStriped }
                            onChange={ () => props.setAttributes( {
                                tableStriped: ! tableStriped,
                                tableClass: buildTableClasses( props.attributes ),
                            } ) }
                        />
                        <ToggleControl
                            label={ __( 'Bordered' ) }
                            checked={ !! tableBordered }
                            onChange={ () => props.setAttributes( {
                                tableBordered: ! tableBordered,
                                tableClass: buildTableClasses( props.attributes ),
                            } ) }
                        />
                        <ToggleControl
                            label={ __( 'Hover' ) }
                            checked={ !! tableHover }
                            onChange={ () => props.setAttributes( {
                                tableHover: ! tableHover,
                                tableClass: buildTableClasses( props.attributes ),
                            } ) }
                        />
                        <TextControl
                            label={ __( '<table> classes' ) }
                            type="text"
                            value={ tableClass }
                            onChange={ ( value ) =>
                                props.setAttributes( { tableClass: value } )
                            }
                        />
                    </PanelBody>
                </InspectorControls>
            </Fragment>
        );
    };
}, 'tableClassControl' );

addFilter( 'editor.BlockEdit', 'extend-block-example/with-spacing-control', tableClassControl );


Get this bounty!!!

Leave a Reply

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