WordPress Logo WordPress ダイナミックブロックの作り方

WordPress のブロックエディタ Gutenberg でダイナミックブロック(Dynamic Blocks)を作成する方法についての覚書です。render_callback を使って PHP 側でレンダリングする方法、ServerSideRender コンポーネントの使い方、withSelect を使って投稿を取得する方法(Selector や getEntityRecords) などについて。

以下は Node.js がインストールされていて、WordPress のローカル環境があることを前提にしています。

更新日:2020年10月23日

作成日:2020年10月03日

WordPress 関連リンク
関連ページ

ファイルの作成

この例ではファイルの作成及び JavaScript 環境の構築は create-block を使って行っています。

カスタムブロックをプラグインとして作成するので、ターミナルでプラグインディレクトリに移動して npx コマンド「 npx @wordpress/create-block 」を実行します。

以下では対話モードで実行していますが、slug とオプションを指定して実行することもできます。

関連ページ:Gutenberg ブロック開発の環境構築

この例では slug に dynamic-block-sample を指定したので、以下のようなファイルが出力されます。

dynamic-block-sample //作成されたプラグインのディレクトリ
├── block.json  //ブロックディレクトリの登録で使用されるファイル(?)
├── build //ビルドで出力されるファイルのディレクトリ
│   ├── index.asset.php //依存情報とファイルバージョンが記載されるファイル(自動生成)
│   ├── index.css  //editor.scss がビルドで変換された CSS
│   ├── index.js  //ビルドされたブロック用のスクリプト
│   └── style-index.css // style.scss がビルドで変換された CSS
├── node_modules //関連パッケージのディレクトリ
├── dynamic-block-sample.php  //メインのプラグインファイル
├── package-lock.json
├── package.json //パッケージの設定ファイル
├── readme.txt
└── src  //開発用ディレクトリ(この中のファイルを編集)
    ├── edit.js
    ├── editor.scss
    ├── index.js  //ブロック用のスクリプト
    ├── save.js
    └── style.scss

ターミナルで作成されたプラグインのディレクトリに移動して npm start を実行して開発モードにして作業をしていきます。

開発モードを終了するには control + c を押します。

$ cd dynamic-block-sample  return  //プラグインのディレクトリに移動
  
$ npm start  return  //開発モード

procuction ビルドするには npm run build を実行します。

生成された雛形のファイルを以下のように変更します(この例では翻訳関数は使わないので雛形のファイルから翻訳関連の記述とコメントを削除しています)。

src/index.js(ブロック用のスクリプト)
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  edit: Edit,  //edit 関数はインポートした Edit コンポーネントを指定
  save,  //save 関数はインポートした save 関数を指定(save: save と同じこと)
} );   
src/edit.js(edit プロパティに指定する edit 関数:Edit コンポーネントが記述されたファイル)
import './editor.scss';
export default function Edit( { className } ) {
  return (
    <p className={ className }>hello from the editor!</p>
  );
}
src/save.js(save プロパティに指定する save 関数が記述されたファイル)
export default function save() {
  return (
    <p>hello from the saved content!</p>
  );
}

プラグインページで作成したプラグインを有効化し、投稿に挿入すると以下のように表示されます。

以下はフロントエンド側の表示例です。

最初から動的なブロックを作成することもできますが、静的なブロックと比較するため、取り敢えず RichText コンポーネントを使った静的なブロックを作成します。

関連項目:WordPress のコンポーネント/RichText

それぞれのファイルを以下のように変更します。

src/index.js

ブロックを登録・定義する registerBlockType 関数が記述されたエントリーポイントのソース・ファイル(src/index.js)に属性(attributes)を追加します。

src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //入力された値を保存するための属性を設定
  attributes: {
    //タイトル(h3)の内容を保持するための属性
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle' 
    },
    //コンテンツ(div)の内容を保持するためのの属性
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent' 
    },
  },
  edit: Edit,
  save,
} );

src/edit.js

edit 関数が定義されている Edit コンポーネントでは、props 経由で取得するプロパティを分割代入で変数に代入し、return ステートメント内にタイトルと文章を入力する RichText コンポーネントを記述します。

src/edit.js
import './editor.scss';
//RichText コンポーネントを block-editor パッケージからインポート
import { RichText } from '@wordpress/block-editor';

export default function Edit( props ) {
  //プロパティを変数に分割代入
  const { attributes: { myTitle, myContent}, className, setAttributes } = props;
  
  return (
    <div className={ className }  >
      <RichText 
        className='myRichTextTitle'
        value={ myTitle }
        onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
        tagName='h3'
        placeholder= 'タイトルを入力' 
        keepPlaceholderOnFocus={true}
      />
      <RichText
        className='myRichTextContent'
        value={ myContent }
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
        tagName='div'
        multiline= 'p'
        placeholder= '文章を入力' 
      />
    </div>
  );
}

src/save.js

この時点では save 関数では RichText コンポーネントのコンテンツを RichText.Content を使用して保存しています。

src/save.js
//RichText コンポーネントを block-editor パッケージからインポート
import { RichText } from '@wordpress/block-editor';

export default function save( props ) {
  //プロパティを変数に分割代入
  const { attributes: { myTitle, myContent} } = props;
  
  //save 関数でコンテンツを正しく保存するためには RichText.Content を使用
  return (
    <div>
      <RichText.Content
        className='myRichTextTitle'
        value={ myTitle }
        tagName='h3'
      />
      <RichText.Content
        className='myRichTextContent'
        value={ myContent }
        tagName='div'
      />
    </div>
  )
}

妥当性検証プロセス

変更後、投稿のページを再読み込みすると以下のように「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示され、コンソールにはエラーが表示されます。

これは、エディターが現在定義しているものとは異なる save 関数の出力を検出するために発生します。この場合、表示されるボタンをクリックして「ブロックを削除」を選択し、現在のブロックを一度削除してから再度ブロックを挿入します。

※ 静的なブロックでは save 関数の return ステートメント内に変更が発生するとブロックの検証(妥当性検証プロセス)により、ブロックは無効(invalid)としてマークされ上記のように表示されます。

再度ブロックを挿入すると以下のような表示になり、変更が反映されます。

更新(変更)したブロック

RichText を使って変更したブロックでは、タイトルと文章を入力できるようになっています。

Block Editor Handbook/Edit and Save/Validation

PHP でブロックをレンダリング

前のセクションで作成したブロックをダイナミックブロックに変更します。

ダイナミックブロックでは save 関数が null を返すようにします。これによりブロックのレンダリングに PHP が使用され、エディターはデータベースにブロックの属性のみを保存します。

また、save 関数で null を返すとエディターはブロックのマークアップの妥当性検査プロセスをスキップするため、マークアップを変更する際に発生するブロックを一度削除してから再度ブロックを挿入しなければならない問題を回避できます。

例えば新しいクラスを追加したり、HTML 要素を追加してレイアウトを変更してブロックの構造を更新すると、ダイナミックブロックを使わない場合はブロックコードが更新されると Gutenberg の妥当性検証プロセスが適用され、ユーザーに検証メッセージ「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示されますが、ダイナミックブロックを使えばサイト内のすべてのブロックの使用箇所に即座に変更を適用できます。

save 関数で null を返す

save 関数が null を返すようにします。

src/save.js
export default function save() {
  //ブロックを PHP でレンダリングするため save 関数では null を返す
  return null;
}

この例の場合、create-block パッケージを使って生成した雛形のファイルをそのまま使っていたので、save 関数は save.js に記述していましたが、単に null を返すだけなので、index.js に含めてしまい、save.js は削除します(そのままでも問題ありませんが)。

src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
//import save from './save';  //削除

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //入力された値を保存するための属性を設定
  attributes: {
    //タイトル(h3)の内容を保持するための属性
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle' 
    },
    //コンテンツ(div)の内容を保持するためのの属性
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent' 
    },
  },
  edit: Edit,
  // save 関数の定義を直接記述して null を返す
  save: () => { return null }
} );

ブロックを挿入していた投稿を再読み込みすると、ブロックは無効と判定されるので、前回同様、表示されるボタンをクリックして「ブロックを削除」を選択し、現在のブロックを一度削除してから再度ブロックを挿入します。

エディター画面では今まで通りに表示され、タイトルや文章も入力できますが、上記の変更によりフロントエンド側には何も出力されません。

また、この例の場合、入力したタイトルや文章が保存できません。

タイトルや文章を入力したり変更して「更新」ボタンを押して保存しても、ページを再読込すると何も保存されません。これは attributes プロパティで source プロパティを使っているのが原因です。

attributes の source プロパティ

設定している attributes(属性)で source プロパティを使用している場合は変更する必要があります。

source プロパティを設定していると、ブロックのデータを save 関数の出力の属性から取得するめ、PHP でレンダリングされるブロックではサポートされていません(save 関数で null を返しているため)。

このため属性のいずれかが source プロパティを使用している場合はそれらを削除します。

この例の場合、RichText の値を保存する属性で source プロパティを使用しているのでそれらを削除します。また、1つの属性は type を array にしていますが、source プロパティを削除すると機能しないため、type を string に変更します。

src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  attributes: {
    //タイトル(h3)の内容を保持するための属性
    myTitle: {
      type: 'string',
      default: '',
      //source: 'html', //削除
      selector:'.myRichTextTitle' 
    },
    //コンテンツ(div)の内容を保持するためのの属性
    myContent: {
      //type: 'array',   //string に変更
      type: 'string',
      default: '',
      //source: 'children', //削除
      selector:'.myRichTextContent' 
    },
  },
  edit: Edit,
  save: () => { return null }
} );

source プロパティを削除すると、入力した値が保存されるようになります(source プロパティを指定しない場合、属性はブロックのコメントデリミタに JSON 形式で保存されます)。以下はコードエディターでの表示です。

render_callback

ブロックの出力を PHP でレンダリングするには、PHP 側でブロックを登録する register_block_type 関数の第2パラメータの配列に render_callback を追加します。

PHP 側でブロックを登録する register_block_type 関数は、この例ではメインのプラグインファイル dynamic-block-sample.php に記述してあります。

