Search code examples
react-hookswordpress-gutenbergwordpress-plugin-creation

Wordpress Custom Widget Save Multiple Attributes?


I am using the @wordpress/create-block package to build a simple widget. I do not understand how to make the save.js save more than one attribute.

I have 2 attributes defined in the block.json: theNotes and theContact.

"attributes": {
    "theNotes": {
        "type": "string",
        "source": "text",
        "selector": "div",
        "default": ""
    },
    "theContact": {
        "type": "string",
        "source": "text",
        "selector": "div",
        "default": ""
    }
}

My edit.js looks like this:

export default function Edit( { attributes, setAttributes } ) {
return (
    <div { ...useBlockProps() }>
            <div>
                <TextControl
                    label={ __( 'The Notes', 'editor-notes' ) }
                    value={ attributes.theNotes }
                    onChange={ ( val ) => setAttributes( { theNotes: val } ) }
                />
            </div>
            <div>
                <TextControl
                    label={ __( 'The Contact', 'editor-notes' ) }
                    value={ attributes.theContact }
                    onChange={ ( val ) => setAttributes( { theContact: val } ) }
                />
            </div>
    </div>
);}

For the save.js file, I cannot find instructions on how to save both of those attributes using this default scaffolding. I thought something like this would work, but I get a block validation error. It says that both the attributes were saved twice to both attribute values.

export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
return (
    <div { ...blockProps }>
        <div>{ attributes.theNotes }</div>
        <div>{ attributes.theContact }</div>
    </div>
);  }

The error says:

Content generated by `save` function:

<div class="wp-block-create-block-editor-notes"><div>notesTestcontactTest</div><div>notesTestcontactTest</div></div>

Content retrieved from post body:

<div class="wp-block-create-block-editor-notes"><div>notesTest</div><div>contactTest</div></div>

The Getting Started Guide shows how to save one attribute called "message" like this. Apparently, I do not know what to do when I have multiple attributes to update:

import { useBlockProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
return <div { ...blockProps }>{ attributes.message }</div>;}

Solution

  • The error indicates an issue with the save() function; however this is a little misleading. The content mismatch between save and post body is actually caused by how the attribute selector in block.json is defined as "div" for both theNotes and theContact.

    Given the attributes selector defined is not a unique element (<div>...</div> appears 3 times in the saved content), the method used to extract data from the post content gets multiple matches so both attributes save the text content of <div> and <div> again. This results in the "notesTestcontactTest" text seen in the error message.

    block.json (current)

    "attributes": {
        "theNotes": {
            "type": "string",
            "source": "text",  // the value saved for theNotes is the text
            "selector": "div", // within this matching selector
            "default": ""
        },
        "theContact": {
            "type": "string",
            "source": "text",  // the value save for theContact is the text
            "selector": "div", // with the same matching selector as theNotes
            "default": ""
        }
    }
    

    By adding a unique class name to div selector of both attributes, the values will save and update correctly, eg:

    block.json (updated)

    "attributes": {
        "theNotes": {
            "type": "string",
            "source": "text", 
            "selector": "div.the-notes", // a div with the className "the-notes"
            "default": ""
        },
        "theContact": {
            "type": "string",
            "source": "text", 
            "selector": "div.the-contact", // a div with the className "the-contact"
            "default": ""
        }
    }
    

    The RichText component would be more suitable than using TextControl for editing content to be saved as text of block-level elements like <div>, eg:

    edit.js

    import { useBlockProps, RichText } from '@wordpress/block-editor';
    
    export default function Edit( { attributes, setAttributes } ) {
        return (
            <div { ...useBlockProps() }>
                <RichText
                    tagName="div" // Output as a <div>, no extra div needed
                    value={ attributes.theNotes } 
                    onChange={ ( content ) => setAttributes( { theNotes: content } ) }
                    placeholder="Enter Notes.." // Optional
                />
                
                <RichText
                    tagName="div" // Output as a <div>, no extra div needed
                    value={ attributes.theContact } 
                    onChange={ ( content ) => setAttributes( { theContact: content } ) }
                    placeholder="Enter Contact.." // Optional
                />
            </div>
        )
    }
    

    The save() function would then also need updating to ensure the attributes selector finds the right match to extract the props.attributes values. RichText.Content can be used in save to render the tags markup and content:

    save.js

    import { useBlockProps, RichText } from '@wordpress/block-editor';
    
    export default function save( { attributes } ) {
        const blockProps = useBlockProps.save();
        return (
            <div { ...blockProps }>
                <RichText.Content 
                    tagName="div" 
                    className="the-notes"    // Used for the selector
                    value={ attributes.theNotes } 
                />
                <RichText.Content 
                    tagName="div" 
                    className="the-contact"  // Used for the selector
                    value={ attributes.theContact } 
                />
            </div>
        ) 
    }
    

    The RichText tagName could also be another block-level element like "h2" or "p" as long as the attributes selector matches. When retesting these changes to your block, remove the previous block from the Editor, save, refresh/clear the cache then insert your block again. To confirm the attributes save corrently, in the Editor use the Code view to see the markup that will be saved, it should be something like:

    <!-- wp:create-block/editor-notes -->
    <div class="wp-block-create-block-editor-notes"><div class="the-notes">Notes Test</div><div class="the-contact">Contact Test</div></div>
    <!-- /wp:create-block/editor-notes -->