0

I thought I had it. I added an attribute "max_level" and a SelectControl element "SelectMaxLevel". Everything works according to plan: I can set the max level and it is saved in the block. When I save the post and display the post see that the php part uses the "maximum level" variable. But when I edit the post the blocks loads it's default value. Why does this happen?

registerBlockType('simpletoc/toc', {
  title: __('SimpleTOC', 'simpletoc'),
  icon: simpletocicon,
  category: 'layout',
  attributes: {
        no_title: {
            type: 'boolean',
      default: false,
        },
        max_level: {
            type: 'integer',
      default: 6,
        },
    },
  edit: function(props) {

    return (
    <span>
    <InspectorControls>
      <Panel>
        <PanelBody>
          <PanelRow>
            <ToggleControl
                label={__("Disable heading", 'simpletoc')}
                help={__('Remove "Table of contents" block heading.', 'simpletoc')}
                checked={ props.attributes.no_title }
                onChange={ () => props.setAttributes( {  no_title: ! props.attributes.no_title } ) }
            />
          </PanelRow>
          <PanelRow>
            <SelectControl
                label={__("Maximum Level", 'simpletoc')}
                help={__('Maximum depth of the headings.', 'simpletoc')}
                value= { props.attributes.max_level }
                options={ [
                    { label: __('Including', 'simpletoc') + ' H6 (' + __('Show all', 'simpletoc') + ')' , value: '6' },
                    { label: __('Including', 'simpletoc') + ' H5', value: '5' },
                    { label: __('Including', 'simpletoc') + ' H4', value: '4' },
                    { label: __('Including', 'simpletoc') + ' H3', value: '3' },
                    { label: __('Including', 'simpletoc') + ' H2', value: '2' },
                ] }
                onChange={ ( level ) => props.setAttributes( { max_level: level } ) }
            />
          </PanelRow>
        </PanelBody>
      </Panel>
    </InspectorControls>
    <BlockControls>
      <ToolbarGroup>
        <ToolbarButton
          className="components-icon-button components-toolbar__control"
          label={__('Update table of contents', 'simpletoc')}
          onClick={function() {
            sendfakeAttribute(props)
          }}
          icon="update"
        />
      </ToolbarGroup>
  </BlockControls>
  <p>
    <ServerSideRender block={props.name} attributes={props.attributes} />
  </p>
  </span>
    )
  },
  save: props => {
    return null;
  },
});

The relevant php part:

register_block_type('simpletoc/toc', [
    'editor_script' => 'simpletoc-js',
    'editor_style' => 'simpletoc-editor',
        'attributes' => array(
                'no_title' => array(
                    'type' => 'boolean',
              'default' => false,
                ),
            'max_level' => array(
                    'type' => 'integer',
              'default' => 6,
                ),
            'updated' => array(
              'type' => 'number',
              'default' => 0,
              '_builtIn' => true,
            ),
    ),
    'render_callback' => __NAMESPACE__ . '\\render_callback'
   ]);
7
  • I would suggest using useState instead, but in this case there is no need for the component to have state at all, and it may even be the cause of your problem! Setting the attribute just sets the attribute locally. Blocks and attributes are saved when the post is saved, there is no need to use component state here
    – Tom J Nowell
    Commented Jan 11, 2021 at 22:28
  • Also, why is attributes defined twice?
    – Tom J Nowell
    Commented Jan 11, 2021 at 22:29
  • Man, I wanted to be cool and use you useState and now it is not needed? I tried this without the constant but it not working at all: <PanelRow><SelectControllabel={__("MaximumLevel",'simpletoc')}help={__('Maximumdepthoftheheadings.','simpletoc')}value={props.attributes.max_level}options={[{label:__('Including','simpletoc')+'H6('+__('Showall','simpletoc')+')',value:'6'},{label:__('Including','simpletoc')+'H2',value:'2'},]}onChange={(level)=>{setState(props.setAttributes({max_level:level}))}}/></PanelRow>
    – Marc
    Commented Jan 11, 2021 at 22:36
  • Attributes is defined in php and js. I thought I had to do this.
    – Marc
    Commented Jan 11, 2021 at 22:37
  • This does not work either: <PanelRow><SelectControllabel={__("MaximumLevel",'simpletoc')}help={__('Maximumdepthoftheheadings.','simpletoc')}value={props.attributes.max_level}options={[{label:__('Including','simpletoc')+'H6('+__('Showall','simpletoc')+')',value:'6'},{label:__('Including','simpletoc')+'H5',value:'5'},{label:__('Including','simpletoc')+'H4',value:'4'},{label:__('Including','simpletoc')+'H3',value:'3'},{label:__('Including','simpletoc')+'H2',value:'2'},]}onChange={(level)=>props.setAttributes({max_level:level})}/></PanelRow></PanelBody>
    – Marc
    Commented Jan 11, 2021 at 22:53

1 Answer 1

7

The reason why the block editor uses the default value — for the max_level ("Maximum level") attribute only — instead of the one parsed from the (block comment delimiter in the) saved post content, is because in the parsed/saved attributes, max_level is invalid.

Why is it invalid: Because the type of the attribute (value) doesn't match the one defined when the block is registered — max_level: { type: 'integer', ... }.

And why doesn't it match, is because this part which updates the attribute with a value that is a string:

// "level" here is an input value, and input values are string unless
// explicitly converted to another type (boolean, number, etc.):

onChange={ ( level ) => props.setAttributes( { max_level: level } ) }

Which then, for instance when the "Including H3" is selected as the "Maximum level", results in "max_level":"3" instead of the correct one, "max_level":3:

  • Bad — max_level is a string: <!-- wp:simpletoc/toc {"max_level":"3"} /-->

  • Good — max_level is an integer: <!-- wp:simpletoc/toc {"max_level":3} /-->

So be sure to respect the attribute type and in your case, you would want to use Number( level ) like so:

props.setAttributes( { max_level: Number( level ) } )

BTW, I could've just pointed you to my answer here, but I thought it'd be better if I write an answer specific to this very question. :)

Additional Notes

  • The ServerSideRender component uses wp.element.RawHTML() which wraps the output in a div, hence you shouldn't wrap the ServerSideRender in a p, which (in my case) resulted in an error (in the console) which says, "<div> cannot be a descendant of <p>" ( in fact, the regular HTML <p><div>something</div></p> is indeed invalid :) ). So,

    // Instead of this:
    <p><ServerSideRender .../></p>
    
    // Use a div or Fragment as a wrapper:
    <div><ServerSideRender .../></div>
    <><ServerSideRender .../></>
    
    // Or simply, no wrapper:
    <ServerSideRender .../>
    
6
  • 1
    Thank you very much for the detailed explanation. I will thank you and Tom J Nowell for helping me with this plugin in the credits. I learn so much.
    – Marc
    Commented Jan 12, 2021 at 7:56
  • 1
    Thanks. I will change that asap
    – Marc
    Commented Jan 12, 2021 at 14:02
  • 1
    Saved my day. Thanks! Commented Aug 13, 2021 at 7:02
  • 1
    Thanks! very important this part " respect the attribute type"
    – Klian
    Commented Oct 28, 2021 at 8:25
  • 1
    Thank you so much for explaining, this is excellent!
    – Paxjah
    Commented Apr 19, 2022 at 8:11

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.