dynamic-block-sample //作成されたプラグインのディレクトリ
├── block.json  
├── build 
│   ├── index.asset.php 
│   ├── index.css 
│   ├── index.js  
│   └── style-index.css 
├── node_modules 
├── dynamic-block-sample.php  //メインのプラグインファイル
├── package-lock.json
├── package.json 
├── readme.txt
└── src  
    ├── edit.js
    ├── editor.scss
    ├── index.js  
    ├── // save.js は削除済み(index.js に統合)
    └── style.scss

register_block_type 関数の第2パラメータの配列に render_callback キーを追加し、値にコールバック関数を指定します。

register_block_type( 'wdl/dynamic-block-sample', array(
  'editor_script' => 'wdl-dynamic-block-sample-block-editor',
  'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
  'style'         => 'wdl-dynamic-block-sample-block',
  //コールバック関数を指定
  'render_callback' => 'wdl_dynamic_block_sample_render',
) );
PHP でレンダリングする関数

render_callback に指定したコールバック関数(PHP でレンダリングする関数)を定義します。この関数は2つのパラメータを取ります。

  • $attr:registerBlockType で定義した attributes(属性)。PHP 側でも設定可能(後述)。
  • $content:ネストされたブロック(ネストされたブロックを使用する場合。省略可能)

また、この関数はブロックの出力を return する必要があるので、文字列(HTML)を作成して最後に return します。

function wdl_dynamic_block_sample_render($attr, $content) {
  // ブロックの出力を return
}

$attr(第1パラメータ)

第1パラメータの $attr には、保存された属性のみが渡されます。属性が実際に保存されなかった場合、JavaScript 側でデフォルトを設定していてもその属性は PHP 関数に含まれません。

この例の場合、registerBlockType(JavaScript 側)では以下のような attributes が定義されています。

src/index.js からの抜粋(JavaScript 側)
attributes: {
  myTitle: {
    type: 'string',
    default: '',  //デフォルト値に空文字
    selector:'.myRichTextTitle' 
  },
  myContent: {
    type: 'string',
    default: '',
    selector:'.myRichTextContent' 
  },
},

属性にアクセスするにはパラメータの $attr を使います。属性の myTitle の値は以下で取得できます。

 $attr['myTitle']; 

以下のようにコールバック関数を定義した場合、

function wdl_dynamic_block_sample_render($attr, $content) {
  return $attr['myTitle'];
}

ブロックに myTitle が入力されて保存されていれば、その値が渡されて出力しますが、myTitle に何も入力されていない場合、$attr['myTitle'] は未定義になり、Notice: Undefined index: myTitle のようなエラーが表示されてしまいます。

この問題に対処するには、常に if (isset($attr['...'])) で属性が設定(保存)されているかを確認するか、または、PHP 側の register_block_type に attributes を設定し、default を指定します(次項)。

PHP 側で attributes キーの追加

register_block_type 関数の第2パラメータの配列に attributes キーを追加して、ブロックから抽出したいすべての属性を設定することができます。register_block_type に attributes を設定する場合は、オブジェクトではなく連想配列で設定する必要があります。

また、PHP 側で設定した attributes は JavaScript 側では不要になります(JavaScript 側の attributes はそのまま残しておいても問題ないようです)。edit 関数では PHP 側で設定した attributes に props 経由でアクセスできます。

[訂正・追記]この例の場合は、JavaScript 側で設定した attributes は削除しても問題ありませんが、例えばインスペクターなどの初期値は初期状態では JavaScript 側で設定した attributes の値が反映される場合があるようなので(?)、そのような場合は残す必要があります。

register_block_type( 'wdl/dynamic-block-sample', array(
  'editor_script' => 'wdl-dynamic-block-sample-block-editor',
  'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
  'style'         => 'wdl-dynamic-block-sample-block',
  'render_callback' => 'wdl_dynamic_block_sample_render', //コールバック関数の指定
  //属性を設定(連想配列で指定)
  'attributes' => [
    'myTitle' => [
      'type' => 'string',
      'default' => ''
    ],
    'myContent' => [
      'type' => 'string',
      'default' => ''
    ],
  ]
) );

このようにすれば、例えば前述の myTitle に何も入力されていない場合でもデフォルト(default)が設定されているので未定義にはならず Notice エラーも表示されません。

※ 但し、default が設定されていない場合は、isset($attr['...']) でチェックする必要があります。

ブロックの出力(Output Buffer を使う例)

ブロックの出力では文字列を return する必要があるので、sprintfob_start (output buffer)などを利用します。この例では edit 関数での出力に対応するように以下のようなブロックを出力します。

render_callback に指定した PHP でレンダリングする関数では props の className を取得する方法がないようなのでクラス名はハードコーディングしています。attributes に設定しておくこともできます。

function wdl_dynamic_block_sample_render($attr, $content) {
  ob_start(); // バッファリングを開始
?>
<div class="wp-block-wdl-dynamic-block-sample">
  <h3 class="myRichTextTitle"><?php echo $attr['myTitle']; ?></h3>
  <div class="myRichTextContent"><?php echo $attr['myContent']; ?></div>
</div>
<?php
  $output = ob_get_contents(); // バッファの内容(4〜7行目)を $output に取得
  ob_end_clean();  // バッファをクリアしてバッファ制御をオフに
  return $output;  //バッファの内容を出力
}

PHP 側の出力を変更しても妥当性検証プロセスは行われないので、マークアップを変更する際に発生するブロックを一度削除してから再度ブロックを挿入しなければならない問題を回避できます。

エディター画面で以下のように入力すると

フロントエンド側では以下のように表示されます。

以下は output buffer を使って設定されている属性($attr の内容)を var_dump() で出力する例です。

function wdl_dynamic_block_sample_render($attr, $content) {
  ob_start(); // バッファリングを開始
?>
<pre>
<?php var_dump($attr); ?>
</pre>
<?php
  $output = ob_get_contents(); // バッファの内容を $output に取得
  ob_end_clean(); // バッファをクリアしてバッファ制御をオフに
  return $output; //バッファの内容(5〜7行目)を出力
}
dynamic-block-sample.php
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  //wp_set_script_translations( 'wdl-dynamic-block-sample-block-editor', 'dynamic-block-sample' );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback に指定したコールバック関数 
function wdl_dynamic_block_sample_render($attr, $content) {
  ob_start(); 
?>
<div class="wp-block-wdl-dynamic-block-sample">
  <h3 class="myRichTextTitle"><?php echo $attr['myTitle']; ?></h3>
  <div class="myRichTextContent"><?php echo $attr['myContent']; ?></div>
</div>
<?php
  $output = ob_get_contents(); 
  ob_end_clean(); 
  return $output; 
}
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //入力された値を保存するための属性(PHP 側で設定したのでコメントアウト)
  /*attributes: {
    //タイトル(h3)の内容を保持するための属性
    myTitle: {
      type: 'string',
      default: '',
      //source: 'html',
      selector:'.myRichTextTitle' 
    },
    //コンテンツ(div)の内容を保持するためのの属性
    myContent: {
      //type: 'array',
      type: 'string',
      default: '',
      //source: 'children',
      selector:'.myRichTextContent' 
    },
  },*/
  edit: Edit,
  save: () => { return null }
} );
src/edit.js
import './editor.scss';
import { RichText } from '@wordpress/block-editor';

export default function Edit( props ) {
  
  const { attributes: { myTitle, myContent}, className, setAttributes } = props;
  
  return (
    <div className={ className }  >
      <RichText 
        className='myRichTextTitle'
        value={ myTitle }
        onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
        tagName='h3'
        placeholder= 'タイトルを入力' 
        keepPlaceholderOnFocus={true}
      />
      <RichText
        className='myRichTextContent'
        value={ myContent }
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
        tagName='div'
        multiline= 'p'
        placeholder= '文章を入力' 
      />
    </div>
  );
}

ServerSideRender

ServerSideRender コンポーネントを使うと、PHP の render_callback 関数に基づいたレンダリングをエディター画面で確認(プレビュー)することができます。

ServerSideRender コンポーネントは @wordpress/editor パッケージからインポートするか直接 @wordpress/server-side-render からインポートします。

ServerSideRender コンポーネントでは、ブロックの名前(識別子)を block プロパティに指定する必要がありますが、これは props.name で取得できます(必須)。

※ また、表示する内容(PHP でのレンダリング)に対応する属性を attributes プロパティに指定する必要があります。

src/edit.js
import './editor.scss';
import { RichText } from '@wordpress/block-editor';
//ServerSideRender コンポーネントを editor パッケージからインポート
import { ServerSideRender } from '@wordpress/editor';
// または import ServerSideRender from '@wordpress/server-side-render';

export default function Edit( props ) {
  
  const { attributes: { myTitle, myContent}, className, setAttributes } = props;
  
  return (
    <div className={ className }  >

    ・・・中略・・・
    
      <ServerSideRender
        // ブロックの識別子 'wdl/dynamic-block-sample'
        block={props.name} 
        // PHP のレンダリングで使用している属性を指定(指定しないと表示されない)
        attributes={{  
          myTitle: myTitle,  //右辺は props.attributes.myTitle
          myContent: myContent,  //右辺は props.attributes.myContent
        }}
        className='my-custom-ssr'
      />

    </div>
  );
}

上記の場合、attributes プロパティには props 経由で分割代入を使って変数に代入した属性(例 props.attributes.myTitle)を指定しています。

この場合、オブジェクトのプロパティの短縮構文を使えば以下のように記述することもできます。

<ServerSideRender
  block={props.name} //'wdl/dynamic-block-sample'
  attributes={{  
    myTitle,  //短縮構文
    myContent,  //短縮構文
  }}
  className='my-custom-ssr'
/>
ServerSideRender コンポーネントのプロパティ(一部)
プロパティ 説明
attributes サーバー(PHP)側でレンダリングする際に使用する属性を指定します。ServerSideRender の props として渡したい属性をこのプロパティで設定する必要があります。
block(必須) サーバー側でレンダリングされるブロックの識別子(namespace/name)。props.name で取得できます。
className サーバー側のレンダリングされたブロックをラップする DOM 要素に追加されるクラス。

