ブロックエディター

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>
		);
	},
} );

Gutenbergのブロックを作る

目次

  1. ブロック作成の概説
    1. 必要知識とスキル
    2. 作例ブロック
  2. 開発環境の用意
    1. ビルドしてみる
  3. ブロック本体 index.js の調整
  4. InnerBlockのjsファイルを作成・登録
    1. InnerBlock用 jsファイルの作成
    2. InnerBlockを使えるようにする
  5. InnerBlockの実装
    1. edit メソッドにコンポーネントを登録
    2. attributes を設定
    3. HTMLでの保存をSaveメソッドに実装
  6. index.js の edit/save メソッド
  7. スタイルの調整
  8. 完成

1. ブロック作成の概説

ブロックの作成には、WordPress用のnpmパッケージである@wordpress/create-blockを利用します。

Create Block は公式でサポートされるブロック作成方法です。WordPress プラグインを使用してブロックを登録します。Create Block はモダンなビルド設定を提供します。構成は必要ありません。PHP、JS、CSS コード、その他、プロジェクトの開始に必要なすべてのファイルを生成します。

@wordpress/create-block – Japanese Team — WordPress.org

npx コマンドを実行すると、プラグインのPHPファイル、ブロック用jsファイルの雛形、scssファイルなど、Gutenbergブロック用のファイルと、ビルド環境が一式用意されます。jsはECMAScript2015( ES6 / ESNext )で書いていきます。

1.1 必要知識とスキル

ECMAScript2015 と JSX で書いて Babel でビルドしますが、ビルド環境は公式スクリプトが自動で構築してくれます。npm runができれば大体、大丈夫でしょう。JavaScript と JSX は基本的な構文や仕組みが分かっていれば問題ないかと思います。npm も package.json が読めてnpmコマンドが使えれば十分でしょう。

他には、プラグインのPHPでブロック用js/CSSを読み込ませますので、WordPressにおけるスクリプトのエンキュー方法など、確認しておくといいかと思います。 wp_register_script() | WordPress Developer Resources

ローカルでのWordPress実行環境は今回 wp-env を利用しました。ローカル実行環境は各自でご用意下さい。

1.2 作例ブロック

このようなフレームワークに関する定義リストのブロックを作ってみたいと思います。イメージとしてはACFの繰り返しフィールドに近いものです。

左図がGutenbergエディタでの表示と入力のモックアップで、右が表示のモックです。

gutenberg-block
block-page-view

フレームワーク名をdt要素、登場年、言語、説明をそれぞれ3つのdd要素とします。このdt×1・dd×3を一組として、一つの定義リスト(dl要素)内に複数のフレームワークの説明を入力し、表示します。

InnerBlockとGutenbergコンポーネントを組み合わせて作っていきます。

2. 開発環境の用意

適当なフォルダで、npx @wordpress/create-block で対話モードとして実行し、スラッグと名前空間、ブロックのタイトルを指定しました。各項目で特に指定がない場合は、何も入力せずenterでデフォルト値が使われます。「The short description for your block」やライセンス項目などは今回、特に指定していません。

下記のように、最低限スラッグだけ指定すればいいのですが、ブロックの識別子がcreate-block/となってしまう上に、後から識別子を変更するのは結構手間がかかるため、最初に指定しました。

npx @wordpress/create-block framework-dl-block

セットアップが完了すると、このように表示されます。

npxを実行したフォルダ内に、framework-dl-blockというスラッグ名のフォルダが生成され、フォルダ内にはプラグインとして使えるようにファイル一式が入っています。

生成されるファイルは下記の通りです。
editorconfig や gitignore も揃っているので、このままgit init / git commit -am "initial commit"してGitでのトラッキングを始められます。

.editorconfig
.gitignore
block.json
framework-dl-block.php
package-lock.json
package.json
readme.txt
build
|       index.asset.php
|       index.css
|       index.js
|       style-index.css
|               
src
|       edit.js
|       editor.scss
|       index.js
|       save.js
|       style.scss
|       
node_modules

ビルドコマンドを叩くと、src 内の jsファイルが babel によってビルドされてbuid フォルダに書き出されます。build/index.js ファイルを framework-dl-block.php 内のwp_register_script()関数がエンキューすることで、Gutenberg内でオリジナルのブロックが使えるようになります。

