Gutenberg インナーブロック 発展編

先に作ったサンプルのブロック―フレームワークに関する説明リストのブロックを元にインナーブロックのTips的なカスタマイズ、活用を紹介します。

参照: Gutenbergのブロックを作る – Autumnsky

ブロック追加時にインナーブロックを予め入れておく

ブロック挿入時にインナーブロックを入れておく

InnerBlocks の template プロパティを設定する。普通の説明リストなら、常に dt ブロックと dd ブロックを組で挿入する等もできる。

src/index.js

edit: ( { className } ) => {
    return (
        <dl className={ className }>
            <InnerBlocks
                allowedBlocks={ [ 'framework-dl/description-block' ] }
                template={ [ [ 'framework-dl/description-block', {} ] ] }
            />
        </dl>
    );
},

template プロパティには配列を渡す。配列は[ ブロック名, 引数オブジェクト ]の配列を1つ以上含める。

これで、flamework 説明ブロックを挿入した時に、description ブロックが一つ入ります。

追加ボタンをカスタマイズする

インナーブロックの追加は Add ボタンに変えてみた。

renderAppenderプロパティを設定すると、Gutenberg デフォルトのbutton-block-appender コンポーネントを上書きして、任意のタグと動作を指定できる。
ただ、このプロパティはハンドブックなどに記載がみつかりませんでした。

edit: ( { className } ) => {
    return (
        <dl className={ className }>
            <InnerBlocks
                allowedBlocks={ [ 'framework-dl/description-block' ] }
                renderAppender={ 
                    // JSXを返す関数 //
                }
	    />
        </dl>
    );
},

JSX を返す無名関数を実装。

renderAppender={ () => (
	<button
		type="button"
		onClick={
                     // ブロックを追加する関数 //
                }
        >
		{ __( 'Add', 'framework-dl-block' ) }
	</button>
	)
}

クリック時の動作も自分で設定する必要があるので、ブロック追加の関数を改めて実装する。ということで、onClickメソッドは下記の通り。

onClick={ () => {
    dispatch('core/block-editor').insertBlocks(
        createBlock('framework-dl/description-block'), 9999, clientId
    ) 
} }

dispatch オブジェクトは@wordperss/dataから、createBlock 関数は@wordpress/blocksからインポートしておく。

import { registerBlockType, createBlock } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

それとclientIdという実行時に割り振られるプロパティが要るが、特に宣言なしで呼び出せるので、editメソッドの引数として渡しておく。

editメソッド全体は下記の通り。

edit: ( { className, clientId } ) => {
	return (
		<dl className={ className }>
			<InnerBlocks
				allowedBlocks={ [ 'framework-dl/description-block' ] }
				template={ [ [ 'framework-dl/description-block', {} ] ] }
				renderAppender={ () => (
					<button
						type="button"
						onClick={ () => { 
							dispatch('core/block-editor').insertBlocks(
								createBlock('framework-dl/description-block'), 9999, clientId)
						} }
					>
						{ __( 'Add', 'framework-dl-block' ) }
					</button>
				) }
			/>
		</dl>
	);
},

親ブロックでインナーブロックの値を利用する

インナーブロックの値を取得するにはwithSelect を使います。
例えば、インナーブロックの登場年を取得して { minYear }~{ maxYear }年 という表示をすることもできる。

witheSelect( 関数1 )( 関数2 ) のように書く。関数1にはデータを取得する関数、関数2にレンダー関数などを渡す。withSelect自身は高階関数のように動作するとのことです。withSelect@wordperss/dataからインポートします。

editメソッドにするとこうなる