この例では、サーバー側のレンダリングの部分がわかりやすいように間に h3 要素を入れています。また、editor.scss にスタイルを指定して見栄えを変更しています。

src/edit.js
import './editor.scss';
import { RichText } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';

export default function Edit( props ) {
  const { attributes: { myTitle, myContent}, className, setAttributes } = props;
  return (
    <div className={ className }  >
      <RichText 
        className='myRichTextTitle'
        value={ myTitle }
        onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
        tagName='h3'
        placeholder= 'タイトルを入力' 
        keepPlaceholderOnFocus={true}
      />
      <RichText
        className='myRichTextContent'
        value={ myContent }
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
        tagName='div'
        multiline= 'p'
        placeholder= '文章を入力' 
      />
      <h3 className='server-side-render-title'>Server Side Render(プレビュー)</h3>
      <ServerSideRender
        block={props.name} 
        attributes={{  
          myTitle: myTitle,  
          myContent: myContent, 
        }}
        className='my-custom-ssr'
      />
    </div>
  );
}
src/editor.scss
.wp-block-wdl-dynamic-block-sample h3.server-side-render-title {
  background: #eee;
  color: #666;
  padding: 20px;
  font-size: 24px;
  margin: 0;
}

/* className プロパティで指定したクラスを利用 */
.my-custom-ssr .wp-block-wdl-dynamic-block-sample{
  background: #F5F9C5;
  border: 1px solid #999;
  color: #428509;
}

上記の場合、以下のように表示されます。

プレビューボタンの追加

ツールバーにプレビューボタンを追加して、サーバー側のレンダリングを表示するプレビューモードと通常の編集画面の編集モードを切り替えられるようにする例です。

現在のモードの状態を表す属性 isEditMode を register_block_type の attributes に追加します。

dynamic-block-sample.php 抜粋
register_block_type( 'wdl/dynamic-block-sample', array(
  'editor_script' => 'wdl-dynamic-block-sample-block-editor',
  'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
  'style'         => 'wdl-dynamic-block-sample-block',
  'render_callback' => 'wdl_dynamic_block_sample_render',
  'attributes' => [
    'myTitle' => [
      'type' => 'string',
      'default' => ''
    ],
    'myContent' => [
      'type' => 'string',
      'default' => ''
    ],
    //isEditMode を属性として追加
    'isEditMode' => [
      'type' => 'boolean',
      'default' => true
    ],
  ]
) );
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  //wp_set_script_translations( 'wdl-dynamic-block-sample-block-editor', 'dynamic-block-sample' );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      //isEditMode を属性として追加
      'isEditMode' => [
        'type' => 'boolean',
        'default' => true
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback に指定したコールバック関数 
function wdl_dynamic_block_sample_render($attr, $content) {
  ob_start(); 
?>
<div class="wp-block-wdl-dynamic-block-sample">
  <h3 class="myRichTextTitle"><?php echo $attr['myTitle']; ?></h3>
  <div class="myRichTextContent"><?php echo $attr['myContent']; ?></div>
</div>
<?php
  $output = ob_get_contents(); 
  ob_end_clean(); 
  return $output; 
}


ツールバーにプレビューボタンを追加するため、BlockControls、Button、Toolbar コンポーネントをインポートします。また、通常の編集画面を出力する際に2つの RichText コンポーネントをまとめるための Fragment もインポートします。

getBlockControls はツールバーにプレビューボタンを追加して表示する関数です。プレビューボタンに表示するアイコンやラベルは、isEditMode の値により切り替わるように label と icon プロパティで設定しています。

現在のモードの状態を表す属性 isEditMode の true/false の切り替えは、ボタンがクリックされたらプレビューボタンの onClick のコールバックで、setAttributes を使って値を反転させます。

そして、 isEditMode が true の場合(編集 モード)は通常の出力をし、isEditMode が false の場合(プレビュー モード)は ServerSideRender コンポーネントを使って PHP のレンダリングをエディター画面でプレビューするようにしています。

関連項目:ツールバー

src/edit.js
import './editor.scss';
//BlockControls を追加でインポート
import { RichText, BlockControls } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
//Button、Toolbar を追加でインポート
import { Button, Toolbar } from '@wordpress/components';
//Fragment を追加でインポート
import { Fragment } from '@wordpress/element';

export default function Edit( props ) {
  //属性 isEditMode を attributes から取得
  const { attributes: { myTitle, myContent, isEditMode }, className, setAttributes } = props;
  
  //ツールバーにプレビューボタンを追加する関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  
  return (
    [
      getBlockControls(), //プレビューボタン
      <div className={ className }  >
        { isEditMode &&  // isEditMode が true の場合(編集 モード)
          <Fragment>    
            <RichText 
              className='myRichTextTitle'
              value={ myTitle }
              onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
              tagName='h3'
              placeholder= 'タイトルを入力' 
              keepPlaceholderOnFocus={true}
            />
            <RichText
              className='myRichTextContent'
              value={ myContent }
              onChange={ (newContent) => setAttributes({ myContent: newContent }) }
              tagName='div'
              multiline= 'p'
              placeholder= '文章を入力' 
            />
          </Fragment>
        }
        { !isEditMode &&   // isEditMode が false の場合(プレビュー モード)
          <ServerSideRender
            block={props.name} 
            attributes={{  
              myTitle: myTitle,  
              myContent: myContent, 
            }}
            className='my-custom-ssr'
          />
        }
      </div>
    ]
  );
}
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //attributes は PHP 側(dynamic-block-sample.php)で設定
  edit: Edit,
  save: () => { return null }
} );

投稿を表示

PHP のレンダリング関数(render_callback)の出力は何でもかまいません。また、すべての WordPress 関数にアクセスすることができます。

例えば、投稿 ID を属性に保存して render_callback の PHP 関数で、get_post() を使って ID から投稿を取得してその情報を出力することができます。

以下は前述の例を使って投稿 ID を入力するとその投稿のリンク付きのタイトルを出力する例です。投稿 ID を入力しなければならないので実用的ではありませんが、通常のリンクとは異なり、投稿側でタイトルを変更すると自動的に表示されるタイトルも更新されます。

まずは入力された投稿 ID を保存する属性 selectedPostId を attributes に設定します。

dynamic-block-sample.php 抜粋
register_block_type( 'wdl/dynamic-block-sample', array(
  'editor_script' => 'wdl-dynamic-block-sample-block-editor',
  'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
  'style'         => 'wdl-dynamic-block-sample-block',
  //コールバック関数を指定
  'render_callback' => 'wdl_dynamic_block_sample_render',
  //属性を設定
  'attributes' => [
    'myTitle' => [
      'type' => 'string',
      'default' => ''
    ],
    'myContent' => [
      'type' => 'string',
      'default' => ''
    ],
    'isEditMode' => [
      'type' => 'boolean',
      'default' => true
    ],
    // selectedPostId を属性として追加
    'selectedPostId' => [
      'type' => 'number',
      'default' => 0
    ],
  ]
) );

edit 関数に投稿 ID を入力する入力欄を TextControl コンポーネントを使って追加します。

TextControl コンポーネントは @wordpress/components からインポートします。また、属性 selectedPostId を props 経由で取得します。

TextControl の onChange プロパティでは入力した値(newId)を念の為 parseInt() で整数(attributes の type で指定した number 型)に変換してから setAttributes で属性に設定しています。

ServerSideRender コンポーネントの attributes プロパティに selectedPostId を追加します。

src/edit.js
import './editor.scss';
import { RichText, BlockControls } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
//TextControl を追加でインポート
import { TextControl, Button, Toolbar } from '@wordpress/components';
import { Fragment } from '@wordpress/element';

export default function Edit( props ) {

  //属性 selectedPostId を props 経由で attributes から取得
  const { attributes: { selectedPostId, myTitle, myContent, isEditMode }, className, setAttributes } = props;
  
  const getBlockControls = () => {
    return (
      <BlockControls>
        ・・・中略・・・
      </BlockControls>
    );
  }
  
  return (
    [
      getBlockControls(),
      <div className={ className }  >
        { isEditMode &&  // isEditMode が true の場合(編集 モード)
          <Fragment>  
            <TextControl  // TextControl コンポーネントを追加
              label="投稿 ID を入力"
              type="number"
              value={selectedPostId}
              onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
            />   
            <RichText 
              ・・・中略・・・
            />
            <RichText
              ・・・中略・・・
            />
          </Fragment>
        }
        { !isEditMode &&   // isEditMode が false の場合(プレビュー モード)
          <ServerSideRender
            block={props.name} 
            // 属性を追加(設定しないと表示されない)
            attributes={{  
              myTitle: myTitle,  
              myContent: myContent, 
              selectedPostId: selectedPostId,  //追加
            }}
            className='my-custom-ssr'
          />
        }
      </div>
    ]
  );
}
import './editor.scss';
import { RichText, BlockControls } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
//TextControl を追加でインポート
import { TextControl, Button, Toolbar } from '@wordpress/components';
import { Fragment } from '@wordpress/element';

export default function Edit( props ) {
  //属性 selectedPostId を attributes から取得
  const { attributes: { selectedPostId, myTitle, myContent, isEditMode }, className, setAttributes } = props;
  
  //ツールバーにプレビューボタンを追加する関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  
  return (
    [
      getBlockControls(),
      <div className={ className }  >
        { isEditMode &&  // isEditMode が true の場合(編集 モード)
          <Fragment>  
            <TextControl  //
              label="投稿 ID を入力"
              type="number"
              value={selectedPostId}
              onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
            />     
            <RichText 
              className='myRichTextTitle'
              value={ myTitle }
              onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
              tagName='h3'
              placeholder= 'タイトルを入力' 
              keepPlaceholderOnFocus={true}
            />
            <RichText
              className='myRichTextContent'
              value={ myContent }
              onChange={ (newContent) => setAttributes({ myContent: newContent }) }
              tagName='div'
              multiline= 'p'
              placeholder= '文章を入力' 
            />
          </Fragment>
        }
        { !isEditMode &&   // isEditMode が false の場合(プレビュー モード)
          <ServerSideRender
            block={props.name} 
            // PHP のレンダリングで使用している属性を指定(設定しないと表示されない)
            attributes={{  
              myTitle: myTitle,  
              myContent: myContent, 
              selectedPostId: selectedPostId,  //追加
            }}
            className='my-custom-ssr'
          />
        }
      </div>
    ]
  );
}