ビルドしてみる

@wordpress/eslint-pluginとしてeslintもインストールされていますので、必要なら.eslintrcをプロジェクトルートに配置します。

一度ビルドしてみます。

npm run build

srcのjs、scssがビルドされてbuildフォルダに出力されます。

セットアップ完了後に表示される、最初のコマンドnpm startを試します。

npm start

こちらはファイル監視をして、変更が保存されると自動でビルドします。eslintで文法チェックもされているので、jsの文法が間違っているとエラーメッセージが出力され、ビルドは行われません。

「return文が関数の外側である」という構文エラー

終了する時は、ctrl-cです。

コーディングスタイルに準拠するよう自動整形も利用できます。

npm run format:js
Gutenbergのコーディングスタイルに合わせるよう自動整形してくれる

3. ブロック本体 index.js の調整

index.jsではregisterBlockType関数に、ブロックの識別子とオブジェクトが渡されています。オブジェクト内は、各種設定・editメソッド・saveメソッドなど、Gutenbergのブロックとして必要な機能や設定が記述されています。edit/saveでのHTML出力はJSXで書かれています。
参照: ブロックの登録 – WordPress.org / JSX の導入 – React

npx @wordpress/create-blockで生成されたindex.jsは、そのままだと今回は使いにくいので調整します。

コメントは全削除。プロパティやコンポーネントの説明と参照URLが書いてありますが、コードの流れが追いにくくなるため削除します。説明とURLは目を通しておいて下さい。
edit.js/save.js も削除して import 文も削除。分割するほどの行数ではなく、InnerBlock の js にも edit / save のメソッドを実装するため、ファイル構成が冗長になってしまいますので、今回は削除。
多言語対応の __関数のインポートも削ってしまってもいいのですが、今回は残してます。

index.jsは一旦、このようになります。

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './style.scss';

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: 'smiley',
	supports: {
		// Removes support for an HTML mode.
		html: false,
	},
	edit: ( { className } ) => {
		return (
			<p className={ className }>
				{ __(
					'Framework description list',
					'framework-dl-block'
				) }
			</p>
		);
	},
	save: () => {
		return null;
	},
} );

この状態で、ビルドを通せば最低限のブロックとして動作します。

4. InnerBlockのjsを作成、登録

4.1 InnerBlock用 jsファイルの作成

InnerBlockとして、もう一つ、ブロックのjsファイルを作ります。

src/framework-dd-block.js

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './style.scss';

registerBlockType( 'framework-dl/description-block', {
    title: __( 'Framework description ', 'framework-dl-block' ),
    description: __( 'Framework descriptions block', 'framework-dl-block' ),
    category: 'widgets',
    parent: [ 'framework-dl/framework-dl-block' ],
    supports: {
        // Removes support for an HTML mode.
        html: false,
    },
    edit: ( { className } ) => {
        return (
            <p className={ className }>
                { __(
                    'Framework description block, this is inner block!',
                    'framework-dl-block'
                ) }
            </p>
        );
    },
    save: () => {
        return null;
    },
} );

index.jsファイルをコピーして、framework-dd-block.js にリネームして中身を書き換えました。
変更点は主に二つ。ブロック名と parent プロパティです。

