Getting attributes of InnerBlocks and saving them to parent
Asked Answered
A

1

6

I have created a "Tabbed Panels" (tabbed content) block that is essentially just an InnerBlocks component that only allows the block "Panel". When you create a Panel you must give the Panel a heading which is then used in the Panel as well as the Tab button. So in my render function for Tabbed Panels, I need to pull the heading from the children Panel blocks.

There are a couple approaches I could use, like just using regex in the tabbed-panels-render.php function to search the children html for the proper attributes, but this doesn't seem like the best approach.

I think the simplest solution would be to listen for any changes to the Panel blocks and saves the changes (heading and id in this case) to the parent. My current approach is based off this discussion which uses hooks to listen for changes. That part seems to work fine, but I need to save the output somewhere so I'm saving them as attributes to the Tabbed Panels block. This seems to work fine at first, but putting the "setAttributes" method directly in the edit function leads to issues. If there are too many Tabbed Panel blocks on the page, then React throws a "too many renders" error.

Where should my "setAttributes" function live, or is there a better approach to passing data from child to parent? I thought about using the useDispatch hook in the child, but I need to check a lot of events (the heading changes, the block is reordered, the block is deleted, etc.)

Here are the relevant js and php files. There are some custom elements, but you should be able to parse it.

tabbed-panels.js

import { arraysMatch } from 'Components/utils.js'
const { InnerBlocks } = wp.blockEditor
const { createBlock } = wp.blocks
const { Button } = wp.components
const { useDispatch, useSelect } = wp.data
const { __ } = wp.i18n

export const tabbedPanels = {
  name: 'my/tabbed-panels',
  args: {
    title: __('Tabbed Panels', '_ws'),
    description: __('Tabbable panels of content.', '_ws'),
    icon: 'table-row-after',
    category: 'common',
    supports: {
      anchor: true
    },
    attributes: {
      headings: {
        type: 'array',
        default: []
      },
      uids: {
        type: 'array',
        default: []
      }
    },
    edit: props => {
      const { setAttributes } = props
      const { headings, uids } = props.attributes
      const { insertBlock } = useDispatch('core/block-editor')
      const { panelHeadings, panelUids, blockCount } = useSelect(select => {
        const blocks = select('core/block-editor').getBlocks(props.clientId)
        return {
          panelHeadings: blocks.map(b => b.attributes.heading),
          panelUids: blocks.map(b => b.clientId),
          blockCount: select('core/block-editor').getBlockOrder(props.clientId).length
        }
      })
      if (!arraysMatch(panelHeadings, headings)) {
        setAttributes({ headings: panelHeadings })
      }
      if (!arraysMatch(panelUids, uids)) {
        setAttributes({ uids: panelUids })
      }
      return (
        <div className="block-row">
          <InnerBlocks
            allowedBlocks={ ['my/panel'] }
            templateLock={ false }
            renderAppender={ () => (
              <Button
                isSecondary
                onClick={ e => {
                  insertBlock(createBlock('my/panel'), blockCount, props.clientId)
                } }
              >
                { __('Add Panel', '_ws') }
              </Button>
            ) }
          />
        </div>
      )
    },
    save: props => {
      return (
        <InnerBlocks.Content />
      )
    }
  }
}

tabbed-panels-render.php

<?php
function block_tabbed_panels($atts, $content) {
  $atts['className'] = 'wp-block-ws-tabbed-panels ' . ($atts['className'] ?? '');
  $headings = $atts['headings'] ?? '';
  $uids = $atts['uids'] ?? '';
  ob_start(); ?>
    <div class="tabs" role="tablist">
      <?php
      foreach ($headings as $i=>$heading) : ?>
        <button
          id="tab-<?= $uids[$i]; ?>"
          class="tab"
          role="tab"
          aria-selected="false"
          aria-controls="panel-<?= $uids[$i]; ?>"
          tabindex="-1"
        >
          <?= $heading; ?>
        </button>
        <?php
      endforeach; ?>
    </div>
    <div class="panels">
      <?= $content; ?>
    </div>
  <?php
  return ob_get_clean();
}

panel.js

import ComponentHooks from 'Components/component-hooks.js'
const { InnerBlocks, RichText } = wp.blockEditor
const { __ } = wp.i18n

export const panel = {
  name: 'my/panel',
  args: {
    title: __('Panel', '_ws'),
    description: __('Panel with associated tab.', '_ws'),
    icon: 'format-aside',
    category: 'common',
    supports: {
      customClassName: false,
      html: false,
      inserter: false,
      reusable: false
    },
    attributes: {
      heading: {
        type: 'string'
      },
      uid: {
        type: 'string'
      }
    },
    edit: props => {
      const { setAttributes } = props
      const { heading } = props.attributes
      return (
        <>
          <ComponentHooks
            componentDidMount={ () => setAttributes({ uid: props.clientId }) }
          />
          <RichText
            label={ __('Tab Name', '_ws') }
            placeholder={ __('Tab Name', '_ws') }
            tagName="h4"
            onChange={ newValue => setAttributes({ heading: newValue }) }
            value={ heading }
          />
          <InnerBlocks
            templateLock={ false }
          />
        </>
      )
    },
    save: props => {
      return (
        <InnerBlocks.Content />
      )
    }
  }
}

panel-render.php

<?php
function block_panel($atts, $content) {
  $uid = $atts['uid'] ?? '';
  ob_start(); ?>
    <div
      id="panel-<?= $uid ?>"
      class="panel"
      role="tabpanel"
      aria-labelledby="tab-<?= $uid; ?>"
      tabindex="0"
      hidden="hidden"
    >
      <?= $content; ?>
    </div>
  <?php
  return ob_get_clean();
}
Auria answered 23/1, 2020 at 18:17 Comment(0)
Y
6

You can access children blocks from the parent and get the attributes (tab title in your case).

componentDidUpdate(previousProps, previousState) {
    var myID = this.props.clientId;
    var tabs_title = [];
    this.myBlock = wp.data.select('core/block-editor').getBlock(myID);
    this.myBlock.innerBlocks.map(block => {
         tabs_title.push( block.attributes.title );
    });
    this.props.setAttributes({ 'tabs_title': tabs_title });
}
Yearround answered 8/4, 2020 at 6:38 Comment(2)
There is a problem with this approach. setAttributes is triggering componentDidUpdate and this leads to an infinite loop.Drawee
Yes, but you can check with lodash.isEqual if previousProps vs props.attributes has changed, or previousState vs state, or both o none of them.Yearround

© 2022 - 2024 — McMap. All rights reserved.