PHP でレンダリング

TextControl コンポーネントに入力された投稿 ID は属性に保存されるので render_callback 関数では $attr['selectedPostId'] でアクセスできます。

get_post() で投稿 ID で指定して投稿を取得し、マークアップを作成して出力します。

この例では、RichText に入力された値と共に出力しています。また、render_callback 関数の第2パラメータ($content)は使わないので省略しています。

function wdl_dynamic_block_sample_render($attr) {
  $str = ''; //マークアップの文字列を保存する変数
  if ($attr['selectedPostId'] > 0) {
    // 投稿 ID から投稿を取得
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $str = '';  //投稿が取得できない場合
    } else {
      $str = '<div class="fethed_post">';
      $str .= '<a href="' . esc_url( get_permalink($my_post) ). '">';
      $str .= '<h3>' . esc_html( get_the_title($my_post) ) . '</h3>';
      $str .= '</a>';
      $str .= '</div>';
    }
  }
  ob_start(); 
?>
<div class="wp-block-wdl-dynamic-block-sample">
  <h3 class="myRichTextTitle"><?php echo $attr['myTitle']; ?></h3>
  <div class="myRichTextContent"><?php echo $attr['myContent']; ?></div>
  <?php echo $str; //投稿のタイトルのマークアップ ?> 
</div>
<?php
  $output = ob_get_contents(); 
  ob_end_clean(); 
  return $output; 
}
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );
  //wp_set_script_translations( 'wdl-dynamic-block-sample-block-editor', 'dynamic-block-sample' );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      'isEditMode' => [
        'type' => 'boolean',
        'default' => true
      ],
      // selectedPostId を属性として追加
      'selectedPostId' => [
        'type' => 'number',
        'default' => 0
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback のコールバック関数(PHP でレンダリングする関数)
function wdl_dynamic_block_sample_render($attr) {
  $str = ''; //マークアップの文字列を保存する変数
  if ($attr['selectedPostId'] > 0) {
    // 投稿 ID から投稿を取得
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $str = '';  //投稿が取得できない場合
    } else {
      $str = '<div class="fethed_post">';
      $str .= '<a href="' .esc_url( get_permalink($my_post) ) . '">';
      $str .= '<h3>' . esc_html( get_the_title($my_post) ). '</h3>';
      $str .= '</a>';
      $str .= '</div>';
    }
  }
  ob_start(); 
?>
<div class="wp-block-wdl-dynamic-block-sample">
  <h3 class="myRichTextTitle"><?php echo $attr['myTitle']; ?></h3>
  <div class="myRichTextContent"><?php echo $attr['myContent']; ?></div>
  <?php echo $str; ?> 
</div>
<?php
  $output = ob_get_contents(); 
  ob_end_clean(); 
  return $output; 
}

以下の表示例では ServerSideRender コンポーネントでのレンダリングを区別するために CSS で背景色などを適当に指定しています。

withSelect を使って投稿を取得

ブロックの edit 関数から投稿をクエリ(取得)するにはいろいろな方法があるようです。以下のサイトを参考にさせていただきました。間違って理解している可能性もあります。

以下は上記の参考にさせていただいたサイトからの抜粋です。

WordPress データモジュール(Data module)はさまざまなストア(stores)のハブであり、さまざまなモジュール間でデータを管理するための機能を提供し、Redux の上に構築されています。

ストアのデータにアクセスするには、セレクター(selectors)を使用する必要があります。 WordPress には wp.data パッケージ内にセレクター select() があります。

いくつかのコンポーネントで WordPress REST API からデータをフェッチする必要があるブロックに取り組んできました。最初のコンポーネントはエディター内およびポストピッカーコンポーネントのモーダル内に投稿のリストを表示する役割を果たします。 2つ目は各投稿に割り当てられたカテゴリを取得して各投稿のカテゴリ名を表示できるようにするためのものでした。

ありがたいことに、WordPress の Core Data API を使用すると、withSelect 高階コンポーネントを使用してこのデータをフェッチし、props としてコンポーネントに渡すことができます。 getEntityRecords 関数を使用すると、特定の REST API エンドポイントにデータをフェッチして、コンポーネント内で使用するデータを取得できます。

WordPress REST API とは?

WordPress REST API はデータベースから情報を取得し、JSON 形式で提供します。これには、投稿、メディア、ページ、ユーザーなどが含まれます。

最新の投稿のブロックまたは他のブロックでデータベースから情報にアクセスする場合、wp.data パッケージからアクセスできる便利な withSelect を使用して REST API を利用できます。

edit 関数で投稿を取得するには、WordPress の高階コンポーネントである withSelect を利用すれば良さそうです。

以下は React の「高階 (Higher-Order) コンポーネント」からの抜粋です。

高階コンポーネント (higher-order component; HOC) はコンポーネントのロジックを再利用するための React における応用テクニックです。HOC それ自体は React の API の一部ではありません。HOC は、React のコンポジションの性質から生まれる設計パターンです。

具体的には、高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

Selector

ブラウザの JavaScript コンソールを使用して、WordPress Data API を調べることができます。

投稿の編集ページでブラウザの開発ツール(JavaScript console/debugger tool)を開いて以下を入力して return キーを押すと、core data API にアクセスします。

wp.data.select('core') 

応答として、使用可能な全てのセレクター(関数)を含むオブジェクトを取得できます。

getAuthors や getComments など多数ありますが、投稿を取得するのに使用するセレクターの関数は getEntityRecords です。

続いて wp.data.select('core').getEntityRecords() と入力して return キーを押すと、getEntityRecords() にパラメータを指定していないため空の配列が返されます。

全ての投稿を取得するには以下をコンソールに入力して return キーを押します。

wp.data.select('core').getEntityRecords('postType', 'post', { per_page: -1 }) 

実行して null が返った場合は、もう一度実行します。この例の場合、 per_page: -1 を指定しているので、全ての投稿のオブジェクトの配列が返されます。

各投稿を展開すると、その投稿の id や link 、title などのプロパティを確認することができます。

core/block-editor

以下を入力して return キーを押すと、core/block-editor にアクセスして getBlocks() を使って現在投稿にあるすべてのブロックに関する情報が返されます。 

wp.data.select('core/block-editor').getBlocks()

以下は2つのブロックを使っている投稿の例です。

core/editor

以下を入力して return キーを押すと、core/editor にアクセスして getCurrentPostId() を使って現在の投稿の ID が返されます。 

wp.data.select('core/editor').getCurrentPostId()

withSelect

withSelect は、登録されたセレクターである select() を使用して、フェッチしたデータを props としてコンポーネントに渡すことがきる高階コンポーネント(Higher Order Component) です。

言い換えると、withSelect 内では select() にアクセスでき、それを使用して呼び出しを実行できます。 select() の結果は withSelect に渡すコンポーネントへの props になり、withSelect の戻り値はその props がマージされた拡張されたコンポーネントになります。

withSelect を使うには、@wordpress/data から withSelectselect をインポートします。

import { withSelect, select } from '@wordpress/data';

ブロックの edit 関数で withSelect を使い、Edit コンポーネント(この例の場合)を渡します。

withSelect 内で、select をパラメーターとして分割代入し、select('core') で core data API にアクセスして getEntityRecords を使って投稿を取得します。そして取得した投稿をプロパティとする posts というオブジェクトを返します(オブジェクトのキー posts は任意の名前)。

getEntityRecords の詳細

src/index.js の registerBlockType() 一部抜粋
registerBlockType( 'wdl/dynamic-block-sample', {
  ・・・中略・・・
  // edit 関数
  edit: withSelect( select => {
    return {
      //posts という select() の呼び出し結果をプロパティとするオブジェクトを返す
      posts: select('core').getEntityRecords('postType', 'post', { per_page: 3 })
    };
  })(Edit), //Edit コンポーネント(edit 関数用のコンポーネント)を渡します
  save: () => { return null }
} );

上記 edit: 部分は以下のように記述することもできます。ownProps は使用していないので省略できます。

edit: withSelect( ( select, ownProps ) => {
  const { getEntityRecords } = select( 'core' );
  return {
    posts: getEntityRecords('postType', 'post', { per_page: 3 })
  };
})(Edit),

これにより Edit コンポーネントは posts という props を持ちます。withSelect 内で return するものは渡されたコンポーネント(この例では Edit)で props としてアクセスできます。

withSelect の書式

withSelect の書式は以下のような感じになるのかと思います。

const EnhancedMyComponent = withSelect( ( select, ownProps ) => {
	 
   /*** select( ) を使って data API にアクセスして 
    props にマージされるオブジェクトを取得する処理 ***/

  return {
    props にマージされる取得したオブジェクトを返す
  };
} )( MyComponent);

withSelect 内に記述する関数は状態(state)が変化するたびに呼び出される関数で、上記の場合 MyComponent に渡すオブジェクトを返します。

戻り値(EnhancedMyComponent)は withSelect 内で取得したオブジェクトがマージされた props を持つ(MyComponent が)拡張されたコンポーネントになります。

Data/withSelect

Edit コンポーネント

withSelect に渡された Edit コンポーネントには edit 関数の内容(編集画面でのブロックのレンダリング)を記述します。この例の場合は今までの例と同じ RichText を使ったタイトルと文章を入力するエリアと withSelect で取得した投稿のタイトルのリストをレンダリングします。

このコンポーネントでは props 経由で withSelect 内で取得した投稿のオブジェクト(この例では posts)を受け取ることができます。以下のように取得した props をコンソールに出力すると、2つのログが出力されます。