edit: withSelect( ( select, blockData ) => {
		return {
			innerBlockProps: select( 'core/block-editor' ).getBlocks( blockData.clientId ),
		};
	} )( ( { className, clientId, innerBlockProps } ) => {
		return (
			<dl className={ className }>
                           …

関数1、関数2ともに無名関数として実装。関数1でのデータ取得結果は、関数2の引数として利用できる。上記のコードでは、innerBlockProps プロパティとした。

このinnerBlockPropsの中身は配列になっており、インナーブロックの数だけオブジェクトが格納されている。オブジェクトの中身はインナーブロックの内容である。

(2) […]
    0: {…}
​​        attributes: {…}
​​​            description: "Framework1 description."
​​​            language: ""
​​​            name: "Framework1"
​​​            since: "2011"
​​​            <prototype>: Object { … }
​​        clientId: "60bcf62c-034f-45dd-82ba-f6b1b98c217c"
​        innerBlocks: Array []
​​        isValid: true
​​        name: "framework-dl/description-block"
​​        originalContent: "<dt>Framework1</dt><div class=\"flex\"><dd><span class=\"since\">2011</span>年~</dd><dd>言語:<span class=\"language\"></span></dd></div><dd class=\"description\">Framework1 description.</dd>"
​       validationIssues: Array []
​​       <prototype>: Object { … }

​    1: ​{…}
​​​        attributes: {…}
​​​        clientId: "60bcf62c-034f-45dd-82ba-f6b1b98c217c"
​​        innerBlocks: Array []
​​        isValid: true
​             …
length: 2
​<prototype>: Array []

単純にエディター内で表示するだけなら、これでもいい。

…
} )( ( { className, clientId, innerBlockProps } ) => {
    const years = innerBlockProps.map( prop => prop.attributes.since );
    return (
          <>
          <p>{ Math.min( ...years ) }~{ Math.max( ...years ) }年</p>
          <dl className={ className }>
              …
もちろんリアクティブ

実用的にはattributesに格納して、saveメソッドでも使えるようにしておく。

attributes: {
	minYear: {
		type: 'string',
	},
	maxYear: {
		type: 'string',
	},
},

edit: withSelect( ( select, blockData ) => {
	return {
		innerBlockProps: select( 'core/block-editor' ).getBlocks( blockData.clientId ),
	};
} )( ( { className, clientId, innerBlockProps, attributes, setAttributes } ) => {
	const years = innerBlockProps.map( prop => prop.attributes.since );
	setAttributes( { minYear: Math.min( ...years ) } );
	setAttributes( { maxYear: Math.max( ...years ) } );
	return (
		<>
		{ attributes.minYear !== attributes.maxYear &&
			<p>{ attributes.minYear }~{ attributes.maxYear }年</p>
		}
		<dl className={ className }>
			<InnerBlocks
				... 中略 ...
		</dl>
		</>
	);
} ),

save: ( { attributes } ) => {
	return (
	<div>
		{ attributes.minYear !== attributes.maxYear &&
			<p>{ attributes.minYear }~{ attributes.maxYear }年</p>
		}
		<dl>
			<InnerBlocks.Content />
		</dl>
	</div>
	);
},

インナーブロックが一つしかないとか、同じ年ばかりだと、2010~2010年 と表示されて不自然なので、条件判定をいれた。

{ attributes.minYear !== attributes.maxYear &&
	<p>{ attributes.minYear }~{ attributes.maxYear }年</p>
}

withSelectに関しては、文法的なところも今ひとつ理解できていないし、ハンドブックにもEditメソッドでの使い方の記載がないんですが、Gutenberg のイッシューで「Edit に渡すなら withSelect を使う」と返答があって、実際これで期待通り動きます。
How to know if an InnerBlock is selected · Issue #22282 · WordPress/gutenberg

withSelect関数自身は、内部的にはReactのフックを呼んでいるらしい。
参考: Introducing useDispatch and useSelect – Make WordPress Core

ブロック内部のデータを取得するだけなら、useSelect関数を使うのがよいそうです。

全部入りコード

ここまでのTipsを全て組み込むと、コードはこの通り。
@wordpress/create-block でインストールしたリンターに基づいて、コーディングスタイルの整形をかけてあります。

import { registerBlockType, createBlock } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';
import { dispatch, withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

import './editor.scss';
import './style.scss';

import './framework-dd-block.js';

registerBlockType( 'framework-dl/framework-dl-block', {
	title: __( 'Framework description list', 'framework-dl-block' ),

	description: __(
		'Example block written with ESNext standard and JSX support – build step required.',
		'framework-dl-block'
	),

	category: 'widgets',

	icon: 'feedback',

	attributes: {
		minYear: {
			type: 'string',
		},
		maxYear: {
			type: 'string',
		},
	},

	edit: withSelect( ( select, blockData ) => {
		return {
			innerBlockProps: select( 'core/block-editor' ).getBlocks(
				blockData.clientId
			),
		};
	} )(
		( {
			className,
			clientId,
			innerBlockProps,
			attributes,
			setAttributes,
		} ) => {
			const years = innerBlockProps.map(
				( prop ) => prop.attributes.since
			);
			setAttributes( { minYear: Math.min( ...years ) } );
			setAttributes( { maxYear: Math.max( ...years ) } );

			return (
				<>
					{ attributes.minYear !== attributes.maxYear && (
						<p>
							{ attributes.minYear }~{ attributes.maxYear }年
						</p>
					) }
					<dl className={ className }>
						<InnerBlocks
							allowedBlocks={ [
								'framework-dl/description-block',
							] }
							template={ [
								[ 'framework-dl/description-block', {} ],
							] }
							renderAppender={ () => (
								<button
									type="button"
									onClick={ () => {
										dispatch(
											'core/block-editor'
										).insertBlocks(
											createBlock(
												'framework-dl/description-block'
											),
											9999,
											clientId
										);
									} }
								>
									{ __( 'Add', 'framework-dl-block' ) }
								</button>
							) }
						/>
					</dl>
				</>
			);
		}
	),

	save: ( { attributes } ) => {
		return (
			<div>
				{ attributes.minYear !== attributes.maxYear && (
					<p>
						{ attributes.minYear }~{ attributes.maxYear }年
					</p>
				) }
				<dl>
					<InnerBlocks.Content />
				</dl>
			</div>
		);
	},
} );

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください