Search code examples
javascriptwordpress-gutenberg

What could be causing the 'setAttribute' function of a custom Gutenberg block not to work with the save/publish button?


Consider the following blocks.

block.js

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 2,
    "name": "create-block/demo-block",
    "version": "0.1.0",
    "title": "Demo Block",
    "category": "widgets",
    "icon": "smiley",
    "description": "Example block scaffolded with Create Block tool.",
    "attributes": {
        "arr": {
            "type": "array",
            "default": []
        }
    },
    "supports": {
        "html": false
    },
    "textdomain": "demo-block",
    "editorScript": "file:./index.js"
}

edit.js

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

import {
    Panel,
    PanelBody
} from '@wordpress/components';

import './editor.scss';

import { DemoObject } from './demo';

export default function Edit({ attributes, setAttributes }) {
    return (
        <>
            <InspectorControls>
                <Panel>
                    <PanelBody title='Demo Setting'>
                        <DemoObject
                            arr = { attributes.arr }
                            update = { (newArr) => setAttributes({ arr: newArr }) }
                        />
                    </PanelBody>
                </Panel>
            </InspectorControls>
            <p { ...useBlockProps() }>
                { 'Demo Block – hello from the editor!' }
            </p>
        </>
    );
}

demo.js

import { Component } from '@wordpress/element';
import { Button } from '@wordpress/components';

export class DemoObject extends Component {
    constructor(props) {
        super(props);

        this.state = {
            arr: props.arr,
            updater: props.update
        }

        this.increase = this.increase.bind(this);
        this.decrease = this.decrease.bind(this);
    }

    increase() {
        let newArr = this.state.arr;
        newArr.push("");
        this.setState({ arr: newArr });
        this.state.updater(newArr);
    }

    decrease() {
        if (this.state.arr.length > 0) {
            let newArr = this.state.arr;
            newArr.pop();
            this.setState({ arr: newArr });
            this.state.updater(newArr);
        }
    }

    render() {
        return(
            <div>
                <Button onClick={ this.decrease }>decrease</Button>
                <p>{ this.state.arr.length }</p>
                <Button onClick={ this.increase }>increase</Button>
            </div>
        )
    }
}

This block has two buttons in the editor's settings sidebar. One should increase the array saved in the backend, while the other should decrease it.

It has a strange behavior, though. Upon saving (while editing the site) or publishing (in a post) and then reloading the page, you'll notice that the array's length stays the same. If you click the increase button in the site editor, the save button remains disabled. And when using this block in a post, the publish button (although active) does not save the array.

I have seen similar threads, but none has a solution.


Solution

  • Aduth posted the solution to this problem in an issue about the setAttribute not causing a re-render. Although the symptoms are different, the cause is the same.

    As Aduth said, all "boils down to nextAttributeValue !== previousAttributeValue." He also mentioned, "The problem is that you're mutating the attribute, not creating a new value, so the editor does not detect that a change has occurred."

    To solve this issue, all it takes is to use the concat and slice methods instead. These JavaScript methods create new arrays instead of mutating the old ones. Replace the increase and decrease functions as shown.

    increase() {
        let newArr = this.state.arr;
        newArr = newArr.concat([""]);
        this.setState({ arr: newArr });
        this.state.updater(newArr);
    }
    
    decrease() {
        if (this.state.arr.length > 0) {
            let newArr = this.state.arr;
            newArr = newArr.slice(0, -1);
            this.setState({ arr: newArr });
            this.state.updater(newArr);
        }
    }