最初のログは null が出力され、続いて取得した投稿の配列が出力されます。

これは投稿のクエリが非同期で行われるためです。コンポーネントは最初に応答前にレンダリングされ、その時点で props.posts は null で、レスポンスを受け取るとコンポーネントが再レンダリングされます。

edit.js
export default function Edit( props ) {
  const { posts, className, setAttributes, attributes: { myTitle, myContent } } 
  = props; // 投稿 posts を props 経由で取得

  console.log(posts); //コンソールに出力(null に続いて投稿の配列が出力される)
  ・・・
log の例
null  //最初のログ
(3) [{…}, {…}, {…}]  //次のログ(投稿の配列。内容は以下の3つの投稿のオブジェクト)
  0: {id: 1353, date: "2020-09-27T17:17:36", date_gmt: "...", guid: {…}, …}
  1: {id: 1328, date: "2020-09-27T11:45:14", date_gmt: "...", guid: {…}, …}
  2: {id: 1307, date: "2020-09-26T10:20:13", date_gmt: "...", guid: {…}, …}
  length: 3
  __proto__: Array(0)

ブラウザの開発ツールでコンソールに出力された投稿のオブジェクトを展開するとどのようなプロパティがあるかを確認できます。

0:
author: 1
categories: [1]
comment_status: "open"
content: {raw: "<!-- wp:wdl/dynamic-block-sample {"myTitle":"サンプルタ…003cp\u003eサンプルテキスト abcdefgh\u003c/p\u003e"} /-->", rendered: "<div class="wp-block-wdl-dynamic-block-sample my-d…le-block-09/">Sample Block 09</a></li></ul></div>", protected: false, block_version: 1}
date: "2020-09-27T17:17:36"
date_gmt: "2020-09-27T08:17:36"
excerpt: {raw: "", rendered: "", protected: false}
featured_media: 0
format: "standard"
generated_slug: "sample-01"
guid: {rendered: "http://localhost/blocks/?p=1353", raw: "http://localhost/blocks/?p=1353"}
id: 1353
link: "http://localhost/blocks/2020/09/27/%e3%83%80%e3%82%a4%e3%83%8a%e3%83%9f%e3%83%83%e3%82%af%e3%83%96%e3%83%ad%e3%83%83%e3%82%af-01/"
meta: []
modified: "2020-09-30T13:54:29"
modified_gmt: "2020-09-30T04:54:29"
password: ""
permalink_template: "http://localhost/blocks/2020/09/27/%postname%/"
ping_status: "open"
slug: "%e3%83%80%e3%82%a4%e3%83%8a%e3%83%9f%e3%83%83%e3%82%af%e3%83%96%e3%83%ad%e3%83%83%e3%82%af-01"
status: "publish"
sticky: false
tags: []
template: ""
title: // title には raw と rendered がある
  raw: "Sample 01"
  rendered: "Sample 01"
__proto__: Object
type: "post"
_links: {self: Array(1), collection: Array(1), about: Array(1), author: Array(1), replies: Array(1), …}
__proto__: Object

非同期で行われる getEntityRecords は値がまだ受信されていない(投稿が読み込まれていない)場合は null を返すので、読み込みの状態により表示する内容を分岐します。

  1. !posts :投稿の読み込みが完了していない場合は null を返すので、スピナーと「投稿を読込中です」と表示(値が存在しない場合は undefined を返す)
  2. 0 === posts.length:投稿の読み込みが完了したが、投稿数が0の場合は「表示する投稿がありません」というメッセージを表示
  3. 上記以外:投稿の読み込みが完了して投稿が1つ以上あれば、投稿のリストを表示

投稿のリストは ul li 要素(JSX)で記述しています(ul 要素は return ステートメントの中に記述)。

posts は投稿のオブジェクトの配列なので、map() メソッドを使ってそれぞれの投稿のオブジェクトのプロパティ(link と title.rendered)を使ってタイトルのリンクを li 要素として生成しています。

  • 投稿のリンクの URL: post.link
  • 投稿のタイトル: post.title.rendered

return ステートメントの中ではレンダリングする内容として RichText のタイトルとコンテンツ及び取得した投稿のリストを記述します。

※ post_content に格納される値は HTML(文字列)ではなく JSX で記述した React 要素です。

export default function Edit( props ) {
  const { posts, className, setAttributes, attributes: { myTitle, myContent } } 
  = props;
  
  //投稿部分のレンダリング(格納される値は JSX で記述した React 要素)
  let post_content = [];
  
  if (!posts) {
    //読み込みが完了していない場合
    post_content = <li><Spinner /> 投稿を読込中です・・・。</li>;
  }else{
    if (0 === posts.length) {
      //読み込みが完了して投稿数が0の場合
      post_content = <li>表示する投稿がありません。</li>;
    }else{
      //読み込みが完了して投稿がある場合
      post_content = 
      posts.map(post => {
        return (
          <li><a href={ post.link }> { post.title.rendered } </a></li>
        );
      })
    }
  }
  
  return (
    <div className={ className }  >
      <RichText 
        ・・・中略・・
      />
      <RichText
        ・・・中略・・
      />
      <ul>
        { post_content }
      </ul>
    </div>
  );
}
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
//withSelect, select をインポート
import { withSelect, select } from '@wordpress/data';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //withSelect を使って投稿を取得(投稿 posts を props で Edit に渡す)
  edit: withSelect(select => {
    //現在の投稿の post ID を取得
    const currentPostId = select('core/editor').getCurrentPostId();
    //クエリパラメータ
    const query = {
      per_page: 3,
      exclude: currentPostId //除外する投稿
    }
    return {
      posts: select('core').getEntityRecords('postType', 'post', query)
    }
  })(Edit),
  save: () => { return null }
} );
src/edit.js
import './editor.scss';
import { RichText } from '@wordpress/block-editor';
// Spinner をインポート
import { Spinner } from '@wordpress/components';

export default function Edit( props ) {
  const { posts, className, setAttributes, attributes: { myTitle, myContent } } 
  = props;
  
  //投稿部分のレンダリング
  let post_content = [];
  
  if (!posts) {
    //読み込みが完了していない場合
    post_content = <li><Spinner /> 投稿を読込中です・・・。</li>;
  }else{
    if (0 === posts.length) {
      //読み込みが完了して投稿数が0の場合
      post_content = <li>表示する投稿がありません。</li>;
    }else{
      //読み込みが完了して投稿がある場合
      post_content = 
      posts.map(post => {
        return (
          <li><a href={ post.link }> { post.title.rendered } </a></li>
        );
      })
    }
  }
  
  return (
    <div className={ className }  >
      <RichText 
        className='myRichTextTitle'
        value={ myTitle }
        onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
        tagName='h3'
        placeholder= 'タイトルを入力' 
        keepPlaceholderOnFocus={true}
      />
      <RichText
        className='myRichTextContent'
        value={ myContent }
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
        tagName='div'
        multiline= 'p'
        placeholder= '文章を入力' 
      />
      <ul>
        { post_content }
      </ul>
    </div>
  );
}

getEntityRecords

投稿を取得するのに使用するセレクターの関数 getEntityRecords は値がまだ受信されていない場合は null を返し、値が存在しない場合は undefined を、値が存在して受信されている場合はエンティティオブジェクトを返します。

また、以下のようなパラメータがあるようです。

  • state Object: State tree
  • kind string: Entity kind.
  • name string: Entity name.
  • key number: Record's key
  • query ?Object: Optional query.

前述の例では以下のようなパラメータを指定しました。

 getEntityRecords('postType', 'post', { per_page: 3 })

この場合、パラメータの Entity kind に postType、Entity name に投稿を意味する post を指定しています。特定の結果を取得するために、post を REST API で公開された他の投稿タイプまたはカスタム投稿タイプに置き換えることができます。

以下はカテゴリー ID が5の添付ファイルを取得する例です。

getEntityRecords('postType', 'attachment', { categories: 5 })

以下は全てのカテゴリーを取得する例です。

getEntityRecords('taxonomy', 'category', {per_page: -1})    

前述の例では Optional query にオブジェクト { per_page: 3 } を渡して、結果のセットの最大値として 3 を指定して3つの投稿を取得するようにしています。エンドポイントに対応する REST API パラメーターを使用して取得するデータをフィルターできます。

以下のようなクエリのパラメータが使えるようです(一部)。

パラメータ 説明
per_page 取得する投稿数を制限する数値。デフォルトは 10。-1を設定すると全ての投稿を取得(最大100?)
exclude 除外する投稿の post ID を指定(複数ある場合は配列で指定)
order 昇順(asc)または降順(desc)を指定
status 投稿のステータスを指定。例(公開済みと下書きの両方):['publish', 'draft']
categories カテゴリーでフィルター。category ID またはその配列を指定。
tags タグでフィルター。tag ID またはその配列を指定。
search 検索クエリ(文字列)を追加します。

投稿をクエリするパラメータは REST API Handbook などで確認できます。

現在の投稿を除外 getCurrentPostId

取得する投稿から現在の投稿を除外するには現在の投稿の post ID を exclude に指定します。

現在の投稿の post ID の取得は core/editor の getCurrentPostId() を使います。withSelect の中では select() を使って WordPress の core data にアクセスできます。

以下は現在の投稿を除外して全ての投稿を取得する例です。

edit: withSelect(select => {
          
  //select() で core data にアクセスして現在の投稿の post ID を取得
  const currentPostId = select('core/editor').getCurrentPostId();
  
  //クエリオブジェクトを作成
  const query = {
    per_page: -1,  //-1 を指定して全ての投稿を取得
    exclude: currentPostId  //現在の投稿を除外
  }
  
  return {
    posts: select('core').getEntityRecords('postType', 'post', query)
  }
})(Edit),

PHP でのレンダリング

render_callback に指定したコールバック関数(PHP でレンダリングする関数)では、属性の myTitle と myContent を使って入力されたタイトルとコンテンツの出力と withSelect を使って取得した投稿に対応する内容を出力するマークアップを返します(echo で出力はしません)。

