1

I have a custom block that has block variations. If I make the scope of the variations "inserter", they work as expected. But if I make the scope "block" and use BlockVariationPicker so the user chooses a variation upon adding a block (like they do when adding the core columns block), nothing happens when I click on a variation icon.

I assume it's because I need onSelect added, but I'm not sure what to do.

Register block:

import { __ } from "@wordpress/i18n";

import { registerBlockType } from "@wordpress/blocks";

import "./style.scss";

import Edit from "./edit";
import save from "./save";

registerBlockType("create-block/variation", {
    edit: Edit,
    save,
});

My variations:

const variations = [
{
    name: "one-column",
    title: __("One Column"),
    description: __("One column"),
    icon: "palmtree",
    innerBlocks: [["core/paragraph"]],
    scope: ["inserter"],
},
{
    name: "two-columns",
    title: __("Two Column"),
    description: __("Two columns"),
    icon: "palmtree",
    // isDefault: true,
    innerBlocks: [["core/paragraph"], ["core/paragraph"]],
    scope: ["inserter"],
},
];

export default variations;

My edit.js

import { __ } from "@wordpress/i18n";

import {
    useBlockProps,
    __experimentalBlockVariationPicker as BlockVariationPicker,
} from "@wordpress/block-editor";

import variations from "./variations";

import "./editor.scss";

export default function Edit() {
    return (
        <div {...useBlockProps()}>
            <BlockVariationPicker
                variations={variations}
                label={__("Menu Columns")}
                instructions={__("Choose how many menu columns")}
            />
        </div>
    );
}

What do I need to add to make it so when the user clicks on the variation it's entered in the editor? This is what it looks like when I add my block, but nothing happens when I click. enter image description here

2 Answers 2

2

I assume it's because I need onSelect added, but I'm not sure what to do.

What do I need to add to make it so when the user clicks on the variation it's entered in the editor?

One way, is add a variation attribute to your block type, then change the attribute value in the onSelect callback of the variation picker. That in turn will instruct React to re-render the block (because the block state has changed), hence on that render, we no longer display the variation picker. So basically, the picker is "closed". :)

See example below where the block type renders a simple button with two variations — with an icon, and without it.

// File: index.js

import { registerBlockType } from '@wordpress/blocks';
import {
    useBlockProps,
    __experimentalBlockVariationPicker as BlockVariationPicker,
} from '@wordpress/block-editor';
import { Button } from '@wordpress/components';

const variations = [{
    name: 'button-with-icon',
    title: 'Button + Text + Icon',
    icon: 'button',
    scope: [ 'block' ],
    attributes: { icon: 'heart' },
}, {
    name: 'button-without-icon',
    title: 'Button + Text only',
    icon: 'button',
    scope: [ 'block' ],
}];

registerBlockType( 'my-blocks/foo-button', {
    apiVersion: 2,
    title: 'Foo Button',
    category: 'formatting',
    attributes: {
        icon: {
            type: 'string',
            default: '',
        },
        variation: {
            type: 'string',
            default: '',
        },
    },
    edit( { attributes, setAttributes } ) {
        if ( ! attributes.variation ) {
            return (
                <div { ...useBlockProps() }>
                    <BlockVariationPicker
                        variations={ variations }
                        label="Button Variant"
                        onSelect={ ( variation = variations[0] ) => {
                            setAttributes( {
                                ...variation.attributes,
                                variation: variation.name,
                            });
                        }}
                    />
                </div>
            );
        }

        return (
            <div { ...useBlockProps() }>
                <Button
                    text="Foo Button"
                    icon={ attributes.icon }
                />
            </div>
        );
    },
    save: ( { attributes } ) => (
        <div { ...useBlockProps.save() }>
            <Button
                text="Foo Button"
                icon={ attributes.icon }
            />
        </div>
    ),
} );

However, in the case of inner blocks, you would want to use the same approach as used by the core Columns block (see source on GitHub), where it checks if the current block contains any inner blocks and if yes, the inner blocks are displayed; otherwise, the variation picker is then displayed.

Here's a full working example you can try where I used the same variations in your variations.js file:

// File: index.js

import {
    registerBlockType,
    createBlocksFromInnerBlocksTemplate,
} from '@wordpress/blocks';
import {
    useBlockProps,
    __experimentalUseInnerBlocksProps as useInnerBlocksProps,
    store as blockEditorStore,
    __experimentalBlockVariationPicker as BlockVariationPicker,
    InnerBlocks,
} from '@wordpress/block-editor';
import { useDispatch, useSelect } from '@wordpress/data';

import variations from './variations';

// Note that you can do EditContainer( props ) or EditContainer( { attributes, etc } ).
function EditContainer() {
    const blockProps = useBlockProps();
    const innerBlocksProps = useInnerBlocksProps( blockProps, {
        //allowedBlocks: ALLOWED_BLOCKS,
        orientation: 'horizontal',
        renderAppender: false,
    } );

    return <div { ...innerBlocksProps } />;
}

