2

I am trying to develop a custom WP block, have read approximately the entire internet, have tried many things and still can't solve the problem(s) I have. I am a WP noob and the more I read, the more I am confused.

What I want to achieve: building an offcanvas drawer block with its handle as a button text or image. In the editor it should be decided where the block should be placed (left, top, right, bottom). So basically I have to add a class to the block element itself on the fly. The user builds the block themselves by adding nested blocks

The problems I have are mostly related to the block validation mechanism. So the questions would be:

  • How to add a CSS class to the block itself when the user selects the placement?

  • Why does the block becomes invalid as soon as I uncomment these lines in the Edit() and Save() functions?:

    {/* <Button className="offcanvas-drawer-handle"> */}
    {/*   <img className="offcanvas-drawer-handle-image" src={ attributes.handleImage.url }/> */}
    {/* </Button> */}
    
  • An last question, more design oriented: it sounds odd to put the handle in the same div and position it no? What do you think?

Here the support and attributes section of the block.json file:

"supports": {
    "html": false,
    "typography": {
        "fontSize": true,
        "lineHeight": true,
        "textAlign": true
    },
    "color": {
        "text": true,
        "link": true,
        "background": true
    },
    "spacing": {
        "padding": true
    }
},
"attributes": {
    "style": {
        "type": "object",
        "margin": "value",
        "padding": "value"
    },
    "handleType": {
        "type": "string",
        "default": "button"
    },
    "handleImage": {
        "type": "object",
        "default": null
    },
    "handleText": {
        "type": "string",
        "default": "Type drawer's button text"
    },
    "placement": {
        "type": "string",
        "default": "right"
    }
}

Here the edit.js

// imports omitted
const Edit = ({ attributes, setAttributes }) => {

    const blockProps = useBlockProps();
    const ALLOWED_MEDIA_TYPES = [ 'image/jpg', 'image/png', 'image/svg' ];
    const isHandleImageLoaded = attributes.handleImage !== null;

    const onHandleTypeChange = (value) => {
        setAttributes({ handleType: value });
    }

    const onHandleTextChange = (value) => {
        setAttributes({ handleText: value });
    }

    const onSelectHandleImage = (value) => {
        setAttributes({ handleImage: value })
    }

    const onPlacementChange = (value) => {
        setAttributes({ placement: value })
    }

/* this was an attempt to append the placement class -> the block becomes
   invalid (which I understand this time but don't know how I can solve it
   and it's horrible to do that without validation. I was trying out)
*/
    // blockProps.className = `${blockProps.className} offcanvas-drawer-${attributes.placement}`;

    return (
        <>
            <InspectorControls>
                <PanelBody title={__("General Settings")}>
                    <ToggleGroupControl
                        label="Placement"
                        value="right"
                        isBlock
                        __nextHasNoMarginBottom
                        __next40pxDefaultSize
                        onChange={ onPlacementChange }>
                        <ToggleGroupControlOption value="left" label="Left" />
                        <ToggleGroupControlOption value="top" label="Top" />
                        <ToggleGroupControlOption value="right" label="Right" />
                        <ToggleGroupControlOption value="bottom" label="Bottom" />
                    </ToggleGroupControl>

                    <ToggleGroupControl
                        label="Handle Type"
                        value={ attributes.handleType }
                        isBlock
                        __nextHasNoMarginBottom
                        __next40pxDefaultSize
                        onChange={ onHandleTypeChange }>
                        <ToggleGroupControlOption value="button" label="Button" />
                        <ToggleGroupControlOption value="image" label="Image" />
                    </ToggleGroupControl>

                    { isHandleImageLoaded && attributes.handleType === 'image'  ? (
                            <figure className="canvas-thumbnail">
                                <img src={ attributes.handleImage.url } />
                                <figcaption>{attributes.handleImage.title}</figcaption>
                            </figure>
                        )
                        : null }

                    { attributes.handleType == 'button' ? (
                        <TextControl
                            __nextHasNoMarginBottom
                            __next40pxDefaultSize
                            label="Handle Text"
                            value={ attributes.handleText }
                            onChange={ onHandleTextChange }
                        />
                        )
                        : attributes.handleType == 'image' ? (
                        <MediaUpload
                            onSelect={ onSelectHandleImage }
                            allowedTypes={ ALLOWED_MEDIA_TYPES }
                            multiple={ false }
                            render={ ( { open } ) => (
                                <>
                                    <Button onClick={open} variant="primary">
                                        { attributes.handleImage === null
                                            ? 'Upload new file'
                                            : 'Upload' }
                                    </Button>
                                </>
                            )}
                        />)
                        : null}

                </PanelBody>
            </InspectorControls>

            <div { ...blockProps }>
                <InnerBlocks/>

// here as soon as I add something (a simple div as well), the block become invalid
                {/* <Button className="offcanvas-drawer-handle"> */}
                {/*     <img className="offcanvas-drawer-handle-image" src={ attributes.handleImage.url }/> */}
                {/* </Button> */}
            </div>
        </>
    );
}

Here the save.js

// import omitted
const Save = (props) => {
    const { attributes, innerBlocks } = props;
    const blockProps = useBlockProps.save();

// here same as in Edit()
    // blockProps.className = `${blockProps.className} offcanvas-drawer-${attributes.placement}`;

    return (
        <div { ...blockProps }>
            <InnerBlocks.Content />
            {/* <Button className="offcanvas-drawer-handle"> */}
            {/*     <img className="offcanvas-drawer-handle-image" src={ attributes.handleImage.url }/> */}
            {/* </Button> */}
        </div>
    )
}

So frustrating. In HTML + CSS, this would be done in 20 minutes :D And here it is not possible to build the checkbox-trick to trigger the css open/close animation, right? (I haven't tried yet, just asking)

I am very happy to receive all the comments, critics and insights and I thank you in advance for your help!

2
  • Gutenberg blocks are declarative. This means WordPress compares the output generated in the editor (from the edit() function) with the output of the save() function - the static HTML that gets stored in the database. You can view further details at this post. Commented Oct 26 at 15:52
  • Oh thank you very much @ShahbazAhmed! Actually I wanted to write again because I found a solution to all the above mentioned problems a few hours ago and I am still working at it. Basically "when you think you read the docs, please read it again..." :D And apparently I don't even know how to do a google search :D Thank you very much!! Crazy, that's exactly what I am trying to do... Commented Oct 26 at 16:16

1 Answer 1

4

Why the Block Became Invalid When You Added <Button>...</Button>

This happens because the HTML structure between your Edit and Save outputs must be identical.
In your code:

  • You commented out the <Button> inside edit.js

  • You also commented it out in save.js

But when you uncomment, both versions must output the exact same structure.

However, you’re likely using Gutenberg’s internal <Button> component (from @wordpress/components), which renders differently in the editor and frontend (because it’s a React component, not pure HTML).

That’s why your block goes invalid — the editor outputs <div class="components-button is-primary">...</div>
but the frontend (static save) outputs just <button>...</button> — mismatch!

Solution:

You must use plain HTML elements in save.js, not Gutenberg UI components.

So:

// In edit.js
<Button
  variant="primary"
  className="offcanvas-drawer-handle"
>
  {attributes.handleType === 'image' && attributes.handleImage ? (
    <img
      src={attributes.handleImage.url}
      alt=""
      className="offcanvas-drawer-handle-image"
    />
  ) : (
    attributes.handleText
  )}
</Button>

and in save.js, replicate the HTML that would be produced:

// In save.js
<div {...blockProps}>
  <InnerBlocks.Content />
  <button className="offcanvas-drawer-handle">
    {attributes.handleType === 'image' && attributes.handleImage ? (
      <img
        src={attributes.handleImage.url}
        alt=""
        className="offcanvas-drawer-handle-image"
      />
    ) : (
      attributes.handleText
    )}
  </button>
</div>

Rule of thumb:
Only use <Button> from @wordpress/components in the editor (edit.js) for UX convenience.
But never in save.js — there you must output plain static HTML.

Adding Dynamic Classes Without Breaking Validation

You tried this (and it broke validation):

blockProps.className = `${blockProps.className} offcanvas-drawer-${attributes.placement}`;

Correct way to do this:

You should use the built-in helper to add classes dynamically:

const blockProps = useBlockProps({
  className: `offcanvas-drawer offcanvas-drawer-${attributes.placement}`,
});

…and the same for your save.js:

const blockProps = useBlockProps.save({
  className: `offcanvas-drawer offcanvas-drawer-${attributes.placement}`,
});

Why this works:
useBlockProps() automatically handles merging your custom class with the default Gutenberg class for the block, preserving internal validation.

Sign up to request clarification or add additional context in comments.

3 Comments

Concerning the className, I tried exactly this solution without pushing the reflection to the fact that I could append the class with a space... It's so nice to feel dumb sometimes :D
Thank you for this comprehensive explanation! Especially the <button> part which was still not clear to me. Now I understand the error I was doing. I didn't even noticed it in the post you sent me... Thank you for pointing it out! Now it's clear.
I'm happy to hear this solution sorts out the issue

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.