また、この例では以下のような追加のクラス名のための属性 addClassName を追加で定義しています。

// addClassName(追加のクラス名)を属性として追加
'addClassName' => [
  'type' => 'string',
  'default' => 'my-dynamic-block-sample'
],

以下が render_callback のコールバック関数です。

4行目と6行目は属性の値が設定されていればその値を使ってタイトルとコンテンツのマークアップを変数に保存しています。

render_callback の関数では WordPress の関数が使えるので、投稿を wp_get_recent_posts() を使って取得しています。クエリパラメータでは exclude に現在の投稿 get_the_ID() を指定して除外し、numberposts に記事数 3 を指定しています。

取得した記事のマークアップを foreach() と sprintf() で組み立てて変数に保存しています。その際に、get_the_title() や get_permalink() の値はエスケープが必要です。

31〜34行目は属性 addClassName に追加のクラス名が指定されていれば、$class に指定したクラス名に追加しています。

37〜43行目では最終的なマークアップを sprintf() を使って組み立てて、46行目で組み立てたマークアップを返しています。

function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿をリストで表示するマークアップ(空文字で初期化)
  $list_item_markup = '';

 // wp_get_recent_posts で最新の記事3件を取得(現在の投稿は除外)
  $recent_posts = wp_get_recent_posts(
    array(
      'numberposts' => 3,
      'post_stats'  => 'publish',
      'exclude' => [get_the_ID()],
    )
  );
  
  // 各投稿のマークアップを作成
  foreach ( $recent_posts as $post ) {
    $post_id = $post['ID'];
    $title = get_the_title( $post_id );
    $list_item_markup .= sprintf(
      '<li><a href="%1$s">%2$s</a></li>',
      esc_url( get_permalink( $post_id ) ), //%1$s
      esc_html( $title ) //%2$s
    );
  }

  // クラス属性の値を作成(属性 addClassName が指定されていれば追加)
  $class = 'wp-block-wdl-dynamic-block-sample';
  if ( isset( $attr['addClassName'] ) ) {
    $class .= ' ' . $attr['addClassName'];
  }

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<ul>%4$s</ul></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    $list_item_markup
  );
  
  //ブロックの出力の文字列(HTML)を返す
  return $block_content;
}
get_post() を使う

前述の例では投稿記事のマークアップの組み立ては wp_get_recent_posts() を使いましたが、以下は代わりに get_post() を使う場合の例です。

クエリパラメータの指定は wp_get_recent_posts() と同じですが、foreach() での投稿 ID の指定は $post->ID のようにしています。

// get_posts で最新の記事3件を取得(現在の投稿は除外)
$args = array(
  'numberposts' => 3,
  'post_stats'  => 'publish',
  'exclude' => [get_the_ID()],
);
$my_posts = get_posts( $args ); 

// 各投稿のマークアップを作成
if (! empty($my_posts)) {
  foreach ($my_posts as $post) {
    $title = get_the_title( $post->ID );
    $list_item_markup .= sprintf(
      '<li><a href="%1$s">%2$s</a></li>',
      esc_url( get_permalink(  $post->ID  ) ),
      esc_html( $title )
    );
  }
}
WP_Query() を使う

以下は WP_Query() を使う例です。ループ終了後の処理として wp_reset_postdata() を実行しています。

// WP_Query で最新の記事3件を取得(現在の投稿は除外)
$args = array(
  'posts_per_page' => 3,
  'post_stats'  => 'publish',
  'post__not_in' => [get_the_ID()],
);
$my_query = new WP_Query( $args ); 

// 各投稿のマークアップを作成
if ( $my_query->have_posts() ) {
  while ( $my_query->have_posts() ) {
    $my_query->the_post();
    $title = get_the_title( get_the_ID() );
    $list_item_markup .= sprintf(
      '<li><a href="%1$s">%2$s</a></li>',
      esc_url( get_permalink(  get_the_ID()  ) ),
      esc_html( $title )
    );
  } 
}
wp_reset_postdata();  //グローバル変数 $post をリセット

以下は WP_Query() で取得した投稿を foreach() で処理してマークアップを組み立てる例です。

wp_get_recent_posts() や get_posts() とほぼ同じです。

// WP_Query で最新の記事3件を取得(現在の投稿は除外)
$args = array(
  'posts_per_page' => 3,
  'post_stats'  => 'publish',
   'post__not_in' => [get_the_ID()],
);
$my_query = new WP_Query( $args ); 

// 各投稿のマークアップを作成
if (! empty($my_query->posts)) {
  foreach ($my_query->posts as $post) {
    $title = get_the_title( $post->ID );
    $list_item_markup .= sprintf(
      '<li><a href="%1$s">%2$s</a></li>',
      esc_url( get_permalink(  $post->ID  ) ),
      esc_html( $title )
    );
  }
}
dynamic-block-sample.php
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      // addClassName(追加のクラス名)を属性として追加
      'addClassName' => [
        'type' => 'string',
        'default' => 'my-dynamic-block-sample'
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback のコールバック関数(PHP でレンダリングする関数)
function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿をリストで表示するマークアップ(空文字で初期化)
  $list_item_markup = '';
  
  // get_posts で最新の記事3件を取得(現在の投稿は除外)
  $args = array(
    'numberposts' => 3,
    'post_stats'  => 'publish',
    'exclude' => [get_the_ID()],
  );
  $my_posts = get_posts( $args ); 
  
  // 各投稿のマークアップを作成
  if (! empty($my_posts)) {
    foreach ($my_posts as $post) {
      $title = get_the_title( $post->ID );
      $list_item_markup .= sprintf(
        '<li><a href="%1$s">%2$s</a></li>',
        esc_url( get_permalink(  $post->ID  ) ),
        esc_html( $title )
      );
    }
  }

  // クラス属性の値を作成(属性 addClassName が指定されていれば追加)
  $class = 'wp-block-wdl-dynamic-block-sample';
  if ( isset( $attr['addClassName'] ) ) {
    $class .= ' ' . $attr['addClassName'];
  }

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<ul>%4$s</ul></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    $list_item_markup
  );
  
  //ブロックの出力の文字列を返す
  return $block_content;
}

投稿のリストから選択

投稿を表示」の例では TextControl コンポーネントでレンダリングした input 要素に入力された投稿 ID を元に投稿を表示しましたが、使い勝手がよくありません。

以下の例ではインスペクター(設定サイドバー)に投稿のリストを表示し、選択された投稿のタイトルと抜粋を投稿に挿入します。

SelectControl コンポーネントを使ってセレクトメニューを表示します。

前の例では最新の投稿3件をブロックに挿入しました。表示される3件の投稿は常に最新のもので、新規に投稿が追加されればその投稿を含む3件が表示されます。

この例では、選択した投稿のタイトルと抜粋を表示します。投稿側で内容が変更されると自動的に表示される内容も更新されます。

前の例と同様、withSelect を使って投稿を取得しますが、この例の場合、全ての投稿を取得しセレクトメニューにリストとしてタイトルを表示します。

以下では前の例で作成したブロックを使って変更していきます。

全ての投稿を取得

withSelect 内の投稿を取得する関数 getEntityRecords に渡すクエリパラメータを全ての投稿を取得するように変更します。

src/index.js 抜粋
edit: withSelect(select => {
  const currentPostId = select('core/editor').getCurrentPostId();
  //クエリパラメータ
  const query = {
    per_page: -1,  //全ての投稿を取得
    exclude: currentPostId
  }
  return {
    posts: select('core').getEntityRecords('postType', 'post', query)
  }
})(Edit),
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
//withSelect, select をインポート
import { withSelect, select } from '@wordpress/data';

registerBlockType( 'wdl/dynamic-block-sample', {
  title:'Dynamic Block Sample',
  description: 'My first dynamic block sample.',
  category: 'widgets',
  icon: 'smiley',
  supports: {
    html: false,
  },
  //withSelect を使って投稿を取得(投稿 posts を props で Edit に渡す)
  edit: withSelect(select => {
    //現在の投稿の ID
    const currentPostId = select('core/editor').getCurrentPostId();
    //クエリパラメータ
    const query = {
      per_page: -1,
      exclude: currentPostId
    }
    return {
      posts: select('core').getEntityRecords('postType', 'post', query)
    }
  })(Edit),
  save: () => { return null }
} );
属性の追加

選択した投稿 ID を保存する属性 selectedPostId を register_block_type() に追加します。

dynamic-block-sample.php 抜粋
register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    'render_callback' => 'wdl_dynamic_block_sample_render',
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      //選択した投稿 ID を保存する属性
      'selectedPostId' => [
        'type' => 'number',
        'default' => 0
      ],
    ]
  ) );
dynamic-block-sample.php
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      'selectedPostId' => [
        'type' => 'number',
        'default' => 0
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback のコールバック関数(PHP でレンダリングする関数)
function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿のタイトルを表示するマークアップ(空文字で初期化)
  $post_title_markup = '';
  // 投稿の抜粋を表示するマークアップ(空文字で初期化)
  $my_excerpt_markup = '';

  if ($attr['selectedPostId'] > 0) {
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $post_title_markup = '';
    }
    $my_url = esc_url( get_permalink($my_post) );
    $my_title = esc_html( get_the_title($my_post) );
    $post_title_markup = '<h4><a href="' . $my_url . '">'. $my_title . '</a></h4>';
    $my_excerpt = get_the_excerpt($attr['selectedPostId']);
    if($my_excerpt) {
      $my_excerpt_markup = '<p>'. esc_html( $my_excerpt ).'</p>' ;
      //$my_excerpt_markup = '<p>'. $my_excerpt .'</p>' ;
    }
  }
  
  // 全体のクラス名
  $class = 'wp-block-wdl-dynamic-block-sample';
  //投稿部分のクラス名
  $post_class = 'post-content';

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<div class="%4$s">%5$s%6$s</div></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    esc_attr( $post_class ),
    $post_title_markup,
    $my_excerpt_markup
  );
  
  //ブロックの出力の文字列を返す
  return $block_content;
}
インスペクターの追加

