I’m extending the group block. The problem I experience is that if I select an option nothing changes on the backend. But if I change it a second time it shows the class of the previous selected option. I think the const combo leads to that issue. I don’t really know how to combine multiple attributes properly. Could you please help me?
/**
* Create HOC to add group control to inspector controls of block.
*/
const withGroupControl = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
// Do nothing if it's another block than our defined ones.
if ( ! enableGroupControlOnBlocks.includes( props.name ) ) {
return (
<BlockEdit { ...props } />
);
}
const { width } = props.attributes;
const { color } = props.attributes;
const { highlight } = props.attributes;
const combo = width + ' ' + color + ' ' + highlight;
if ( combo ) {
props.attributes.className = `${ combo }`;
}
return (
<Fragment>
<BlockEdit { ...props } />
<InspectorControls>
<PanelBody
title={ 'Functions' }
initialOpen={ true }
>
<SelectControl
label={ 'Width settings' }
value={ width }
options={ groupWidthControlOptions }
onChange={ ( selectedWidthOption ) => {
props.setAttributes( {
width: selectedWidthOption,
} );
} }
/>
<SelectControl
label={ 'Color settings' }
value={ color }
options={ groupColorControlOptions }
onChange={ ( selectedColorOption ) => {
props.setAttributes( {
color: selectedColorOption,
} );
} }
/>
<SelectControl
label={ 'Highlight settings' }
value={ highlight }
options={ groupHighlightControlOptions }
onChange={ ( selectedHighlightOption ) => {
props.setAttributes( {
highlight: selectedHighlightOption,
} );
} }
/>
</PanelBody>
</InspectorControls>
</Fragment>
);
};
}, 'withGroupControl' );
addFilter( 'editor.BlockEdit', 'ww-group/with-group-control', withGroupControl );
1 Answer
In order to modify an attribute, you should use the setAttributes()
function as you do elsewhere in your code. This function updates the value in Gutenberg’s data stores, which triggers a re-render for any components depending on it and stages the change to be updated in the database when the post is saved.
const { width } = props.attributes;
const { color } = props.attributes;
const { highlight } = props.attributes;
const combo = width + ' ' + color + ' ' + highlight;
if ( combo ) {
props.attributes.className = `${ combo }`;
}
Additionally, in this code the if( combo )
condition will always execute, as the combo
assignment will always evaluate to a non-empty string which is “truthy” in JavaScript. In the case that width
, color
, and highlight
are undefined, combo
becomes the string "undefined undefined undefined"
, which seems undesirable for an HTML class name.
I think it would be a good idea to prefix each attribute-derived class name as well so you can distinguish them from one another, e.g. att-highlight-red
instead of red
. A function to convert an object of key/value pairs into class names and ignore undefined values would be useful to that end:
function attsToClassNames( atts = {} ) {
const classes = [];
for( const [ key, value ] in Object.entries( atts ) ) {
if( ! value && value !== 0 ) // Ignore falsey values, except 0
continue;
classes.push( `att-${key}-${value}` );
}
return classes;
}
All of the above in mind, I’d replace the section of code referenced at the beginning of this answer with something like this:
const {
attributes,
setAttributes,
} = props;
const {
width,
color,
highlight,
className,
} = attributes;
// Retrieve the current attribute classes.
const attClasses = attsToClassNames( { width, color, highlight } ).join( ' ' );
// If the `className` attribute does not reflect the current attribute classes,
// update it.
if( className !== attClasses )
setAttributes( { className: attClasses } );
Now, className
should always reflect the most recent list of class names derived from the other attributes, and will be staged in Gutenberg’s data stores to be saved along with the rest of the post & block attributes.
In order to make this (very slightly) more efficient, we can leverage React’s useEffect()
hook (imported from @wordpress/element
) and specify the relevant attributes in the “dependency array” – this will make it so that rebuilding the attribute classes only occurs on the very first render, then only if one or more of those attributes change after that:
useEffect(
() => {
const attClasses = attsToClassNames( { width, color, highlight } ).join( ' ' );
if( className !== attClasses )
setAttributes( { className: attClasses } );
},
[ width, color, highlight ]
);
This may still pose some other potential issues depending on your specific use-case – namely if you’d like to incorporate any other external className
like that which is passed in on props
.