registerBlockType( 'framework-dl/description-block', {

index.jsで登録するdlブロックと被らないよう、第1引数であるブロックの一意識別子を変えておきます。index.jsにてInnerBlockとして許可するブロックを指定する際に、この識別子を利用します。ファイル名と識別子は一致していなくても構いません。

また、下記のように「親」ブロックを指定することで、ページ内に自由に挿入できなくなります。InnerBlockとしてのみ使用するので、今回は「親」ブロックを設定して、framework-dl-block 以外では description-block を使えなくします。

parent: [ 'framework-dl/framework-dl-block' ],

この時点での、srcフォルダ内のファイルは下記の通りです。

src
        framework-dd-block.js
        index.js
        style.scss

4.2 InnerBlockを使えるようにする

先に作ったframework-dd-block.jsをInnerBlockとして使えるように、index.jsのedit/saveメソッドを書き換えます。
参照:ネストしたブロック: InnerBlocks の使用 – WordPress.org

src/index.js – import 文

import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import './style.scss';
import './framework-dd-block.js';

InnerBlocksと、先に作った インナーブロック用jsファイルをインポートします。framework-dd-block.jsのregisterBlockType()が実行されればいいので、変数に代入しません。そもそもframework-dd-block.jsにはexport宣言がないので、何も代入できません。

src/index.js – editメソッド

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

<InnerBlocks>タグのプロパティで、許可するブロックを指定します。allowedBlocksには framework-dd-block.js で登録したブロックの識別子を入れます。一種類しか許可しない場合でも、配列でないと allowedBlocksプロパティに渡せないので配列に入れて代入します。

saveメソッドも、InnerBlocksの内容を保存するよう実装します。

src/index.js – saveメソッド

save: () => {
    return <InnerBlocks.Content />;
},

この時点で、Gutenbergエディタでは「Framework description list」ブロック内に、 「Framework description block, this is inner block! 」というテキストのブロックを複数、挿入できるようになっているはずです。

Gutenbergのエディターモードを切り替えてコードエディターモードにすると、ブロックの開始と終了を表すHTMLコメントのみが保存されていると思います。

<!-- wp:framework-dl/framework-dl-block -->
<!-- wp:framework-dl/description-block /-->

<!-- wp:framework-dl/description-block /-->

<!-- wp:framework-dl/description-block /-->
<!-- /wp:framework-dl/framework-dl-block -->

5. InnerBlockの実装

5.1 edit メソッドにコンポーネントを登録

Gutenbergエディタでブロック内に表示するフォームは、Gutenbergのコンポーネントを使って実装していきます。入力フォームは下図のようにControlコンポーネントを利用します。
参照: Component Reference | Block Editor Handbook | WordPress Developer Resources

使うコンポーネントは、

framework-dd-block.jsにて@wordpress/componentsモジュールから上記コンポーネントをインポートするよう宣言を追記しておきます。

framework-dd-block.js

import {
	Flex,
	TextControl,
	__experimentalNumberControl as NumberControl,
	SelectControl,
	TextareaControl,
} from '@wordpress/components';

登場年を入力する NumberControl は WordPress5.5のGutenbergでは実験的な実装となっているため、__experimentalNumberControlと長いエクスポート名になっています。NumberControl変数として扱えるようにエイリアスを使ってインポートします。

Flex コンポーネントは、タグで囲うだけでフレックスボックスにしてくれます。エディターでのレイアウトを組むのに便利なので、合わせてインポートしています。(editor-style.cssでフレックスボックスを指定することもできます)

続いてframework-dd-block.jsのeditメソッドにJSXでコンポーネントを記述します。

edit: ( { attributes, setAttributes } ) => {
	const { name, since, language, description } = attributes;
	return (
		<>
		<TextControl
			placeholder="Framework name"
			value={ name }
			onChange={ ( value ) => setAttributes( { name: value } ) }
		/>
		<Flex>
			<NumberControl
				label="登場年"
				shiftStep={ 1 }
				value={ since }
				onChange={ ( value ) =>
						setAttributes( { since: value } )
				}
			/>
			<SelectControl
				label="言語"
				options={ [
					{ label: 'PHP', value: 'PHP' },
					{ label: 'JavaScript', value: 'JavaScript' },
					{ label: 'Ruby', value: 'Ruby' },
					{ label: 'Erlang', value: 'Erlang' },
					] }
				value={ language }
				onChange={ ( value ) =>
						setAttributes( { language: value } )
				}
			/>
		</Flex>
		<TextareaControl
			placeholder="概要"
			value={ description }
			onChange={ ( value ) =>
				setAttributes( { description: value } )
				}
		/>
		</>
	);
},

attributessetAttributes はブロックへの入力内容を保持するために必要なオブジェクト、関数です。editメソッド内で利用するため、引数として渡しておきます。グローバル変数のような感じで、特に宣言なく利用できます。attributesについて詳しくは次節で説明します。

HTML要素に規定されている属性に対しても、placeholder="概要"のようにプロパティを設定できます。

valueに代入した内容が、各input要素の内容としてGutenbergエディタで表示されます。

onChangeには変更を検知した際の動作を書きます。今回はvalueattributesプロパティに代入しています。

余談:setAttributesの他にも、特に宣言なくregisterBlockType関数で利用できる変数がいくつかありますが、公式ハンドブックにも解説がない場合があります。気づいたらイシューを立ててみたり、ドキュメントを書いてみるといいでしょう。
Project Overview | Block Editor Handbook | WordPress Developer Resources
gutenberg/docs at master · WordPress/gutenberg · GitHub

5.2 attributesを設定

ブロック内で入力した内容はattributesプロパティを利用して保存、読み出しを行います。

attributes プロパティはすべての利用可能な属性と対応する値を表します。属性はブロックタイプ登録の際に attributes プロパティで記述されます。属性ソースを指定する方法については属性のドキュメントを参照してください。

edit と save – Japanese Team — WordPress.org

registerBlockType関数の第2引数のオブジェクトにattributesプロパティを追記します。attributesのプロパティ(=属性)には、保持したい値についての設定を記述していきます。

Framework dl blockでは、下表のように4種類の属性を保持します。

入力内容使用コンポーネント保存先
フレームワーク名TextControlattributes.name
登場年NumberControlattributes.since
言語SelectControlattributes.language
概要TextareaControlattributes.description

完成形ではHTMLから読み出してattributesにセットするため、年を表すsince属性もintegerでなく、stringとします。

parent: [ 'framework-dl/framework-dl-block' ],
supports: {
	// Removes support for an HTML mode.
	html: false,
},

attributes: {
	name: {
		type: 'string',
	},
	since: {
		type: 'string',
		default: 2010,
	},
	language: {
		type: 'string',
	},
	description: {
		type: 'string',
	},
},

edit: ( { attributes, setAttributes } ) => {

attributesで属性を設定したので、onChangesetAttributesを実行すると、入力内容がattributesに反映されます。

onChangeイベント時に、value と attributes を console.log に出力するとこのようになります。sinceプロパティはデフォルト値の2010が格納されています。

<TextControl
	placeholder="Framework name"
	value={ name }
	onChange={ ( value ) => {
		console.log( 'TextControl value: ' + value );
		setAttributes( { name: value } );
		console.log( attributes );
		}
	}
/>
name: framewor となっていて、保存完了までは若干ラグがある

5.3 HTMLでの保存をSaveメソッドに実装

saveメソッドがnullを返しているので、HTMLで保存するように実装します。

下記のようにブロックで入力したとして、

input-sample

現時点では入力内容はJSON形式で、ブロックの開始、終了を表すHTMLコメント内に保持されます。Gutenbergをコードエディターモードに切り替えれば実際の保存内容が確認できます。このままではHTMLが保存されないためプレビューや投稿ページには何も表示されませんので、saveメソッドでJSXを記述しHTMLでDBに保存するよう実装します。

逆にnullのままにしておけば、HTML構造に依存しない形でブロックの内容をDBに保存できます。このようにJSONで保存した場合は、HTMLを出力するレンダー関数を別途PHPにて実装する必要があります。

<!-- wp:framework-dl/framework-dl-block -->
<!-- wp:framework-dl/description-block {"name":"framework","since":2016,"language":"JavaScript","description":"A framework description.\nSomthing framework distinction."} /-->

<!-- wp:framework-dl/description-block {"name":"framework 2","since":2009,"description":"A framework description.\nSomthing framework distinction."} /-->
<!-- /wp:framework-dl/framework-dl-block -->

今回は、HTMLで保存するため、saveメソッドは下記のようになります。

save: ( { attributes } ) => {
	return (
	<>
		<dt>{ attributes.name }</dt>
		<dd>
			<span className="since">{ attributes.since }</span>年~
		</dd>
		<dd>
			言語:<span className="language">{ attributes.language }</span>
		</dd>
		<dd className="description">{ attributes.description }</dd>
	</>
	);
},

dt dd要素として表示したいフレームワーク名や言語、説明は、attributesに保持されているので、引数としてattributesを渡します。editメソッドのように、複数の変数へ分割代入せずに、直接attributes.nameなどで値を呼び出しています。

登場年と言語に関しては、attributes.since / attributes.languagespanタグで括っています。下書きや公開状態の投稿をGutenbergエディターを開いて編集する場合、GutenbergはDBを読み込んでattributesに値をセットして表示する仕組みになっています。

そのために、保存したHTMLのどこからname / sinceなどの値を読み取るかの指定をattributesプロパティに追記します。

attributes: {
	name: {
		type: 'string',
		source: 'text',
		selector: 'dt',
	},
	since: {
		type: 'string',
		default: '2010',
		source: 'text',
		selector: 'span.since',
	},
	language: {
		type: 'string',
		source: 'text',
		selector: 'span.language',
	},
	description: {
		type: 'string',
		source: 'text',
		selector: 'dd.description',
	},
},

SelectorプロパティでHTMLを指定します、jQueryでDOMを取得する感じです。
Sourceプロパティでは取得したHTML全体を使うか、内部テキストを使うか、などを指定します。
defaultでデフォルト値を入れておくこともできます。
属性 – Japanese Team — WordPress.org

例えばdescription属性は、descriptionクラスのddタグから内部テキストを取得する設定となっています。

ブロックが表示される時には、このsource / selectorの設定に基づいてDBのHTMLからパースした値をattributesにセットし、editメソッドではコンポーネントのvalueattributes内の値を代入します。このようにして、DBに保存している内容をGutenbergで表示しています。

実際にどういうタイミングで読取りや受け渡しが行われるかは、Reactコンポーネントのライフサイクルとかに基づいていると思いますが、詳しくは調べてません。 state とライフサイクル – React

InnerBlockのjsはこれで完成で、最終的なコードはこうなります。
src/framework-dd-block.js

import { registerBlockType } from '@wordpress/blocks';
import {
	Flex,
	TextControl,
	__experimentalNumberControl as NumberControl,
	SelectControl,
	TextareaControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';

import './style.scss';

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

	description: __( 'Framework descriptions block', 'framework-dl-block' ),

	category: 'widgets',

	parent: [ 'framework-dl/framework-dl-block' ],
	supports: {
		// Removes support for an HTML mode.
		html: false,
	},

	attributes: {
		name: {
			type: 'string',
			source: 'text',
			selector: 'dt',
		},
		since: {
			type: 'string',
			default: 2010,
			source: 'text',
			selector: 'span.since',
		},
		language: {
			type: 'string',
			source: 'text',
			selector: 'span.language',
			default: '',
		},
		description: {
			type: 'string',
			source: 'text',
			selector: 'dd.description',
		},
	},

	edit: ( { attributes, setAttributes } ) => {
		const { name, since, language, description } = attributes;
		return (
			<>
				<TextControl
					placeholder="Framework name"
					value={ name }
					onChange={ ( value ) => setAttributes( { name: value } ) }
				/>
				<Flex>
					<NumberControl
						label="登場年"
						shiftStep={ 1 }
						value={ since }
						onChange={ ( value ) =>
							setAttributes( { since: value } )
						}
					/>
					<SelectControl
						label="言語"
						options={ [
							{ label: 'PHP', value: 'PHP' },
							{ label: 'JavaScript', value: 'JavaScript' },
							{ label: 'Ruby', value: 'Ruby' },
							{ label: 'Erlang', value: 'Erlang' },
						] }
						value={ language }
						onChange={ ( value ) =>
							setAttributes( { language: value } )
						}
					/>
				</Flex>

				<TextareaControl
					placeholder="概要"
					value={ description }
					onChange={ ( value ) =>
						setAttributes( { description: value } )
					}
				/>
			</>
		);
	},

	save: ( { attributes } ) => {
		return (
			<>
				<dt>{ attributes.name }</dt>
				<dd>
					<span className="since">{ attributes.since }</span>年~
				</dd>
				<dd>
					言語:
					<span className="language">{ attributes.language }</span>
				</dd>
				<dd className="description">{ attributes.description }</dd>
			</>
		);
	},
} );

6. index.js の edit/save メソッド

iconを変更して、edit/saveメソッドはdlタグを返すよう実装します。

icon: 'feedback',

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

save: () => {
	return (
		<dl>
			<InnerBlocks.Content />
		</dl>
	);
},

Gutenbergでのスタイルを調整したいのでクラス名を付与したいのですが、saveメソッドで返された最上位のHTML(ここではdl要素)には、自動的にclassNameが付与されます。よって、特に指定はなし。
editメソッドではルート要素(returnされる最上位の要素)が見た目と一致しない場合があるため、明示的にclassNameを指定する必要があるそうです。

実際にWordPressが投稿を表示する際にthe_content()関数で出力するHTMLはこのようになります。

<dl class="wp-block-framework-dl-framework-dl-block">
  <dt>framework</dt>
  <dd><span class="since">2007</span>年~</dd>
  <dd>言語:<span class="language">JavaScript</span></dd>
  <dd class="description">A framework description. Something framework distinction.</dd>
</dl>

参照: Edit and Save | Block Editor Handbook | WordPress Developer Resources

デフォルトで付与されるclassNameは、registerBlockType関数に渡された一意識別子から生成されるようです。
framework-dl/framework-dl-block → wp-block-framework-dl-framework-dl-block

7.スタイルの調整

ブロックに対しクラス名が付与されているので、スタイルを当てます。

scssファイルが既に用意されていて、background-colorpadding が指定されているので、追記していきます。

src/style.scss

.wp-block-framework-dl-framework-dl-block {
	background-color: none;
	padding: 2px;

	dt {
		border-bottom: solid 1px black;
	}
	dd, dt {
		margin-left: 0;
	}
}

src/index.jsで、このscssファイルがインポートされているので一緒にビルドされます。

import './style.scss';

また、ビルド済みのCSSファイルは build/style-index.css で、framework-dl-block.phpにてエンキューの登録がされています。

$style_css = 'build/style-index.css';
wp_register_style(
	'framework-dl-framework-dl-block-block',
	plugins_url( $style_css, __FILE__ ),
	array(),
	filemtime( "$dir/$style_css" )
);

完成

framework-dl-block-post

コードエディターモードにすると、下記のようなHTMLでDBに保存されていることが分かります。

<!-- wp:framework-dl/framework-dl-block -->
<dl class="wp-block-framework-dl-framework-dl-block"><!-- wp:framework-dl/description-block -->
<dt>CakePHP</dt>
<div class="flex"><dd><span class="since">2006</span>年~</dd><dd>言語:<span class="language">PHP</span></dd></div>
<dd class="description">Ruby on Railsの概念を多く取り込んだWEBアプリケーションフレームワーク。PHPでは老舗。ビューが柔軟に構築できる。</dd>
<!-- /wp:framework-dl/description-block -->

<!-- wp:framework-dl/description-block -->
<dt>Ruby on Rails</dt>
<div class="flex"><dd><span class="since">2005</span>年~</dd><dd>言語:<span class="language">Ruby</span></dd></div>
<dd class="description">WEBアプリケーションフレームワークとしては古くかつ有名。MVC、スキャフォルド、設定より規約などの特徴を持ち、DRYであることを基本理念としている。2020年現在も広く使われている。</dd>
<!-- /wp:framework-dl/description-block -->

<!-- wp:framework-dl/description-block -->
<dt>Vue.js</dt>
<div class="flex"><dd><span class="since">2014</span>年~</dd><dd>言語:<span class="language">JavaScript</span></dd></div>
<dd class="description">プロジェクトの一部分に使うことも、シングルページアプリケーションの構築もできる軽量シンプルなUIフレームワーク。開発者は元々GoogleでAngular.jsの開発に携わっていた。</dd>
<!-- /wp:framework-dl/description-block -->

<!-- wp:framework-dl/description-block -->
<dt>Laravel</dt>
<div class="flex"><dd><span class="since">2010</span>年~</dd><dd>言語:<span class="language">PHP</span></dd></div>
<dd class="description">後発ながら高いシェアを誇る。Webアプリケーションに必要な機能は全部入り。フロントはVue.js、バックエンドは依存性注入ができる。Artisanコマンドも種類が豊富。</dd>
<!-- /wp:framework-dl/description-block --></dl>
<!-- /wp:framework-dl/framework-dl-block -->

GitHubにコードをアップしています。

https://github.com/akiya64/framework-dl-block/releases/tag/v1.0.0