この例では投稿のリストを表示するセレクトメニューをサイドバーのインスペクターに表示するので、インスペクターを追加する関数を作成します(関数にせずに return ステートメント内に記述することもできます)。以下がインスペクターを追加する関数の記述です。

InspectorControls コンポーネントの中に PanelBody、PanelRow コンポーネントを配置し、その中にセレクトメニューの SelectControl を配置します。

関連項目:インスペクター

src/edit.js 抜粋
const getInspectorControls = () => {
  return (
    <InspectorControls>
      <PanelBody
        title="投稿の挿入" //パネルのタイトル
        initialOpen={true} //初期状態でパネルを開いて表示
      >
      <PanelRow>
        <SelectControl
          label='投稿セレクトメニュー'  //セレクトメニューのラベル
          options={select_options}
          value={selectedPostId}
          onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
        />
      </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
}
SelectControl

投稿のリストを表示するセレクトメニューを SelectControl で作成します。SelectControl は <select> 要素を使ったコンポーネントで、components パッケージにあります。

value プロパティには選択された値、この例の場合は選択された投稿の ID(属性 selectedPostId の値)を指定します。

onChange プロパティでは、選択された値が変更されると setAttributes を使って属性 selectedPostId の値を新しく選択された値に設定します。

options プロパティはセレクトメニューに表示される項目です。

src/edit.js 抜粋
<SelectControl
  label='投稿セレクトメニュー'
  options={select_options}
  value={selectedPostId}
  onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
/>

options プロパティには label(メニューに表示される文字)と value(値)プロパティを持つオブジェクトの配列を指定します。この例の場合、withSelect 内の getEntityRecords で取得した投稿から作成します。

以下が に指定する配列 select_options の記述です。

posts は props 経由で取得する全ての投稿オブジェクトの配列です。投稿の取得は非同期で行われ、取得が完了していない場合は posts の値は null になるので、その場合は value を 0(初期値)、label に「読み込み中」と指定しています。

投稿の読み込みが完了していれば、forEach() で各投稿のオブジェクトのプロパティから投稿 ID とタイトルを取得して value と label に指定してオブジェクトを生成し、配列に追加します。

src/edit.js 抜粋
let select_options = [];
if (posts) {
  select_options.push({ value: 0, label:'投稿を選択' });
  posts.forEach(post => {
    select_options.push({ value: post.id, label: post.title.rendered });
  });
} else {
  select_options.push({ value: 0, label:'読み込み中' })
}

各投稿のオブジェクトのプロパティは以下のようにすればコンソールに出力して確認することができます。

src/edit.js 抜粋
export default function Edit( props ) {
  const { posts, className, setAttributes, ・・・} = props;
  
  console.log(posts); // props 経由で取得した posts をコンソールに出力
  ・・・

上記の options プロパティの作成では投稿 ID の id とタイトルの title.rendered を使用しています。タイトルは title だけではエラーになるので title.rendered または title.raw を指定する必要があります。

同様に抜粋を取得するには excerpt.rendered または excerpt.raw を指定します。以下の例では .raw は空で、.rendered には値が表示されていますが、.raw は抜粋の入力欄に記述された値が入り、.rendered には実際にレンダリングされる値が入ると思いますが、テーマの抜粋の設定に依存すると思います。

投稿のレンダリング

選択された投稿の部分のレンダリングは、投稿の ID が選択され、投稿の読み込みが完了している場合に生成しています。

forEach() を使って読み込まれた投稿をループして、選択された投稿の ID と読み込まれた投稿の ID が一致する場合に、投稿からリンク(link)とタイトル(title.rendered)、抜粋(excerpt.rendered)を取得してレンダリングする内容を作成しています。

抜粋のプロパティには raw と rendered があり、raw には抜粋の入力欄に入力された値が格納されます。テーマが自動的に抜粋を生成する設定になっていれば、抜粋の入力欄に何も指定されていなくても rendered には自動的に取得された(レンダリングされた)抜粋の値が格納されています。

post_content に格納される値は文字列ではなく JSX で記述された React 要素になります。初期値は空文字列にしていますが、空の配列の方が良いかもしれません(?)。

src/edit.js 抜粋
let post_content = ""; 
if(selectedPostId && posts) {
  posts.forEach(post => {
    //選択された投稿 ID と一致する場合に JSX を生成
    if(selectedPostId === post.id) {
      post_content = (
        <div className="post-content">
          <h3><a href={ post.link }> { post.title.rendered } </a></h3>
          { post.excerpt &&  
            <p> {post.excerpt.rendered} </p>           
          }
        </div>
      )
    }
  });
} 

上記のコードの後に console.log(post_content) と記述すれば、上記で生成した JSX を確認することができます。

src/edit.js
import './editor.scss';
// InspectorControls を block-editor パッケージからインポート
import { RichText, InspectorControls } from '@wordpress/block-editor';
// SelectControl, PanelBody, PanelRow を components パッケージからインポート
import { SelectControl, PanelBody, PanelRow } from '@wordpress/components';

export default function Edit( props ) {
  //attributes:selectedPostId を追加
  const { posts, className, setAttributes, attributes: { selectedPostId, myTitle, myContent } } 
  = props;
  
  //SelectControl の options プロパティに指定する投稿のタイトルとIDから成るオブジェクトの配列
  let select_options = [];
  if (posts) {
    select_options.push({ value: 0, label:'投稿を選択' });
    posts.forEach(post => {
      select_options.push({ value: post.id, label: post.title.rendered });
    });
  } else {
    select_options.push({ value: 0, label:'読み込み中' })
  }
  
  //選択された投稿部分のレンダリング
  let post_content = "";
  
  if(selectedPostId && posts) {
    posts.forEach(post => {
      if(selectedPostId === post.id) {
        post_content = (
          <div className="post-content">
            <h3><a href={ post.link }> { post.title.rendered } </a></h3>
            { post.excerpt &&  
              <p> {post.excerpt.rendered} </p>           
            }
          </div>
        )
      }
    });
  } 
  
  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="投稿の挿入"
          initialOpen={true}
        >
        <PanelRow>
          <SelectControl
            label='投稿セレクトメニュー'
            //multiple={true}
            options={select_options}
            value={selectedPostId}
            onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
          />
        </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }
        
  return (
    [
      getInspectorControls(), //インスペクター
      <div className={ className }  >
        <RichText 
          className='myRichTextTitle'
          value={ myTitle }
          onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
          tagName='h3'
          placeholder= 'タイトルを入力' 
          keepPlaceholderOnFocus={true}
        />
        <RichText
          className='myRichTextContent'
          value={ myContent }
          onChange={ (newContent) => setAttributes({ myContent: newContent }) }
          tagName='div'
          multiline= 'p'
          placeholder= '文章を入力' 
        />
        { post_content }
      </div>
    ]
  );
}
PHP でのレンダリング

render_callback に指定する関数では投稿のタイトルと抜粋を表示するマークアップを作成して、最終的なマークアップに含めます。

選択された投稿の ID は属性から $attr['selectedPostId'] で取得できます。

get_post() に投稿 ID を指定して投稿のオブジェクトを変数 $my_post に格納します。

get_permalink() で投稿へのパーマリンクを取得し、esc_url() でエスケープ処理します。 タイトルは get_the_title() を使って取得し、esc_html() でエスケープ処理し、タイトルにリンクを付けて h4 要素でマークアップします。

抜粋は get_the_excerpt() で取得して、空でなければ esc_html() でエスケープ処理して p 要素でマークアップします。

最終的なマークアップを作成する際に、投稿部分の div 要素にクラス名 post-content を追加しています。

dynamic-block-sample.php 抜粋
function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿のタイトルを表示するマークアップ(空文字で初期化)
  $post_title_markup = '';
  // 投稿の抜粋を表示するマークアップ(空文字で初期化)
  $my_excerpt_markup = '';

  // 投稿が選択されていれば(値が初期値の 0 より大きければ)
  if ($attr['selectedPostId'] > 0) {
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $post_title_markup = '';
    }
    $my_url = esc_url( get_permalink($my_post) );
    $my_title = esc_html( get_the_title($my_post) );
    $post_title_markup = '<h4><a href="' . $my_url . '">'. $my_title . '</a></h4>';
    $my_excerpt = get_the_excerpt($attr['selectedPostId']);
    if($my_excerpt) {
      $my_excerpt_markup = '<p>'. esc_html( $my_excerpt ).'</p>' ;
    }
  }
  
  // 全体のクラス名
  $class = 'wp-block-wdl-dynamic-block-sample';
  //投稿部分のクラス名
  $post_class = 'post-content';

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<div class="%4$s">%5$s%6$s</div></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    esc_attr( $post_class ),
    $post_title_markup,
    $my_excerpt_markup
  );
  
  //ブロックの出力の文字列を返す
  return $block_content;
}
dynamic-block-sample.php
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      'selectedPostId' => [
        'type' => 'number',
        'default' => 0
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback のコールバック関数(PHP でレンダリングする関数)
function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿のタイトルを表示するマークアップ(空文字で初期化)
  $post_title_markup = '';
  // 投稿の抜粋を表示するマークアップ(空文字で初期化)
  $my_excerpt_markup = '';

  if ($attr['selectedPostId'] > 0) {
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $post_title_markup = '';
    }
    $my_url = esc_url( get_permalink($my_post) );
    $my_title = esc_html( get_the_title($my_post) );
    $post_title_markup = '<h4><a href="' . $my_url . '">'. $my_title . '</a></h4>';
    $my_excerpt = get_the_excerpt($attr['selectedPostId']);
    if($my_excerpt) {
      $my_excerpt_markup = '<p>'. esc_html( $my_excerpt ).'</p>' ;
    }
  }
  
  // 全体のクラス名
  $class = 'wp-block-wdl-dynamic-block-sample';
  //投稿部分のクラス名
  $post_class = 'post-content';

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<div class="%4$s">%5$s%6$s</div></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    esc_attr( $post_class ),
    $post_title_markup,
    $my_excerpt_markup
  );
  
  //ブロックの出力の文字列を返す
  return $block_content;
}
プレビューボタンの追加