function Placeholder( { clientId, setAttributes } ) {
    // Always set a default variation, particularly if allowing skipping the variation picker.
    const defaultVariation = variations[0];
    // Or do something like this, which selects the variation having "isDefault: true":
//  const defaultVariation = variations.filter( item => item.isDefault )[0] || variations[0];

    const { replaceInnerBlocks } = useDispatch( blockEditorStore );
    const blockProps = useBlockProps();

    return (
        <div { ...blockProps }>
            <BlockVariationPicker
                label="Section Variant"
                variations={ variations }
                onSelect={ ( variation = defaultVariation ) => {
                    if ( variation.attributes ) {
                        setAttributes( variation.attributes );
                    }
                    if ( variation.innerBlocks ) {
                        replaceInnerBlocks(
                            clientId,
                            createBlocksFromInnerBlocksTemplate(
                                variation.innerBlocks
                            ),
                            true
                        );
                    }
                } }
                allowSkip
            />
        </div>
    );
}

const Edit = ( props ) => {
    const { clientId } = props;
    const hasInnerBlocks = useSelect(
        ( select ) =>
            select( blockEditorStore ).getBlocks( clientId ).length > 0,
        [ clientId ]
    );
    const Component = hasInnerBlocks
        ? EditContainer // display the inner blocks
        : Placeholder;  // or the variation picker

    return <Component { ...props } />;
};

registerBlockType( 'my-blocks/foo-section', {
    apiVersion: 2,
    title: 'Foo Section',
    category: 'layout',
    edit: Edit,
    save: () => (
        <div { ...useBlockProps.save( { className: 'foo-section' } ) }>
            <InnerBlocks.Content />
        </div>
    ),
} );

So try that and just let me know if you have questions regarding the code. :)

6
  • I appreciate this so much! I poured over the columns code before asking but there's a lot in there. My issue now: the variation picker is working, but I don't know how to automatically close it once one is chosen. Not sure what in the columns code does that. Do you know? Thank you!
    – Elizabeth
    Commented Jun 17, 2021 at 14:45
  • To answer that, can I first see your registerBlockType() code? (Append it to your post)
    – Sally CJ
    Commented Jun 17, 2021 at 15:02
  • Added it, Sally. It's just the basic set-up because I'm using a basic block to test making variations.
    – Elizabeth
    Commented Jun 17, 2021 at 15:29
  • Sorry for the delay and for having misunderstood the question.. :) Anyway, see the (completely) revised answer and let me know!
    – Sally CJ
    Commented Jun 18, 2021 at 9:29
  • Thank you!! That works perfectly. I just have one things I can't work out. Is it possible to have additional elements in there? I have RichText and InspectorControls. From what I can tell, the return <div { ...innerBlocksProps } />; can't have anything added to it. If I add anything it puts it around the main container, not in it. Is it possible to do what I'm picturing? (It's a menu. The user adds a menu block and picks a variation (one column or two). But the menu title is the same RichText for either one, and same InspectorControls.)
    – Elizabeth
    Commented Jun 18, 2021 at 20:59
1

*This is a supplemental answer to the accepted answer, so to OP, take this as a supplemental vitamin to the main one as prescribed by the doctor... :p

So in reply to your comment:

Is it possible to have additional elements in there? I have RichText and InspectorControls. From what I can tell, the return <div { ...innerBlocksProps } />; can't have anything added to it.

Yes, it is possible:

  1. Firstly, define our imports:

    import {
        useBlockProps,
        __experimentalUseInnerBlocksProps as useInnerBlocksProps,
        store as blockEditorStore,
        __experimentalBlockVariationPicker as BlockVariationPicker,
        InnerBlocks,
        // add these lines:
        InspectorControls,
        RichText,
    } from '@wordpress/block-editor';
    // and also this:
    import { PanelBody, TextControl } from '@wordpress/components';
    
  2. Change the EditContainer() function so that it includes the InspectorControls and RichText elements — the inspector control (which in my code below, has just a plain simple text box) will be displayed in the sidebar, whereas the rich text will be placed above the inner blocks (as defined via the specific variation):

    function EditContainer() {
        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps( {}, {
            //allowedBlocks: ALLOWED_BLOCKS,
            orientation: 'horizontal',
            renderAppender: false,
        } );
    
        // *This line is just for demonstration, and so does the onChange callback below.
        // In actual implementation, you'd want to set the data via the block attributes.
        const [ foo, setFoo ] = wp.element.useState( 'bar baz' );
    
        return (
            <div { ...blockProps }>
                <InspectorControls>
                    <PanelBody>
                        <TextControl
                            label="Foo Input"
                            value={ foo }
                            onChange={ ( value ) => setFoo( value ) }
                        />
                    </PanelBody>
                </InspectorControls>
    
                <RichText
                    value={ foo }
                    onChange={ ( value ) => setFoo( value ) }
                />
    
                <div { ...innerBlocksProps } />
            </div>
        );
    }
    

    So the main thing you need to know is, I use a div to wrap the inspector controls, rich text and also inner blocks. Therefore, I used the blockProps with that div and not useInnerBlocksProps.

  3. In the save function, though, it will be up to you on how you want to output the rich text and the input value (i.e. the value of "Foo Input" above).

And if everything went well, you'd see something like so:

Sample preview in the editor - when editing the Foo Input

Sample preview in the editor - when editing an inner block

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.