以下は前述の例に ServerSideRender コンポーネントを使ったプレビューを表示するボタンを追加する例です。内容的には先の「プレビューボタンの追加」と同じです。

現在のモードの状態を表す属性 isEditMode を register_block_type の attributes に追加します。

dynamic-block-sample.php 抜粋
register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      ・・・中略・・・
      //isEditMode を属性として追加
      'isEditMode' => [
        'type' => 'boolean',
        'default' => true
      ],
    ]
  ) );
dynamic-block-sample.php
<?php
/**
 * Plugin Name:     Dynamic Block Sample
 * Description:     My first dynamic block sample.
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     dynamic-block-sample
 *
 * @package         wdl
 */

function wdl_dynamic_block_sample_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build` for the "wdl/dynamic-block-sample" block first.'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-dynamic-block-sample-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

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

  register_block_type( 'wdl/dynamic-block-sample', array(
    'editor_script' => 'wdl-dynamic-block-sample-block-editor',
    'editor_style'  => 'wdl-dynamic-block-sample-block-editor',
    'style'         => 'wdl-dynamic-block-sample-block',
    //コールバック関数を指定
    'render_callback' => 'wdl_dynamic_block_sample_render',
    //属性を設定
    'attributes' => [
      'myTitle' => [
        'type' => 'string',
        'default' => ''
      ],
      'myContent' => [
        'type' => 'string',
        'default' => ''
      ],
      'selectedPostId' => [
        'type' => 'number',
        'default' => 0
      ],
      //isEditMode を属性として追加
      'isEditMode' => [
        'type' => 'boolean',
        'default' => true
      ],
    ]
  ) );
}
add_action( 'init', 'wdl_dynamic_block_sample_block_init' );

//render_callback のコールバック関数(PHP でレンダリングする関数)
function wdl_dynamic_block_sample_render($attr) {

  // 属性の myTitle を使った h3 要素のマークアップ
  $dynamic_block_title = ( $attr['myTitle'] ) ? sprintf( '<h3>%1$s</h3>', $attr['myTitle'] ) : '';
  // 属性の myContent を使った div 要素のマークアップ
  $dynamic_block_content = ( $attr['myContent'] ) ? sprintf( '<div>%1$s</div>', $attr['myContent'] ) : '';
  // 投稿のタイトルを表示するマークアップ(空文字で初期化)
  $post_title_markup = '';
  // 投稿の抜粋を表示するマークアップ(空文字で初期化)
  $my_excerpt_markup = '';

  if ($attr['selectedPostId'] > 0) {
    $my_post = get_post($attr['selectedPostId']);
    if (!$my_post) {
      $post_title_markup = '';
    }
    $my_url = esc_url( get_permalink($my_post) );
    $my_title = esc_html( get_the_title($my_post) );
    $post_title_markup = '<h4><a href="' . $my_url . '">'. $my_title . '</a></h4>';
    $my_excerpt = get_the_excerpt($attr['selectedPostId']);
    if($my_excerpt) {
      $my_excerpt_markup = '<p>'. esc_html( $my_excerpt ).'</p>' ;
      //$my_excerpt_markup = '<p>'. $my_excerpt .'</p>' ;
    }
  }
  
  // 全体のクラス名
  $class = 'wp-block-wdl-dynamic-block-sample';
  //投稿部分のクラス名
  $post_class = 'post-content';

  // 最終的なマークアップを作成
  $block_content = sprintf(
    '<div class="%1$s">%2$s%3$s<div class="%4$s">%5$s%6$s</div></div>',
    esc_attr( $class ),
    $dynamic_block_title,
    $dynamic_block_content,
    esc_attr( $post_class ),
    $post_title_markup,
    $my_excerpt_markup
  );
  
  //ブロックの出力の文字列を返す
  return $block_content;
}

edit 関数では、BlockControls、Button、Toolbar、ServerSideRender、Fragment コンポーネントをインポートし、属性 isEditMode を props 経由で取得します。

そしてプレビューボタンを追加する関数 getBlockControls を定義します。詳細は先の「プレビューボタンの追加」を参照ください。

src/edit.js 抜粋
import './editor.scss';
// BlockControls を追加でインポート
import { RichText, InspectorControls, BlockControls} from '@wordpress/block-editor';
// Button、Toolbar を追加でインポート
import { SelectControl, PanelBody, PanelRow, Button, Toolbar  } from '@wordpress/components';
// ServerSideRender を追加でインポート
import ServerSideRender from '@wordpress/server-side-render';
//Fragment を追加でインポート
import { Fragment } from '@wordpress/element';


export default function Edit( props ) {
  //attributes:isEditMode を追加
  const { posts, className, setAttributes, attributes: { isEditMode, selectedPostId, myTitle, myContent } } 
  = props;
  
  //ツールバーにプレビューボタンを追加する関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  ・・・以下省略・・・

return ステートメントでは getBlockControls でプレビューボタンを出力し、isEditMode の値により通常の編集画面またはプレビューをレンダリングします。

通常の編集画面では RichText や投稿のレンダリング部分は Fragment で囲みます。

プレビューの ServerSideRender コンポーネントのプロパティには表示に必要な属性を全て指定します。この例ではプレビューの場合は my-custom-ssr というクラスを外側の div 要素(サーバー側のレンダリングされたブロックをラップする要素)に追加して編集画面と違いがわかるようにスタイルを設定していますが、通常は不要かも知れません。

src/edit.js 抜粋
return (
  [
    getBlockControls(), //プレビューボタン
    getInspectorControls(),  //インスペクター
    <div className={ className }  >
    { isEditMode &&  // isEditMode が true の場合(編集 モード)
      <Fragment> 
        <RichText 
          className='myRichTextTitle'
          value={ myTitle }
          onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
          tagName='h3'
          placeholder= 'タイトルを入力' 
          keepPlaceholderOnFocus={true}
        />
        <RichText
          className='myRichTextContent'
          value={ myContent }
          onChange={ (newContent) => setAttributes({ myContent: newContent }) }
          tagName='div'
          multiline= 'p'
          placeholder= '文章を入力' 
        />
        { post_content }
      </Fragment>
    }
    { !isEditMode &&   // isEditMode が false の場合(プレビュー モード)
      <ServerSideRender
        block={props.name} 
        attributes={{  
          myTitle: myTitle,  
          myContent: myContent, 
          selectedPostId: selectedPostId,
        }}
        className='my-custom-ssr' //ServerSideRender 用のクラスを追加
      />
    }
    </div>
  ]
);
src/edit.js
import './editor.scss';
// BlockControls を追加でインポート
import { RichText, InspectorControls, BlockControls} from '@wordpress/block-editor';
// Button、Toolbar を追加でインポート
import { SelectControl, PanelBody, PanelRow, Button, Toolbar  } from '@wordpress/components';
// ServerSideRender を追加でインポート
import ServerSideRender from '@wordpress/server-side-render';
//Fragment を追加でインポート
import { Fragment } from '@wordpress/element';


export default function Edit( props ) {
  //attributes:isEditMode を追加
  const { posts, className, setAttributes, attributes: { isEditMode, selectedPostId, myTitle, myContent } } 
  = props;
  
  //ツールバーにプレビューボタンを追加する関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        <Toolbar>
          <Button
            //属性 isEditMode の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //属性 isEditMode の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //setAttributes を使って属性の値を更新(真偽値を反転)
            onClick={() => setAttributes({ isEditMode: !isEditMode })}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  
  //SelectControl の options プロパティに指定する投稿のタイトルとIDから成るオブジェクトの配列
  let select_options = [];
  if (posts) {
    select_options.push({ value: 0, label:'投稿を選択' });
    posts.forEach(post => {
      select_options.push({ value: post.id, label: post.title.rendered });
    });
  } else {
    select_options.push({ value: 0, label:'読み込み中' })
  }
  
  //選択された投稿部分のレンダリング
  let post_content = "";
  
  if(selectedPostId && posts) {
    posts.forEach(post => {
      if(selectedPostId === post.id) {
        post_content = (
          <div className="post-content">
            <h3><a href={ post.link }> { post.title.rendered } </a></h3>
            { post.excerpt &&  
              <p> {post.excerpt.rendered} </p>           
            }
          </div>
        )
      }
    });
  } 
  
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="投稿の挿入"
          initialOpen={true}
        >
        <PanelRow>
          <SelectControl
            label='投稿セレクトメニュー'
            options={select_options}
            value={selectedPostId}
            onChange={(newId) => setAttributes({ selectedPostId: parseInt(newId) })}
          />
        </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }
        
  return (
    [
      getBlockControls(), //プレビューボタン
      getInspectorControls(),  //インスペクター
      <div className={ className }  >
      { isEditMode &&  // isEditMode が true の場合(編集 モード)
        <Fragment> 
          <RichText 
            className='myRichTextTitle'
            value={ myTitle }
            onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
            tagName='h3'
            placeholder= 'タイトルを入力' 
            keepPlaceholderOnFocus={true}
          />
          <RichText
            className='myRichTextContent'
            value={ myContent }
            onChange={ (newContent) => setAttributes({ myContent: newContent }) }
            tagName='div'
            multiline= 'p'
            placeholder= '文章を入力' 
          />
          { post_content }
        </Fragment>
      }
      { !isEditMode &&   // isEditMode が false の場合(プレビュー モード)
        <ServerSideRender
          block={props.name} 
          attributes={{  
            myTitle: myTitle,  
            myContent: myContent, 
            selectedPostId: selectedPostId,
          }}
          className='my-custom-ssr' //ServerSideRender 用のクラスを追加
        />
      }
      </div>
    ]
  );
}

この例の場合、編集モードでは抜粋部分にエスケープ処理した <p> が出力されてしまいます。

プレビュー画面では抜粋部分の <p> は表示されません。背景色などは追加したクラス my-custom-ssr を使って適当なスタイルを指定しています。

以下はフロントエンド側の表示例です。文字などの大きさや色はスタイルがいい加減に設定してあるためそれぞれの表示で異なっています。