WordPress Logo WordPress JSX を使った Gutenberg ブロックの作り方

WordPress のブロックエディタ Gutenberg で JSX を使って独自のブロックを作成する方法について。

JSX を使用するための環境構築は @wordpress/scripts(wp-scripts)を使っています。基本的なブロックの作成方法、JSX の基本的な使い方、WordPress のコンポーネントの使い方、webpack.config.js を使った環境のカスタマイズ(複数のエントリーポイントの設定方法)などについての覚書です。

以下は(旧)ブロックの作成 チュートリアル(削除予定)などの古い情報を参考にしているため部分的に古い情報になっています。

以下はすでに Node.js がインストールされていて、WordPress のローカル環境が設定済みであることを前提にしています。また、コマンドの実行例は Mac のターミナルを使用しています。

更新日:2024年01月21日

作成日:2020年09月25日

WordPress 関連リンク

このページ以外にも以下のようなブロックの作成に関するページがあります。

ブロック作成関連ページ

ブロック作成の概要

Gutenberg のブロックを作成するには、少なくとも以下の2つのファイルが必要です。

  • ブロックを定義する JavaScript のファイル(ブロックを定義して登録)
  • 定義したブロック(上記ファイル)を WordPress に登録する PHP ファイル

実際にはブロックのスタイルを設定するためのスタイルシートも必要になります。ブロックの定義ではエディター画面用とフロントエンド用の2つの出力を定義します。そのため、必要に応じてスタイルシートもそれぞれに用意することになります。

プラグイン/テーマ

ブロックを作成する方法としては、プラグインとして作成する方法とテーマの一部として作成する方法があります。

どちらの場合もブロックを定義する JavaScript の内容自体は同じですが、プラグインとして作成する場合はプラグインのディレクトリにファイルを配置し、ブロックを登録する PHP ファイルではプラグインヘッダを記述してプラグインとして登録するのに対し、テーマとして作成する場合はテーマのディレクトリに配置し、ブロックの登録は functions.php に記述します。

JSX

ブロックを定義する JavaScript では、ブラウザが理解できる JavaScript のみを使って記述することもできますが、JSX と呼ばれる XML に似た JavaScript 構文の拡張を使うとブロックの構造を簡潔に記述することができ、複雑なブロックの作成の場合などでは特に便利です。

但し、JSX はそのままではブラウザが理解できないので実際に使用する際はブラウザが理解できる JavaScript に変換(コンパイル)する必要があります。そのため、JSX をコンパイルできる環境を構築して、開発中は JSX で記述してテストし、本番用のファイルはコンパイルした(ブラウザが理解できる JavaScript に変換した)ものを使います。

以下ではブロックの定義で JSX を使うので、WordPress が提供する環境構築用のパッケージ @wordpress/scripts(wp-scripts)を使って環境を構築しています。また、ブロックはテーマの一部として作成しています。

開発環境の構築

このページで使用するサンプルの大まかなファイル構成は以下のようになっています。

wp-content
└─ themes
    └─ blockX //独自テーマのディレクトリ
        ├── category.php
        ├── front-page.php
        ├── functions.php
        ├── index.php
        │   ・・・中略(その他のテンプレートファイル等)・・・
        ├── my-blocks //ブロック用ディレクトリ(本番環境で使用するファイル)
        │   ├── build //ビルドされたファイルの出力先
        │   │   ├── wdl-block-01.asset.php //ビルドで自動生成されるアセットファイル
        │   │   └── wdl-block-01.js //コンパイルされたソースファイル
        │   └── block-registration //スクリプトを登録するPHPファイルを格納するディレクトリ
        │       └── register-wdl-block-01.php //ブロック用スクリプトを登録するPHPファイル
        └── block-dev //開発用ディレクトリ(開発環境をインストールするディレクトリ)
            ├── node_modules //npm でインストールされたモジュールが格納されているディレクトリ
            ├── package-lock.json
            ├── package.json
            ├── webpack.config.js //独自の webpack.config.js
            └── src //編集用ブロック用スクリプト(エントリーポイント)を格納するディレクトリ
                └── wdl-block-01.js  //ブロック用スクリプト(ソースファイル)

JSX を使用するための環境を構築する方法は色々とありますが、この例では @wordpress/scripts を使って環境を構築しています。以下は構築の手順の一例です。

ブロック作成のための JavaScript ビルド環境の構築方法については以下を御覧ください。

Gutenberg ブロック開発の環境構築

package.json を生成

ターミナルで開発用ディレクトリ(この例の場合:block-dev)に移動して npm init -y を実行してデフォルトの値で package.json を生成します。

@wordpress/scripts をインストール

wp-scripts(@wordpress/scripts) をインストールします。

ビルド用コマンドを追加

インストールが完了すると関連パッケージが node_modules 配下にインストールされ、package.json が更新されるので、package.json の scripts フィールドにビルド用コマンド(start と build)を追加します。

package.json
{
  "name": "block-dev",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "wp-scripts start",
    "build": "wp-scripts build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@wordpress/scripts": "12.2.0"
  }
}

webpack の設定を拡張

この例の環境構築で使用している wp-scripts の場合、デフォルトではエントリポイントのブロック用スクリプトとして src ディレクトリに index.js を作成し、ビルドすると build ディレクトリに同じ名前のファイル index.js が出力されるようになっています。

この設定は webpack.config.js という webpack の設定ファイルの entry と output で以下のように設定されています。

node_modules/@wordpress/scripts/config/webpack.config.js 抜粋
entry: {
  index: path.resolve( process.cwd(), 'src', 'index.js' ), // src/index.js
},
output: {
  filename: '[name].js', // エントリポイントのキー(index)と同じ名前で
  path: path.resolve( process.cwd(), 'build' ), // build ディレクトリに出力
},

この例では、デフォルトのエントリポイントの名前と出力先を変更するため、webpack の設定を拡張します。そのため、以下のような独自の webpack.config.js という名前のファイルを作成し、package.json と同じ階層に配置します(元の webpack.config.js は直接編集しません)。

以下の場合、entry で指定したブロック用スクリプトのソースファイルをビルドすると、output で指定したフォルダにコンパイルされて出力されます。output の filename に [name] を使っているので、entry のキー(wdl-block-01)と同じ名前のファイルが生成されます。

webpack.config.js
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const path = require('path');

module.exports = {
  //先頭で取得した既存の設定をスプレッド構文(...)で以下に展開
  ...defaultConfig,
  // 既存の設定を上書き
  entry: {
    'wdl-block-01': './src/wdl-block-01.js'
  },
  output: {
    path: path.join(__dirname, '../my-blocks/build'),
    filename: '[name].js'
  }
}

ブロックを定義するファイルを作成

src フォルダを開発用ディレクトリ(block-dev)の直下に作成し、以下のようなブロックを定義する JavaScript のファイルを作成します(内容の詳細は ブロックの作成 参照)。

wdl-block-01.js
import { registerBlockType } from '@wordpress/blocks';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: () => <div>Hello World! (Edit)</div>,
  save: () => <div>Hello World! (Save)</div>,
} );

ビルドを実行

ターミナルを使って開発用ディレクトリ block-dev でビルドを実行します。

ビルドにより、ソースファイル src/wdl-block-01.js はコンパイルされて my-blocks/build に同じ名前のファイルとして生成(出力)されます。出力先のフォルダ my-blocks/build はビルドの際に webpack により自動的に作成されます。

また、同時に wdl-block-01.asset.php という名前のアセットファイルが出力先の my-blocks/build に自動的に生成されます。

定義したブロックを登録

my-blocks の下に php というフォルダを作成して、その中に以下のようなブロック用スクリプトを読み込むファイルを作成します(内容の詳細は 定義したブロックを登録 参照)。

register-wdl-block-01.php
<?php
function wdl_block_01_enqueue() {
  //アセットファイルをインクルードして変数に保存
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-01.asset.php'));

  //ブロック用のスクリプトを登録
  wp_register_script(
    'wdl-block-01',
    get_theme_file_uri('/my-blocks/build/wdl-block-01.js'),
    $asset_file['dependencies'], //依存スクリプトの配列
    $asset_file['version'] //バージョン
  );

  //ブロックタイプの登録
  register_block_type(
    'wdl/block-01',
    array(
      //エディター用スクリプトとしてブロック用スクリプトを指定(登録)
      'editor_script' => 'wdl-block-01',
    )
  );
}
add_action( 'init', 'wdl_block_01_enqueue' );

functions.php

functions.php で上記のファイル(register-wdl-block-01.php)をインクルードします。

以下を functions.php に記述
include( get_theme_file_path('/my-blocks/block-registration/register-wdl-block-01.php'));

ブロックを確認

投稿の編集ページで、作成したブロックを投稿に挿入して確認します。

ブロックの挿入の「+」印をクリックしても最初はリストに表示されないので、「全てを表示」をクリックして作成したブロックを探します。この例の場合、ブロックを定義する際に category を layout に設定したので、作成したブロックは「デザイン」の中に表示されます。

ブロックを挿入すると「Hello World! (Edit)」と表示されます。

以下はエディター画面での表示です。

以下はフロントエンド側での表示です。フロントエンド側では「Hello World! (Save)」と表示されます。また、フロントエンド側ではラッパー要素の div タグに自動的にクラス名が付けられます。

webpack.config.js

webpack.config.js は webpack の設定が記述されたファイルで、エントリポイントやビルドされたファイルの出力先、Babel の設定、CSS や Sass の出力方法、ソースマップファイルの出力などの設定が記述されています。

wp-scripts のインストールで自動的に生成される webpack.config.js は node_modules ディレクトリの中にあります。

設定の追加や変更は独自の webpack.config.js を作成して行います(直接編集しません)。

関連項目

以下は wp-scripts により生成された webpack.config.js の例です。エントリーポイントや出力先の設定、どのようなローダーやプラグインが使用されているかが確認できます。

ブロックの作成(JavaScript)

ブロックを作成するには、ブロックを定義する JavaScript ファイルを作成し、そのファイルを PHP(WordPress の関数)を使って登録します。

開発環境の構築の際に以下のようなブロックを定義する JavaScript ファイルを作成しました。

先頭では import を使って @wordpress/blocks にアクセスして、ブロックの登録・定義に必要な registerBlockType 関数を取得しています。

そして registerBlockType 関数を使ってブロックを定義しています。

wdl-block-01.js
import { registerBlockType } from '@wordpress/blocks';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: () => <div>Hello World! (Edit)</div>,
  save: () => <div>Hello World! (Save)</div>,
} );

この例の場合、ブロックを定義する JavaScript ファイルでは JSX を使っているので、実際に使用する際にはビルドを実行してコンパイルする必要があります。

通常、開発中は npm start を実行して開発モードで自動的にビルドし、最終の配布用には npm run build を実行してビルドし、ファイルを圧縮及び最適化します。

開発の開始

開発を行う際は npm start を実行して開発モードで自動的にビルドするようにします。

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

$ npm start  return  //開発モードを開始

パッケージへのアクセス

ブロックの作成では、wp グローバル変数を使用して WordPress 内で登録されている JavaScript パッケージにアクセスすることで、様々な機能を利用することができます。

例えば、registerBlockType 関数は wp.blocks に含まれているので使用するには wp.blocks.registerBlockType() のように記述できます。通常は以下のように分割代入を使います。

const { registerBlockType } = wp.blocks;
registerBlockType(); //取得した registerBlockType() を使う

このページの例の場合、以下のように import を使って @wordpress/blocks パッケージにアクセスしてモジュールをインポートしています。

import { registerBlockType } from '@wordpress/blocks';
registerBlockType(); //取得した registerBlockType() を使う
インポート

インポート文を使用するとファイルからエクスポートされた変数や関数をインポートすることができます。

通常のインポートは以下のようにモジュールのパスを指定します。

import 名前(変数名) from 'モジュールのパス';

wp-scripts では webpack を使用しているため、npm 経由でインストールしたモジュールは webpack のモジュール解決の仕組みがあるので通常のローカルファイルのインポートとは異なりパスや拡張子を省略することができます。

import { registerBlockType } from '@wordpress/blocks';

以下はブロックのスタイルに適用するスタイルシートをインポートする例です。この場合はパスや拡張子は省略できません。

import { registerBlockType } from '@wordpress/blocks';
import './style.scss';  //同じ階層(./)にあるスタイルシートをインポート
import './editor.scss';  //同じ階層(./)にあるスタイルシートをインポート

registerBlockType

Javascript を使って新しく独自のブロックを登録・定義するには registerBlockType 関数を使います。

wdl-block-01.js 抜粋(見やすいように改行しています)
registerBlockType(
  //ブロック名 namespace/block-name
  'wdl/block-01',
  //ブロックのプロパティ
  {
    title: 'WDL Sample Block 01',
    icon: 'smiley',
    category: 'layout',
    edit: () => <div>Hello World! (Edit)</div>,
    save: () => <div>Hello World! (Save)</div>,
  }
);

registerBlockType は2つのパラメータを受け取ります。

registerBlockType( ブロック名, ブロックのプロパティ )
ブロック名
  • ブロックを識別するための文字列を namespace/block-name の形式で指定します。
  • ブロック名は小文字の英数字かダッシュ(ハイフン)のみで指定し、文字で始まる必要があります。
  • namespace にはブロックを一意に識別できるようなユニークな文字列を指定します。
  • PHP 側のブロックの登録 register_block_type での第1パラメータ $name と同じ値になります。

この例では namespace に wdl を、block-name に block-01 を指定して、wdl/block-01 としています。

WordPress の標準(コア)のブロックの namespace は core が使われています。例えば、標準の paragraph のブロックは core/paragraph となっているので、core 以外の namespace を指定すれば paragraph という名前のブロックを登録できます。

ブロックのプロパティ

{ key: value } で指定したブロックのプロパティ(オブジェクト)

この例では、必須の title と category、及びオプションの icon を指定しています。 また、edit と save プロパティでブロックがどのようにレンダリングされるかを指定しています。

ブロックの主なプロパティには以下のようなものがあります(registerBlockType 関数より)。

ブロックに指定できるプロパティ一部抜粋
プロパティ type 説明
title String (必須)インサーターに表示されるブロックのタイトル。
category String (必須)ブロックのカテゴリー。以下のいずれかの文字列を指定。
  • common(一般ブロック)
  • formatting(フォーマット)
  • layout(レイアウト要素)
  • widgets(ウィジェット)
  • embed(埋め込み)
カスタムカテゴリーを作成することもできます。
description String ブロックの簡単な説明。
icon String ブロックを表すアイコン画像の指定。任意の WordPress Dashicon またはカスタム svg 要素を指定できます。icon プロパティには文字列として Dashicon の名前を指定します。例: dashicons-smiley なら smiley。
styles Array ブロックスタイルを使用してブロックに代替のスタイルを適用。
attributes Object 任意に設定できる属性(オブジェクト)を指定。
example Object プレビューを作成するための設定。example が定義されていない場合、プレビューは表示されません。※属性が定義されていない場合にもプレビューを表示するには、空の example オブジェクト example: {} を設定します。
supports Object ブロックの拡張機能を有効にするかどうか。例:align: true を指定するとツールバーにブロックの位置揃え(block alignment)のボタンが追加されます。anchor: true を指定するとインスペクターの「高度な設定」に HTML アンカーを設定するセクションが追加されます。customClassName: false を指定するとインスペクターの「高度な設定」から「追加 CSS クラス」を削除します。html: false を指定するとツールバーの右端のボタンをクリックして表示される「HTMLとして編集」のオプションが表示されなくなります。(ブロックサポート
edit function エディターにブロックが挿入された際のこのブロックのレンダリング結果(どのようにブロックを表示するか)を返す関数。 edit と save 関数
save function 投稿が保存された際に、エディターが post_content フィールドに挿入するブロックのレンダリング結果(ブロックがサイトのフロントエンドでどのように表示されるか)を返す関数。post_content フィールドは投稿のコンテンツを保存する WordPress データベース内のフィールドです。

例えば、以下のようなプロパティを設定しているブロックの場合、

registerBlockType( 'wdl/block-01', {
  //表示されるブロックのタイトル
  title: 'WDL Sample Block 01',
  //表示に使われるアイコン画像
  icon: 'smiley',
  //リストされる場所(カテゴリー)
  category: 'layout',
  //簡単な説明
  description: '最初のサンプルブロック',
  //ブロックのプレビュー(この例では空のオブジェクト {} を設定)
  example: {},
  //拡張機能のサポート(「HTMLとして編集」のオプションを非表示)
  supports: {
    html: false,
  },
  //ブロックスタイル
  styles: [
    {
      name: 'default',
      label: '角丸',
      isDefault: true
    },
    {
      name: 'squared',
      label: '角丸なし'
    },
  ],
  ・・・以下省略・・・
});

プロパティに設定したいくつかの項目はブロックのインサーターなどに以下のように表示されます。

インサーターに表示されていない場合は、「全て表示」をクリックするとブロックの一覧が表示されます。

ブロックを挿入後、該当のブロックを選択するとサイドバーには以下のように表示されます。

supports プロパティで html: false を指定するとツールバーの右端のボタンをクリックしてデフォルトでは表示される「HTMLとして編集」のオプションが表示されなくなります。

edit と save プロパティ

edit と save プロパティでは、実際にブロックがどのようにエディター画面とフロントエンドでレンダリングされるかを定義します。これらのプロパティはどちらも、レンダリング結果を返す関数を指定します。

  • edit 関数:エディターがどのようにブロックをレンダリングするかを定義して返す関数。
  • save 関数:フロントエンド側でブロックがどのようにレンダリングされるかを定義して返す関数。

以下は前述の registerBlockType の edit と save プロパティの抜粋です。

この例の場合は、edit 関数と save 関数のどちらも、div 要素をレンダリングする JSX 返しています。

edit: () => <div>Hello World! (Edit)</div>,
save: () => <div>Hello World! (Save)</div>,

以下が出力です。save 関数ではクラス名は自動的に追加されますが、edit 関数では追加されません。

edit 関数 によりエディター画面で出力されるブロック
<div>Hello World! (Edit)</div>
save 関数 によりフロントエンド側で出力されるブロック
<div class="wp-block-wdl-block-01">Hello World! (Save)</div>

return を省略せず記述すると以下のようになります。

edit: () => {
  return <div>Hello World! (Edit)</div>
},
save: () => {
  return <div>Hello World! (Save)</div>
},

また、アロー関数ではなく、function 文で記述すると以下のようになります。

edit: function() {
  return <div>Hello World! (Edit)</div>
},
save: function() {
  return <div>Hello World! (Save)</div>
}

HTML のように見える <div>Hello World! (Edit)</div> や <div>Hello World! (Save)</div> は JSX と呼ばれる JavaScript 構文の拡張で、実際には JavaScript です。

JSX では HTML や XML のタグのように見える JSX 独自の構文を使って画面に描画したい内容を記述します(最終的に DOM にレンダリングするものを定義します)。

JSX を使わずに createElement メソッドを使って記述すると以下のようになります。

import { registerBlockType } from '@wordpress/blocks';
// createElement をインポート
import { createElement } from '@wordpress/element';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  // JSX を使わずに createElement を使って記述
  edit: function() {
    return createElement(
      'div',
      {},
      'Hello World! (Edit)'
    );
  },
  save: function() {
    return createElement(
      'div',
      {},
      'Hello World! (Save)'
    );
  },
} );

ビルドの際には、JSX を使った記述は Babel により上記のような createElement を使った記述に変換されます。

createElement は React のメソッドで、React 要素(React が管理するオブジェクト)を生成するメソッドです(Gutenberg のブロックは内部では React が使われています)。

JSX

JSX は Facebook 社が考案した XML に似た JavaScript 構文の拡張です。

JSX は HTML や XML のタグのように要素を入れ子にしたり、属性を指定することができますが、実際は JavaScript なので JavaScript(JSX)特有の制限や記述方法(文法)があります。

また、JSX により生成される要素(React 要素)は HTML のような見た目ですが、実際には DOM ノードではなく React が管理するオブジェクト(Virtual DOM)で、React によりレンダリングされます。

以下の registerBlockType の edit と save プロパティで return しているのは HTML のように見えますが JSX で記述された React 要素で、JavaScript の式です。

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: () => {
    return <div>Hello World! (Edit)</div>
  },
  save: () => {
    return <div>Hello World! (Save)</div>
  },
});

JSX で属性を指定

HTML のように属性を指定することができます(以下の場合は id 属性に文字列 "foo" を指定)。

edit: () => <div id="foo">Hello World! (Edit)</div>,

属性として JavaScript 式(変数など)を埋め込むには中括弧を使用します。

const myId = 'foo';  //id の値を変数に代入

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  //id 属性に変数(JavaScript 式)を指定
  edit: () => <div id={ myId }>Hello World! (Edit)</div>,
  save: () => <div>Hello World! (Save)</div>,
} );

HTML と似た記述ができますが、以下のような違いがあります。

class は className

class は JavaScript では予約されているため、className を使用します。同様に for も JavaScript では予約されているため、for 属性を指定する場合は htmlFor を使用します。

edit: () => <div className="bar" >Hello World! (Edit)</div>,

JSX はキャメルケース

JSX は HTML とは異なり、キャメルケース (camelCase) のプロパティ命名規則を使用します。例えば、tabindex は tabIndex(I が大文字)、onclick は onClick になります。

空要素(終了タグの省略)

タグが空の場合には、XML のように /> でタグを閉じる事ができます(終了タグを省略できます)。

edit: () => <TextareaControl />  //TextareaControl コンポーネント

関連項目:React/JSX で属性を指定

参考サイト:React/DOM 要素

JSX は JavaScript の式

JSX は JavaScript の式なので以下のように変数に代入することができます。

//JSX で生成した React 要素を変数に代入
const edit_elem = <div>Hello World! (Edit)</div>;
const save_elem = <div>Hello World! (Save)</div>;

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: () => {
    return edit_elem;
  },
  save: () => {
    return save_elem;
  },
});

JSX を複数行に分けて記述する場合は、自動的にセミコロンが挿入されないように括弧 ( ) で囲みます。

edit: () => {
  return (
    <div>
      Hello World! (Editx)
    </div>
  );
},
save: () => {
  return (
    <div>
      Hello World! (Savex)
    </div>
  );
},

JSX も HTML のように子要素を持つことができます(ネストすることができます)。

edit: () => {
  return (
    <div>
      <h3>Hello World!</h3>
      <p>Edit</p>
    </div>
  );
},
save: () => {
  return (
    <div>
      <h3>Hello World!</h3>
      <p>Save</p>
    </div>
  );
},

上記の場合、エディター画面ではブロックは以下のように表示されます。

但し、以下のように JSX 要素を並べて記述するとエラーになります。

edit: () => {
  return (
    //エラーになる例(隣接する JSX 要素は囲みタグで囲んで1まとまりとしなければならない)
    <h3>Hello World!</h3>
    <p>Edit</p>
  );
},

上記をコンパイルしようとすると、以下のようなエラーが表示されコンパイルできません。

隣接する JSX 要素は div 要素などのラッパー要素で囲んで1つのまとまりにする必要があるので、以下のように div 要素などで囲むか、Fragment を使って囲みます。

edit: () => {
  return (
    <div>
      <h3>Hello World!</h3>
      <p>Edit</p>
    </div>
  );
},
save: () => {
  return (
    <div>
      <h3>Hello World!</h3>
      <p>Save</p>
    </div>
  );
},
Fragment

Fragment を使用するには @wordpress/element パッケージからインポートします。

import { registerBlockType } from '@wordpress/blocks';
//Fragment をインポート
import { Fragment } from '@wordpress/element';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: () => {
    return (
      <Fragment>
        <h3>Hello World!</h3>
        <p>Edit(Fragment)</p>
      </Fragment>
    );
  },
  save: () => {
    return (
      <Fragment>
        <h3>Hello World!</h3>
         <p>Save</p>
      </Fragment>
    );
  },
});

Fragment は複数の要素をまとめるために使うコンポーネントで、それ自体は出力されません。

インラインスタイル

JSX で style 属性を使ってインラインスタイルを設定することができます。style 属性の設定は JavaScript のオブジェクト形式 { } で記述します。

JSX で JavaScript 式を埋め込むには中括弧 { } で囲むので、 style 属性でスタイルを指定する場合はさらに波括弧で囲みます。

以下はエディター側のブロックの div 要素に文字色と背景色のインラインスタイルを指定する例です。

オブジェクト形式で指定するので fontSize のようにキャメルケースで指定する必要があります。区切りはセミコロンではなく、カンマになります。

edit: () => <div style={ {color:'#fff', backgroundColor:'red'} }>Hello World! (Edit)</div>,

スタイルはオブジェクトなので変数に入れて使うこともできます。以下はエディター側とフロントエンド側で異なるスタイルを指定する例です。

また、以下ではフォントサイズを指定していますが、サイズを数値で指定した場合は px が適用されます。

//スタイルを変数に代入
const frontStyle = {color:'#fff', fontSize:20, backgroundColor:'blue' };
const editorStyle = {color:'yellow', fontSize:20, backgroundColor:'green' };

registerBlockType('wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  //属性として変数(JavaScript 式)を埋め込むには中括弧を使用
  edit: () => <div style={ editorStyle }>Hello World! (Edit)</div>,
  save: () => <div style={ frontStyle }>Hello World! (Save)</div>,
});

スタイルシートを使ったスタイルの適用は「スタイルの追加」を参照ください。

return null

何もレンダリングしたくない場合は、JSX で null を返します。

if (!props.xxxx) {
  return null; //何もレンダリングしない
}

ブロックを登録(PHP)

定義したブロックを利用するには、定義したブロックとそのスクリプトやスタイルをサーバー側(PHP)で登録して適切なタイミングで読み込むようにします。

この例の場合、ブロックをテーマの一部として作成しているので、ブロックやスクリプト及びスタイルの登録は functions.php に記述します。

但し、 functions.php に直接記述すると、 functions.php がごちゃごちゃしてしまうので、この例では別ファイルに記述し functions.php でそのファイルを読み込むようにしています。

この例の場合、ブロックのスクリプトをビルドすると、webpack.config.js の output で指定した my-blocks の中の build ディレクトリにコンパイルされて、同じファイル名で出力されます。

また、環境構築用のパッケージ @wordpress/scripts(wp-scripts) の設定により、ビルド時に依存スクリプトとバージョンが記述されたアセットファイルが自動的に my-blocks/build に生成されます。

blockX //独自テーマのディレクトリ
  ├── category.php
  ├── front-page.php
  ├── functions.php //register-wdl-block-01.php を読み込む
  ├── index.php
  │    ...
  ├── my-blocks
  │   ├── build  //ビルドされたファイルの出力先
  │   │   ├── wdl-block-01.asset.php //ビルドで自動生成されるアセットファイル
  │   │   └── wdl-block-01.js //コンパイルされたソースファイル
  │   └── block-registration
  │       └── register-wdl-block-01.php /* ブロックのファイルを読み込むPHPファイル */
  └── block-dev //開発用ディレクトリ
      ├── node_modules
      ├── package-lock.json
      ├── package.json
      ├── webpack.config.js
      └── src
          └── wdl-block-01.js  //ブロックを定義したファイル

開発環境の構築の際にブロックやスクリプトを登録する PHP ファイル(register-wdl-block-01.php)を作成しましたが、以下が詳細です。

この例の場合は、スタイルを使用していないので、スクリプトだけを登録していますが、ブロックに適用するスタイルがあれば一緒に登録します。

使用するスクリプトやスタイルの登録は、通常の WordPress でのスクリプトやスタイルの登録と同じように wp_register_script() や wp_register_style() を使います

そして register_block_type() を使ってブロックを登録し、スクリプトやスタイルを関連付けます。

これらの登録は init フックを使って行います。

register-wdl-block-01.php
<?php
function wdl_block_01_enqueue() {
  //依存スクリプトの配列とバージョンが記述されたアセットファイルをインクルード
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-01.asset.php'));

  //ブロック用のスクリプトを登録
  wp_register_script(
    'wdl-block-01', //ハンドル名
    get_theme_file_uri('/my-blocks/build/wdl-block-01.js'), //URL
    $asset_file['dependencies'], //依存スクリプトの配列
    $asset_file['version'] //バージョン
  );

  //ブロックの登録
  register_block_type(
    'wdl/block-01', //ブロック名 namespace/block-name
    array(
      //エディター用スクリプトとしてブロックのスクリプト wdl-block-01.js を関連付け
      'editor_script' => 'wdl-block-01',
    )
  );
}
add_action( 'init', 'wdl_block_01_enqueue' );  //init フック

ブロック用のスクリプトは wp_register_script を使って登録します。

wp_register_script

wp_register_script はスクリプトの登録のみを行う関数で、スクリプトをキューには追加しません。

以下が書式とパラメータです。

wp_register_script( $handle, $src, $deps, $ver, $in_footer )
  • $handle:スクリプトを識別するためのハンドル名(必須)
  • $src:スクリプトの URL(必須)
  • $deps:依存するスクリプトのハンドル名の配列
  • $ver:スクリプトのバージョン番号(ファイル名の末尾にクエリパラメータとして追加される)
  • $in_footer:</body> 終了タグの前に配置するかどうか(デフォルトは false)

依存するスクリプト

ブロック用のスクリプトの登録では、第3パラメータの $deps にブロックとして機能するために必要な依存するスクリプトを指定する必要があります。

以下は registerBlockType と createElement を使用するために依存するスクリプトのハンドル名 wp-blocks と wp-element を指定する例です。

wp_register_script(
  //ブロック用のスクリプトのハンドル名
  'wdl-block-01',
  //スクリプトの URL
  get_theme_file_uri('/my-blocks/build/wdl-block-01.js'),
  //依存スクリプトの配列
  array( 'wp-blocks', 'wp-element' ),
  //スクリプトのバージョン番号。filemtime() を使ってファイルのタイムスタンプを指定
  filemtime(get_theme_file_path('/my-blocks/build/wdl-block-01.js'))
);

この例では依存するスクリプトとバージョンは次項のアセットファイルを使って指定しています。

アセットファイル

@wordpress/scripts を使って環境構築すると、ビルドの際に自動的に依存スクリプトの配列とバージョン(タイムスタンプ)が記述されたアセットファイルが生成されます。

生成されるアセットファイルの名前は、エントリポイントのファイル名に .asset.php がついたもので、この例の場合は wdl-block-01.asset.php というファイルが生成されます。

以下は生成されたアセットファイルの例です。

wdl-block-01.asset.php
<?php
return array(
  //依存スクリプトの配列
  'dependencies' => array('wp-blocks', 'wp-element', 'wp-polyfill'),
  //バージョン
  'version' => '4ae23b24fbdca6bb35585b9d6c601503'
);

dependencies には依存するスクリプトの配列が、version にはファイルのタイムスタンプを使ったバージョンが自動的に更新されます。

そのため、このファイルを読み込んで取得した値を使って、wp_register_script のパラメータを指定すれば、ビルドするたびに必要に応じて更新されます。

register-wdl-block-01.php 抜粋
//依存スクリプトの配列とバージョンが記述されたアセットファイルをインクルード
$asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-01.asset.php'));

wp_register_script(
  'wdl-block-01',
  get_theme_file_uri('/my-blocks/build/wdl-block-01.js'),
  $asset_file['dependencies'], //依存スクリプトの配列
  $asset_file['version'] //バージョン
);

register_block_type

PHP 側でブロックを登録するには register_block_type 関数を使います。

register_block_type は2つのパラメータを受け取ります。以下が書式です。

register_block_type( $name, $args )
パラメータ
  • $name:ブロックを識別するための文字列を namespace/block-name の形式で指定します。
  • $args:ブロックのプロパティを連想配列で指定

$name に指定するブロックの名前は、registerBlockType の第1パラメータの「ブロック名」と同じ値(文字列)を指定します。この例では namespace に wdl を、block-name に block-01 を指定して、wdl/block-01 としています。

$args に指定できるプロパティは以下のようなものがあります

※ editor_script(エディター用スクリプトのハンドル)には、wp_register_script で登録したブロックのスクリプトのハンドル名を指定します。

キー 説明
render_callback このブロックを PHP 側でレンダリングするために使用するコールバック関数
attributes 任意の属性値を格納した連想配列
editor_script(※) エディター用スクリプトとしてブロックのスクリプト(ハンドル名)を指定
editor_style エディター用スタイルとしてスタイル(ハンドル名)を指定
script フロントとエディター用スクリプトとしてスクリプト(ハンドル名)を指定
style フロントとエディター用スタイルとしてスタイル(ハンドル名)を指定

editor_script、editor_style、script、style はブロックタイプのハンドルを表し、wp-include/class-wp-block-type.php で定義されています。

この例の場合、ブロック用のスクリプトのハンドル名 wdl-block-01 を editor_script に指定して、wdl-block-01.js をエディタ用スクリプトとして登録しています。

//ブロック用のスクリプトを登録
wp_register_script(
  'wdl-block-01', //ハンドル名
  get_theme_file_uri('/my-blocks/build/wdl-block-01.js'),
  $asset_file['dependencies'],
  $asset_file['version']
);

//register_block_type を使ったブロックの登録
register_block_type(
  //ブロック名(registerBlockType の第1パラメータと同じ文字列)
  'wdl/block-01',
  array(
    //エディタ用スクリプトとしてブロックのスクリプト wdl-block-01.js を登録(ハンドル名を指定)
    'editor_script' => 'wdl-block-01',
  )
);

複数のエントリーポイント

wp-scripts(@wordpress/scripts)のデフォルトの設定では、src ディレクトリに index.js というブロック用のスクリプト(エントリーポイント)を作成して build ディレクトリに出力するようになっていますが、複数のエントリーポイントを設定して複数のブロックを作成することもできます。

あまりこのような使い方はしないかも知れませんので、複数のエントリーポイントを設定する必要がない場合は読み飛ばしてください(どちらかというと webpack の設定の話になります)。

以下は3つのブロックを作成するために3つのエントリーポイントのファイルを作成した場合のファイル構成の例です。この場合、npm run build を実行すると3つのエントリーポイントのファイルがビルドされ、npm start を実行すると3つのエントリーポイントのファイルを開発モードで作業をすることができます。

複数のエントリーポイントを作成する場合は、ブロック用スクリプトとブロックを登録するファイルを追加する際に、環境構築の際に作成した webpack.config.js を編集する必要があります。

wp-content
└─ themes
    └─ blockX //独自テーマのディレクトリ
        ├── category.php
        ├── front-page.php
        ├── functions.php //ブロックを登録するPHPファイルを読み込む
        ├── index.php
        │    ...
        ├── my-blocks //ブロック用ディレクトリ
        │   ├── build //ビルドで自動的に生成されるファイル
        │   │   ├── wdl-block-01.asset.php
        │   │   ├── wdl-block-01.js
        │   │   ├── wdl-block-02.asset.php
        │   │   ├── wdl-block-02.js
        │   │   ├── wdl-block-03.asset.php
        │   │   └── wdl-block-03.js
        │   └── block-registration //ブロックを登録する PHP ファイル
        │       ├── register-wdl-block-01.php
        │       ├── register-wdl-block-02.php
        │       └── register-wdl-block-03.php
        └── block-dev //開発用ディレクトリ
            ├── node_modules
            ├── package-lock.json
            ├── package.json
            ├── webpack.config.js //独自に作成した拡張用の webpack.config.js
            └── src //ブロック用スクリプト(エントリーポイント)
                ├── wdl-block-01.js
                ├── wdl-block-02.js
                └── wdl-block-03.js

以下は wdl-block-02.js という2つ目のブロック用スクリプト(エントリーポイント)を追加する例です。

ブロック用のスクリプトを作成(追加)

以下のような wdl-block-02.js というブロック用のスクリプトを作成します(内容は後で編集)。

wdl-block-02.js
import { registerBlockType } from '@wordpress/blocks';
//ブロックの登録・定義
registerBlockType( 'wdl/block-02', {
  title: 'WDL Sample Block 02',
  icon: 'smiley',
  category: 'layout',
  edit: ({ className }) => {
    return <div className={ className }>Hello World No.2! (Edit2)</div>
  },
  save: () => {
    return <div>Hello World No.2! (Save2)</div>
  },
});

webpack.config.js の編集

独自に作成した webpack.config.js の entry に上記で作成したファイル wdl-block-02.js を追加します。

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const path = require('path');

module.exports = {
  ...defaultConfig,
  entry: {
    'wdl-block-01': './src/wdl-block-01.js',
    'wdl-block-02': './src/wdl-block-02.js'  //追加
  },
  output: {
    path: path.join(__dirname, '../my-blocks/build'),
    filename: '[name].js'
  }
}

関連項目:webpack 設定の拡張

ビルドを実行

ターミナルで npm run build を実行してビルドします。/my-blocks/build/ に wdl-block-02.js と wdl-block-02.asset.php が出力されます。

$ npm run build  return 

ブロックの登録ファイル(PHP)を作成

PHP 側のブロックの登録ファイル register-wdl-block-02.php を作成し /my-blocks/block-registration/ に保存します。スタイルの追加などがあれば必要に応じて後で編集します。

register-wdl-block-02.php
<?php
function wdl_block_02_enqueue() {
  アセットファイルをインクルード
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-02.asset.php'));
  //ブロック用のスクリプトを登録
  wp_register_script(
    'wdl-block-02',
    get_theme_file_uri('/my-blocks/build/wdl-block-02.js'),
    $asset_file['dependencies'],
    $asset_file['version']
  );
  //ブロックの登録
  register_block_type(
    'wdl/block-02',
    array(
      'editor_script' => 'wdl-block-02',
    )
  );
}
add_action( 'init', 'wdl_block_02_enqueue' );

functions.php の編集

functions.php で上記ブロックの登録ファイル register-wdl-block-02.php をインクルードします。

functions.php
$file_path = '/my-blocks/block-registration/';
include( get_theme_file_path($file_path.'register-wdl-block-01.php'));
//以下を追加
include( get_theme_file_path($file_path.'register-wdl-block-02.php'));

ブロックの登録ファイルの数が多くなってくると毎回登録するのは面倒なので、PHP の dir() 関数を使って指定したディレクトリからファイル名を取得して以下のように記述することもできます。

functions.php
//ブロック登録用ファイルのインクルードする処理
//テーマディレクトリからブロック登録用ファイルのあるディレクトリへのパス
$block_registration_dir = '/my-blocks/block-registration/';
//ブロック登録用ファイルのあるディレクトリへのパス
$block_registration_dir_path = get_theme_file_path($block_registration_dir);
//ファイル名を格納する配列
$block_registration_files = [];
//dir() の read() メソッドを使ってファイル名を取得
if($dir = dir($block_registration_dir_path)) {
  while(FALSE !== ($filename = $dir -> read())) {
    //is_file() でファイルのみを取得( . や .. を除外)
    if(is_file($block_registration_dir_path.$filename)) {
      $block_registration_files[] = $filename;
    }
  }
}
//ディレクトリを閉じる
$dir -> close();
//取得した配列を使ってインクルード
foreach($block_registration_files as $file) {
  include($block_registration_dir_path.$file);
}

この時点でのファイルの構成は以下のようになっています。

wp-content
└─ themes
    └─ blockX //独自テーマのディレクトリ
        ├── category.php
        ├── front-page.php
        ├── functions.php //(編集)
        ├── index.php
        │    ...
        ├── my-blocks  //ブロック用ディレクトリ
        │   ├── build
        │   │   ├── wdl-block-01.asset.php
        │   │   ├── wdl-block-01.js
        │   │   ├── wdl-block-02.asset.php //ビルドで出力される
        │   │   └── wdl-block-02.js //ビルドで出力される
        │   └── block-registration
        │       ├── register-wdl-block-01.php
        │       └── register-wdl-block-02.php //追加
        └── block-dev //開発用ディレクトリ
            ├── node_modules
            ├── package-lock.json
            ├── package.json
            ├── webpack.config.js //独自の webpack.config.js (編集)
            └── src
                ├── wdl-block-01.js
                └── wdl-block-02.js  //追加

投稿の編集画面で、新しいブロックを挿入して表示されれば、問題ありません。

上記のような手順で複数のエントリーポイント(ブロック用スクリプト)を追加することができます。

ブロック用スクリプトでスタイルを適用する場合は、別途スタイル用の設定が必要になります。スタイルの追加/複数エントリーポイントでの設定 を参照ください。

開発を始めるには npm start で開発モードにします。終了するには control + c を押します。

$ npm start  start  //開発モード

props

props は React の機能の1つで、基本的にはデータをコンポーネントに渡す方法です。別の言い方をすると、特定のコンポーネントが利用できるプロパティのリストのようなものです(React props)。

WordPress は、edit 関数と save 関数でいくつかの props を提供します。

edit 及び save 関数の引数に props を受け取ることができ、これらの関数内では props を使って className や attributes などの属性や setAttributes などのメソッドにアクセスすることができます。

どのような props があるかは、例えば edit 関数の引数に props を渡して return の前で console.log(props); を実行することで確認できます。

edit: (props) => {  // 引数に props を渡す
  console.log(props); //props をコンソールへ出力
  return <div>Hello World! (Edit)</div>
},

以下はコンソールへの出力の例です。

{name: "wdl/block-01", isSelected: false, attributes: {…}, setAttributes: ƒ, insertBlocksAfter: ƒ, …}

出力を展開すると、className などの値が確認できます。この例の場合、attributes は何も設定していないので空のオブジェクトになっています。

実際には以下のように ES6 の分割代入を使って props を変数に代入することが多いです。

edit: (props) => {
  // props を変数に分割代入
  const { className, isSelected } = props;
  // 変数を出力
  console.log('className: ' + className + ' isSelectd: ' + isSelected);
  //この例の場合 className: wp-block-wdl-block-01 isSelectd: false と出力される
  return <div>Hello World! (Edit)</div>
},

以下のように引数に受け取る際に props を変数に分割代入することもできます。

// 引数に受け取る際に props を変数に分割代入
edit: ({ className, isSelected }) => {
  // 変数を出力
  console.log('className: ' + className + ' isSelectd: ' + isSelected);
  //この例の場合 className: wp-block-wdl-block-01 isSelectd: false と出力される
  return <div>Hello World! (Edit)</div>
},

className

className プロパティはラッパー要素(ブロックの要素)のクラス名を返します。

このクラス名はブロック名の前に wp-block- を付け、名前空間セパレーターのスラッシュ( / )を - で置換して自動的に生成されます。

この例の場合、ブロック名は wdl/block-01 なのでクラス名は wp-block-wdl-block-01 になります。

また、クラス名は save 関数では自動的に追加されますが、edit 関数では追加されません。

クラス名を使ってエディター画面にもスタイルを適用する場合などは、以下のように edit 関数でブロックの要素にクラス名のプロパティ className を指定します。

import { registerBlockType } from '@wordpress/blocks';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: ({ className }) => {
    //クラス名を追加
    return <div className={ className }>Hello World! (Edit)</div>
  },
  save: () => {
    return <div>Hello World! (Save)</div>
  },
});

JSX で属性を指定を指定する場合、属性の値として JavaScript 式を埋め込むには中括弧を使用します。この場合 className は変数なので Java Script の式(評価の結果として値を返すもの)です。

スタイルの追加

ブロックにスタイルを適用する Sass のスタイルシートを追加する方法です。CSS のスタイルシートも同様の方法で追加できます。

この例では wp-scripts を使って開発環境を構築しているので、Sass はビルドの際に自動的に webpack.config.js の 設定で CSS に変換されます。

エディター用スタイルを editor.scss という名前で src フォルダに作成し読み込むと、ビルドの際に build フォルダに wdl-block-01.css という CSS ファイルが出力されます。

フロントエンド用スタイルを style.scss という名前で作成し読み込むと、ビルドの際に build フォルダに style-wdl-block-01.css という CSS ファイルが出力されます。

  • フロントエンド(及びエディター)用スタイル:style.scss → style-wdl-block-01.css
  • エディター用スタイル:editor.scss → wdl-block-01.css

以下のようなスタイルシート(Sass ファイル)を作成してブロック用スクリプトがある src フォルダに配置します。

src/style.scss(フロントエンド及びエディター用スタイル)
.wp-block-wdl-block-01 {
  background-color: green;
  color: #fff;
  padding: 2px;
}
src/editor.scss(エディター用スタイル)
.wp-block-wdl-block-01 {
  border: 3px dotted #f00;
}

スタイルシートの読み込みはブロックのスクリプトで import を使って読み込みます。

エディター画面にもクラス名を使ってスタイルを指定するので、ブロックのスクリプトの edit 関数に props を使って className 属性を追加します。

src/wdl-block-01.js
import { registerBlockType } from '@wordpress/blocks';
//スタイルシートを読み込む(インポート)
import './style.scss';
import './editor.scss';

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  edit: ({ className }) => {
    //クラス名をブロックの要素に追加
    return <div className={className}>Hello World! (Edit)</div>
  },
  save: () => {
    return <div>Hello World! (Save)</div>
  },
});

ビルドを実行

ビルドを実行して出力先ディレクトリに出力されるファイルを確認します。

$ npm run build return //ビルドを実行

ビルドの出力先のディレクトリに CSS ファイルが生成されて追加されます。

my-blocks //ブロック用ディレクトリ
  │   ├── build  //ビルドされたファイルの出力先
  │   │   ├── style-wdl-block-01.css //出力されるロントエンド及びエディター用 CSS(★)
  │   │   ├── wdl-block-01.css  //出力されるエディター用 CSS(★★)
  │   │   ├── wdl-block-01.asset.php
  │   │   └── wdl-block-01.js
  │   └── block-registration
  │       └── register-wdl-block-01.php
  └── block-dev //開発用ディレクトリ
      ├── node_modules
      ├── package-lock.json
      ├── package.json
      ├── webpack.config.js
      └── src
          ├── editor.scss //追加したエディター用 Sass(★★)
          ├── style.scss  //追加したフロントエンド及びエディター用 Sass(★)
          └── wdl-block-01.js  

PHP 側でスタイルの登録を追加

PHP 側のブロックの登録で、スタイルの登録を追加します。

wp_register_style でスタイルを登録し、register_block_type の style と editor_style に登録したスタイルを指定します。

エディター用スタイルの wp_register_style を使った登録では依存スタイルに wp-edit-block を指定しています(26行目)。

register-wdl-block-01.php
<?php
function wdl_block_01_enqueue() {
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-01.asset.php'));

  wp_register_script(
    'wdl-block-01',
    get_theme_file_uri('/my-blocks/build/wdl-block-01.js'),
    $asset_file['dependencies'],
    $asset_file['version']
  );

  //フロント&エディター用スタイル(追加)
  wp_register_style(
    'wdl-block-01-style', //ハンドル名
    //style.scss は build ディレクトリに style-wdl-block-01.css として出力される
    get_theme_file_uri('/my-blocks/build/style-wdl-block-01.css'),
    array(),
    filemtime(get_theme_file_path('/my-blocks/build/style-wdl-block-01.css'))
  );

  //エディター用スタイル(追加)
  wp_register_style(
    'wdl-block-01-editor-style', //ハンドル名
    //editor.scss は build ディレクトリに wdl-block-01.css として出力される
    get_theme_file_uri('/my-blocks/build/wdl-block-01.css'),
    array('wp-edit-blocks'),  //依存スタイルのハンドル
    filemtime(get_theme_file_path('/my-blocks/build/wdl-block-01.css'))
  );

  //ブロックタイプの登録
  register_block_type(
    'wdl/block-01',
    array(
      'editor_script' => 'wdl-block-01',
      //フロント&エディター用スタイルのハンドル名を style に指定(追加)
      'style' => 'wdl-block-01-style',
      //エディター用スタイルのハンドル名を editor_style に指定(追加)
      'editor_style' => 'wdl-block-01-editor-style',
    )
  );
}
add_action( 'init', 'wdl_block_01_enqueue' );

style で指定したスタイルシートはエディターとフロントエンドの両方でロードされます。editor_style で指定したスタイルシートは、style の後に(エディター内でのみ)ロードされます。(コードの実装 より)

そのため、style と editor_style の両方で同じセレクターを使用する場合、エディター固有のスタイルが優先されます。

この例の場合、フロントエンド側では style で指定した style-index.css が適用されて以下のように表示されます。

エディター側では style 及び editor_style で指定したスタイルが適用されて以下のように表示されます。

依存スタイル(CSS)

wp-components や wp-editor などのパッケージを利用している場合、エディター用スタイルの登録では依存スタイルのハンドルに wp-edit-blocks を指定するようです。

WordPress のブロックサンプルのエディター用スタイルの登録でも指定されています。

ハンドル名 説明
wp-edit-blocks wp-components、wp-editor、wp-block-library、及び wp-block-library-theme が依存している CSS のハンドルです。エディターのスタイルの依存関係として使用することで、それらのハンドルに関連するスタイルも確実にキューに入れられます。(Enqueueing Scripts and Styles for Gutenberg Blocks

複数エントリーポイントでの設定

複数のエントリーポイント(ブロック用スクリプト)があり、それらのファイルでスタイルを読み込んでいる場合、webpack.config.js での設定が必要になります。

wp-script のデフォルトの webpack の設定では、エディター用スタイルは以下のような MiniCSSExtractPlugin の設定により entry プロパティで指定したキーの名前の CSS ファイル([name].css)がそれぞれ出力されるようになっています。

webpack.config.js の plugins 部分抜粋
new MiniCSSExtractPlugin( { esModule: false, filename: '[name].css' } )

この例では、webpack.config.js の entry でエントリーポイント(ブロック用のスクリプト)を以下のように設定しているので、エディター用スタイルを editor.scss という名前(名前は何でも構いません)で src フォルダに作成してエントリーポイントのファイルで読み込むと、ビルドの際に build フォルダに wdl-block-01.css が出力されます。

webpack.config.js の entry 部分抜粋
entry: {
  'wdl-block-01': './src/wdl-block-01.js'
},
output: {
  path: path.join(__dirname, '../my-blocks/build'),
  filename: '[name].js'
}

また、src ディレクトリにエディター用スタイルを作成する場合、それぞれのエントリーポイントで異なる名前でスタイルシートを保存する必要があります。名前は任意の名前を付けることができます。ビルドの際には上記 MiniCSSExtractPlugin の設定によりエントリーポイントの名前が使われて生成されます。

フロントエンド用スタイルは、デフォルトではそれぞれのエントリーポイントごとに生成されるのではなく以下のように splitChunks を使って複数のエントリポイントで共通のモジュール(ファイル)を別のファイルとして出力するような設定になっています。

以下の場合、それぞれのブロック用のスクリプトで style.scss という名前でフロントエンド用スタイルを読み込むと各エントリーポイントの entry のキーをハイフンで連結した名前のファイルが生成されます。

webpack.config.js の optimization 部分抜粋
optimization: {
  splitChunks: { //複数のエントリポイントで共通のモジュールを別のファイルとして分離
    cacheGroups: {
      style: {
        test: /[\\/]style\.(sc|sa|c)ss$/, //対象のファイル(style.scss)
        chunks: 'all',
        enforce: true,
        automaticNameDelimiter: '-', //名前を連結する文字
      },
      default: false,
    },
  },

エントリーポイントが1つだけで、エントリーポイントのキーが wdl-block-01 の場合、style.scss という名前でフロントエンド用スタイルを作成し読み込むと style-wdl-block-01.css という CSS ファイルが build ディレクトリに出力されます。

もし、2つのエントリーポイント wdl-block-01.js と wdl-block-02.js で共通のフロントエンド用スタイル style.scss を読み込んでいると、style-wdl-block-01-wdl-block-02.css という名前が連結された CSS ファイルが build ディレクトリに出力されます。

そのため、デフォルトのままだとスタイルを読み込む必要のあるエントリーポイントを作成する度に、参照するスタイルシートの名前が変わってしまうため問題があります。

また、style.scss 以外の名前でフロントエンド用スタイルを作成して読み込んだ場合は、MiniCSSExtractPlugin の設定によりエディター用スタイルとバンドルされて出力されてしまい、エディター用とフロントエンド用に分けることができません。

解決策の1つとしては、独自の webpack.config.js に各エントリーポイントのスタイル用に splitChunks の設定を追加します。

例えば、以下のような2つのエントリーポイント wdl-block-01.js と wdl-block-02.js があり、

entry: {
  'wdl-block-01': './src/wdl-block-01.js', //エントリーポイント
  'wdl-block-02': './src/wdl-block-02.js', //エントリーポイント
},
output: {
  path: path.join(__dirname, '../my-blocks/build'),
  filename: '[name].js'
},

それぞれのエントリーポイントでスタイルを設定して読み込む構成がある場合、

 src
  ├── wdl-block-01.js //エントリーポイント(ブロック用のスクリプト)
  ├── editor-01.scss //wdl-block-01.js のエディター用スタイル
  ├── style-01.scss //wdl-block-01.js のフロント用スタイル
  ├── wdl-block-02.js //エントリーポイント(ブロック用のスクリプト)
  ├── editor-02.scss //wdl-block-02.js のエディター用スタイル
  └── style-02.scss //wdl-block-02.js のフロント用スタイル

独自の webpack.config.js に以下のような splitChunks の設定を追加します。

以下ではフロントエンド用スタイルごとに cacheGroups に設定を追加し、別ファイルとして出力するようにしています。

webpack.config.js の optimization 部分抜粋
optimization: {
  splitChunks: { //複数のエントリポイントで共通のモジュールを別のファイルとして分離
    cacheGroups: {
      style_01: {
        test: /[\\/]style-01\.(sc|sa|c)ss$/, //対象のファイル style-01.scss
        chunks: 'all',
        enforce: true,
        name: 'style-wdl-block-01' //出力されるファイル名
      },
      style_02: {
        test: /[\\/]style-02\.(sc|sa|c)ss$/, //対象のファイル style-02.scss
        chunks: 'all',
        enforce: true,
        name: 'style-wdl-block-02' //出力されるファイル名
      },
      default: false,
    },
  },
},

上記及び MiniCSSExtractPlugin の設定により、ビルドすると以下のようなファイルが出力されます。

build //ビルド先ディレクトリ
├── wdl-block-01.asset.php
├── wdl-block-01.js
├── wdl-block-01.css //editor-01.scss が変換されたファイル
├── style-wdl-block-01.css  //style-01.scss が変換されたファイル
├── wdl-block-02.asset.php
├── wdl-block-02.js
├── wdl-block-02.css //editor-02.scss が変換されたファイル
└── style-wdl-block-02.css  //style-02.scss が変換されたファイル 

以下は上記の場合の独自の webpack.config.js の例です。

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const path = require('path');

module.exports = {
  ...defaultConfig,  //既存の設定を展開
  entry: {
    'wdl-block-01': './src/wdl-block-01.js',
    'wdl-block-02': './src/wdl-block-02.js',
  },
  output: {
    path: path.join(__dirname, '../my-blocks/build'),
    filename: '[name].js'
  },

  optimization: {
    splitChunks: { //複数のエントリポイントで共通のモジュールを別のファイルとして分離
      cacheGroups: {
        style_01: {
          test: /[\\/]style-01\.(sc|sa|c)ss$/, //style-01.scss を対象
          chunks: 'all',
          enforce: true,
          name: 'style-wdl-block-01'
        },
        style_04: {
          test: /[\\/]style-02\.(sc|sa|c)ss$/, //style-02.scss を対象
          chunks: 'all',
          enforce: true,
          name: 'style-wdl-block-02'
        },
        default: false,
      },
    },
  },
}

追加したブロックのスクリプトを読み込む PHP ファイルにスタイルの登録を追加します。

register-wdl-block-02.php
<?php
function wdl_block_04_enqueue() {
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-02.asset.php'));

  wp_register_script(
    'wdl-block-02',
    get_theme_file_uri('/my-blocks/build/wdl-block-02.js'),
    $asset_file['dependencies'],
    $asset_file['version']
  );

  //フロント&エディター用スタイル
  wp_register_style(
    'wdl-block-02-style', //ハンドル名
    //style-02.scss は build フォルダに style-wdl-block-02.css として出力される
    get_theme_file_uri('/my-blocks/build/style-wdl-block-02.css'),
    array(),
    filemtime(get_theme_file_path('/my-blocks/build/style-wdl-block-02.css'))
  );

  //エディター用スタイル
  wp_register_style(
    'wdl-block-02-editor-style', //ハンドル名
    //editor-02.scss は build フォルダに wdl-block-02.css として出力される
    get_theme_file_uri('/my-blocks/build/wdl-block-02.css'),
    array('wp-edit-blocks'),  //依存スタイルのハンドル
    filemtime(get_theme_file_path('/my-blocks/build/wdl-block-02.css'))
  );

  //ブロックタイプの登録
  register_block_type(
    'wdl/block-02',
    array(
      'editor_script' => 'wdl-block-02',
      //フロント&エディター用スタイルのハンドル名を style に指定
      'style' => 'wdl-block-02-style',
      //エディター用スタイルのハンドル名を editor_style に指定
      'editor_style' => 'wdl-block-02-editor-style',
    )
  );
}
add_action( 'init', 'wdl_block_04_enqueue' );

この例の場合、2つのエントリーポイント wdl-block-01.js と wdl-block-02.js でそれぞれエディター用及びフロントエンド用スタイルを読み込んでいると以下のような構成になります。

wp-content
└─ themes
    └─ blockX //独自テーマのディレクトリ
        ├── category.php
        ├── front-page.php
        ├── functions.php //ファイルを追加する際は編集
        ├── index.php
        │    ...
        ├── my-blocks  //ブロック用ディレクトリ
        │   ├── build  //ビルドで自動的に生成されるファイル
        │   │   ├── wdl-block-01.asset.php
        │   │   ├── wdl-block-01.css
        │   │   ├── wdl-block-01.js
        │   │   ├── wdl-block-02.asset.php
        │   │   ├── wdl-block-02.css
        │   │   ├── wdl-block-02.js
        │   │   ├── style-wdl-block-01.css
        │   │   └── style-wdl-block-02.css
        │   └── block-registration  //ブロックを登録する PHP ファイル
        │       ├── register-wdl-block-01.php
        │       └── register-wdl-block-02.php
        └── block-dev //開発用ディレクトリ
            ├── node_modules
            ├── package-lock.json
            ├── package.json
            ├── webpack.config.js //ファイルを追加する際は編集
            └── src   //ブロック用スクリプト(エントリーポイントと Sass)
                ├── editor-01.scss
                ├── editor-02.scss
                ├── style-01.scss
                ├── style-02.scss
                ├── wdl-block-01.js //エントリーポイント
                └── wdl-block-02.js //エントリーポイント 

ブロックスタイルの追加

registerBlockType 関数の第2パラメータ(ブロックのプロパティ)の1つ styles を使うと、ブロックスタイルを登録することができます。

以下のように registerBlockType の第2パラメータに styles を追加してブロックスタイルを登録します。

それぞれのブロックスタイルはそのスタイルの名前とラベル(表示名)を指定します。名前(name)はそのスタイルが選択されると追加されるクラス名 is-style-xxxx の xxxx の部分に使用されます。

デフォルトのスタイルには isDefault: true を指定します。

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

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  //ブロックスタイルを登録
  styles: [
    {
      name: 'default',
      label: '角丸', //サイドバーに表示される名前(ラベル)
      isDefault: true  //デフォルト
    },
    {
      name: 'squared',
      label: '角丸なし'
    },
  ],
  edit: ({ className }) => {
    return <div className={ className }>Hello World! (Edit)</div>
  },
  save: () => {
    return <div>Hello World! (Save)</div>
  },
});

ブロックスタイルはブロックのラッパーにクラス名(is-style-xxxx)を追加することで動作します。この例では style.scss に以下のようなクラスを追加しています。

.is-style-default
{
  border-radius: 10px;
}

.is-style-squared
{
  border-radius: 0px;
}

ブロックを挿入すると、サイドバーのブロックタブに登録したブロックスタイルが表示され、スタイルが選択できるようになっていています。出力を見るとデフォルトのブロックスタイルを表すクラス is-style-default が付与されているのが確認できます。

スタイルの選択で「角丸なし」を選択するとラッパー(ブロックの外側の要素)に is-style-squared というクラスが付与されます。

また、デフォルトスタイルのプルダウンがあり、こちらでもデフォルトのスタイルを設定できるようになっています。

参考ページ:ブロックの登録

attributes

attributes はブロックに必要なデータ(及びその取得方法)を定義することができるオブジェクトで、registerBlockType() 関数のパラメータとして設定します。

attributes は React の state によく似ています。 setAttributes と呼ばれるよく似た更新メソッドもあります。

設定した attributes(属性)は、 edit 及び save 関数で props 経由で取得して利用できます。

属性には以下のようなプロパティが設定可能です。※ type プロパティのみ必須で、他はオプションです。

プロパティ 説明
type(必須) 属性に保存するデータの型を表し、以下のいずれかの値を取る必要があります。
  • null
  • boolean
  • object
  • array
  • number
  • string
  • integer
default 属性の初期値(デフォルトの値)。指定しない場合、その属性の値は null になります。
source 保存された投稿コンテンツの HTML からどのようにブロックの属性値を取り出すか(抽出するデータの元)を定義します。以下の値を指定できます。
  • children(子ノードから)
  • html(内部の HTML から)
  • text(内部のテキストから)
  • attribute(属性の値から)
  • query(値の配列を取り出す)
  • meta(投稿のメタ情報から)
selector 抽出の対象とする要素をセレクタで指定します。指定がない場合、source プロパティの定義はブロックのルートノードに対して実行されます。指定がある場合は、ブロック内に含まれる selector で指定された要素に対して実行されます。HTML タグのほか、クラスや id 属性などのセレクタ(例:p.sample)を使って指定できます。
attribute source プロパティで attribute を指定した場合に、対応する属性(href や src など)を指定します。
meta source プロパティで meta を指定した場合に、対応するメタキーを指定します。
multiline RichText 内から複数のタグ名に合致する内部 HTML を取り出す場合に対象のタグを指定します。

属性(attributes)の source プロパティを指定しない場合、属性は以下のようなブロックのコメントデリミッター に JSON 形式保存され、ロード時に読み出されます。

source プロパティを設定すると、ブロックのデータをコメントブロックからではなく、(save 関数で)保存された属性から取得することになります。

例えば、以下の場合、selector で指定した div 要素の、source で指定した html から、type で指定した string 型としてデータを取得して、myText という属性に保存するというような意味になります。

attributes: {
  myText: {
    type: 'string',  //属性値のデータの型
    source: 'html',  //抽出するデータの元
    selector: 'div'  //対象の要素
  },
  ...

以下の場合は、selector で指定した img 要素の、source で指定した attribute(要素の属性)の、attribute で指定した src 属性から、type で指定した string 型としてデータを取得して、url という属性に保存するというような意味になります。

attributes: {
  url: {
    type: 'string',  //属性値のデータの型
    source: 'attribute', //抽出するデータの元
    selector: 'img',  //対象の要素
    attribute: 'src',  //対象の属性
  },
  ...

属性の値の取得

属性はブロックの edit 及び save 関数で props 経由で取得できます。

props.attributes.属性の名前

以下は editText と saveText という2つの属性を定義して props 経由で取得して出力する例です。

この場合、default プロパティに初期値が設定されているので、その値がそれぞれの属性の値として出力されて表示されます。

また、JSX を複数行に分けて記述する場合は括弧 ( ) で囲みます。

registerBlockType( 'wdl/block-01', {
  title: 'WDL Sample Block 01',
  icon: 'smiley',
  category: 'layout',
  //属性を定義
  attributes: {
    editText: {
      type: 'string',  //文字列
      default: 'Edit 用のテキスト' //初期値
    },
    saveText: {
      type: 'string',  //文字列
      default: 'Save 用のテキスト' //初期値
    },
  },
  edit: ( props ) => {
    //props 経由で属性を取得
    return (
      <div className={ props.className }>
        Hello World! ({ props.attributes.editText })
        // Hello World! (Edit 用のテキスト) と表示される
      </div>
    )
  },
  save: ( props ) => {
    // props 経由で属性を取得
    return (
      <div>
        Hello World! ({ props.attributes.saveText })
        // Hello World! (Save 用のテキスト) と表示される
      </div>
    )
  },
});

以下は ES6 の分割代入を使って記述した例で、こちらの書き方がよく使われます。

edit: ( props ) => {
  //分割代入を使って変数に代入
  const { className, attributes: { editText }} = props;
  return (
    <div className={ className }>
      Hello World! ({ editText })
    </div>
  )
},
save: ( props ) => {
  //分割代入
  const { attributes: { saveText }} = props;
  return (
    <div>
      Hello World! ({ saveText })
    </div>
  )
},


/*または以下のように引数に受け取る際に分割代入する書き方も良く使われます*/
edit: ( { className, attributes: { editText }} ) => {
  return (
    <div className={ className }>
      Hello World! ({ editText })
    </div>
  )
},
save: ( { attributes: { saveText }} ) => {
  return (
    <div>
      Hello World! ({ saveText })
    </div>
  )
},

属性の値の更新 setAttributes()

属性を更新するには、props 経由でアクセスできる setAttributes() メソッドを使用します。

setAttributes() は更新する属性のオブジェクトを受け取り、その値を更新します。

props.setAttributes({ 属性の名前: 属性の値 });

setAttributes() は React の setState() に対応するメソッドで同じように動作しますが、違いは、React の state がそのコンポーネントにローカルであるのに対して、attributes はコンポーネントの外部のデータとして保存されるようです。

以下は editText という属性の値を 'New Text!' に更新する例です。※ 実際には onChange などの何らかのアクションに伴い setAttributes() を使い値を更新し、通常は以下のような使い方はしません。

attributes: {
  editText: {
    type: 'string',  //文字列
    default: 'Edit 用のテキスト' //初期値
  },
},

edit: ( props ) => {
  //setAttributes で editText の値を更新
  props.setAttributes({ editText: 'New Text' });
  return (
    <div className={ props.className }>
      Hello World! ({ props.attributes.editText })
    </div>
  )
},

以下のように分割代入を使って記述できます。

edit: ( props ) => {
  //分割代入
  const { className, attributes: { editText }, setAttributes} = props;
  setAttributes({ editText: 'New Text' });
  return (
    <div className={ className }>
      Hello World! ({ editText })
    </div>
  )
},

//または 引数に受け取る際に分割代入
edit: ( { className, attributes: { editText }, setAttributes} ) => {
  setAttributes({ editText: 'New Text' });
  return (
    <div className={ className }>
      Hello World! ({ editText })
    </div>
  )
},

WordPress のコンポーネント

WordPress ではブロックを作成する際に利用可能なコンポーネントが含まれるパッケージを提供しています。例えば、テキスト入力、リッチテキスト入力、ドロップダウン、トグル、チェックボックス、ボタン、メディアアップローダー、カラーピッカーなどのコンポーネントがあります。

以下はブロックに使用する代表的なパッケージの概要です。

パッケージ 概要
blocks ブロックに使用されるコンポーネントと機能へのアクセスを提供する wp.blocks を参照します。registerBlockType などが含まれています。@wordpress/blocks
components ほとんどの UI 入力コンポーネントはこのパッケージにあります。例としては、さまざまなテキスト入力、select ボックス、チェックボックス、ラジオボタン、ドラッグ可能なコンポーネント、ボタン、カラーピッカー、日付ピッカーなどがあります。また、エディターのブロックツールバーや設定サイドバー(InspectorControls)のコンテンツに使用できる UI コンポーネントもこの中にあります。@wordpress/components
editor
block-editor
RichText や画像/メディアアップローダー関連のコンポーネント、ツールバーまたはカスタムインスペクター(サイドバー)パネルを追加するためのコンポーネントなどがあります。但し、WordPress 5.3 以降 editor のいくつかのコンポーネント(RichText など)は block-editor に移されています。@wordpress/editor @wordpress/block-editor
element 要素の作成に使用できる React の上の抽象化レイヤーである wp.element へのアクセスを提供します。この中には React コンポーネントに対応する WordPress コンポーネントがあります。例えば、React.Component に対応する Component や React.Fragment に対応する Fragment、React.createElement に対応する createElement などが含まれています。@wordpress/element
i18n 必須ではありませんが、JavaScript 内で使用できる国際化機能(多言語化機能)へのアクセスを提供します。@wordpress/i18n(日本語)

パッケージの情報は Block Editor HandbookPackage Reference の下にありますが、公式の Gutenberg Github リポジトリの gutenberg/packages/ の方がより最新の情報があるようです。

例えば、RichText というコンポーネントは /packages/block-editor/src/components/rich-text/ にあります。

コンポーネントを使う

WordPress のコンポーネントを使うには、目的のコンポーネントが含まれるパッケージを探して、そのパッケージからコンポーネントをインポートします。

例えば、Button コンポーネントは components パッケージにあるので、@wordpress/components から Button をインポートします。

import { Button } from '@wordpress/components';

TextControl/TextareaControl

components パッケージにある TextControl と TextareaControl というユーザがテキストを入力することができるコンポーネントを使う例です。TextControl は input 要素を、TextareaControl は textarea 要素を使ったコンポーネントです。

取り敢えず2つのコンポーネントを edit 関数に記述して表示してみます。

最初のブロックの例では div 要素を使いましたが、この例ではコンポーネントを使うので、通常のタグの代わりにコンポーネント型(TextControl や TextareaControl)を指定します(コンポーネントは大文字で始まります)。また、これらのコンポーネントは空要素なので /> で閉じて終了タグは省略できます。

以下コードはまだ後で追加のコードが必要ですが一応表示することはできます。

wdl-block-02.js
import { registerBlockType } from '@wordpress/blocks';
//TextControl と TextareaControl コンポーネントを components パッケージからインポート
import { TextControl, TextareaControl } from '@wordpress/components';

registerBlockType( 'wdl/block-02', {
  title: 'WDL Sample Block 02',
  icon: 'smiley',
  category: 'layout',

  edit: () => {
    return (
      <div>
        Text input:
        <TextControl />
        Textarea:
        <TextareaControl />
      </div>
    );
  },
  //フロントエンド側は後で変更
  save: () => {
    return <div>Hello World No.2! (Save2)</div>
  },
});

ブロックを挿入すると以下のように表示されます。

コンポーネントのプロパティ

TextControlTextareaControl コンポーネントには label というプロパティがあり、label プロパティに文字列を指定するとラベル(label 要素)として表示されます。

コンポーネントのプロパティは HTML の属性のような形式(プロパティ名=値)で指定します。値が文字列の場合は引用符で囲み、JavaScript の式の場合(真偽値を含む)は { } で囲みます。

edit: () => {
  return (
    <div>
      <TextControl label="Text input:"  />
      <TextareaControl label="Textarea:" />
    </div>
  );
},

どのようなプロパティがあるかは、それぞれのコンポーネントのページで確認できます。例えば TextControl には以下のようなプロパティがあります。

TextControl のプロパティ(一部抜粋)
プロパティ 説明
label このプロパティを指定すると、指定された値の文字列を使って label 要素が出力されます。
value この要素の値(表示される文字列)
type レンダリングする入力要素のタイプ(デフォルトは text)
onChange 入力の値が変更されたら呼び出される関数(イベントハンドラ)

コードの追加

この時点ではまだ必要なプロパティを設定していないので、文字を入力するとエラーがコンソールに表示され、入力した文字を保存することはできません。

入力された値は attributes プロパティを使って保存します。また、入力に変更がある場合は保存した内容を更新する必要があります。

attributes プロパティに TextControl と TextareaControl に入力された値を保持するための属性をそれぞれ設定しています。

それぞれのコンポーネントに表示する値(value 属性の値)は props 経由で属性の値(例:props.attributes.myTextInput)を設定します。最初は default で指定した初期値の空文字ですが、値が入力されると onChange イベントに設定した setAttributes 関数を使って値を入力された値に更新します。

save 関数では、TextControl の値を属性から取得して h3 要素で、TextareaControl の値を属性から取得して div 要素でレンダリングしています。

wdl-block-02.js
import { registerBlockType } from '@wordpress/blocks';
import { TextControl, TextareaControl } from '@wordpress/components';

registerBlockType( 'wdl/block-02', {
  title: 'WDL Sample Block 02',
  icon: 'smiley',
  category: 'layout',
  //入力された値を保存するための属性を追加
  attributes: {
    //TextControl の値を保持する属性
    myTextInput: {
      type: 'string',
      default: ''
    },
    //TextareaControl の値を保持する属性
    myTextAreaInput: {
      type: 'string',
      default: ''
    },
  },

  edit: (props) => {
    // props を変数に分割代入
    const { attributes, setAttributes } = props;
    return (
      <div>
        <TextControl
          label="Text input:"
          //値は属性から取得
          value={ attributes.myTextInput }
          //onChange イベントで setAttributes を使って値を更新
          onChange={ (newText) => setAttributes({ myTextInput: newText }) }
        />
        <TextareaControl
          label="Textarea:"
          value={ attributes.myTextAreaInput }
          onChange={ (newText) => setAttributes({ myTextAreaInput: newText }) }
        />
      </div>
    );
  },

  save: (props) => {
    const { attributes } = props;
    //値はそれぞれの属性から取得
    return (
      <div>
        <h3>{attributes.myTextInput}</h3>
        <div>{attributes.myTextAreaInput}</div>
      </div>
    )
  },
});

以下のように別途イベントハンドラを定義することもできます。また、以下ではぞれぞれの属性(myTextInput, myTextAreaInput)も分割代入で変数に取得しています。

edit 関数と save 関数の抜粋
edit: (props) => {
  //attributes プロパティのそれぞれの属性も分割代入で変数に取得
  const { attributes:{myTextInput, myTextAreaInput}, setAttributes } = props;
  //onChange イベントハンドラ
  const onChangeText = ( newText ) => {
    setAttributes( { myTextInput: newText } );
  };
  //onChange イベントハンドラ
  const onChangeTextArea = ( newText ) => {
    setAttributes( { myTextAreaInput: newText } );
  };
  return (
    <div>
      <TextControl
        label="Text input:"
        //値は属性から取得
        value={ myTextInput }
        //onChange イベント(イベントハンドラを指定)
        onChange={ onChangeText }
      />
      <TextareaControl
        label="Textarea:"
        value={ myTextAreaInput }
        onChange={ onChangeTextArea }
      />
    </div>
  );
},

save: (props) => {
  //attributes プロパティのそれぞれの属性も分割代入で変数に取得
  const { attributes:{myTextInput, myTextAreaInput} } = props;
  //値はそれぞれの属性から取得
  return (
    <div>
      <h3>{myTextInput}</h3>
      <div>{myTextAreaInput}</div>
    </div>
  )
},

※ この変更を行うと、以下のように「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示され、以下のようにブロックが壊れてしまいます。

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

投稿に更新したブロック挿入すると以下のように表示されます。

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

edit 関数では、ユーザーが入力する方法をレンダリングし、現在の値が表示されるようにして、値が変更されるたびに更新するようにします。

上記の例の場合、現在の値(コンポーネントの value 属性)は attributes の属性の値を指定し、onChange イベントを使って値が変更されるたびに、setAttributes() で値を更新します。

save 関数では、保存した属性(attributes)を抽出し、必要に応じて出力をレンダリングします。

RichText

RichText は、太字、斜体、リンクなどのテキストフォーマットをサポートする拡張されたテキストエリアを提供するコンポーネントです。

参考サイト:RichText リファレンス

以下は RichText コンポーネントを使った編集領域(入力エリア)が1つのシンプルなブロックの例です。

RichText は block-editor パッケージにあるので @wordpress/block-editor からインポートします。

TextControl/TextareaControl 同様、入力された値を保持する属性(attributes)を設定し、値が変更されたら onChange イベントで setAttributes を使って値を更新します。

※大切な点は save 関数で RichText のコンテンツを正しく保存するためには、RichText.Content を使う必要があることです。

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

registerBlockType( 'wdl/block-03', {
  title: 'WDL Sample Block 03',
  icon: 'smiley',
  category: 'layout',
  //入力された値を保存するための属性 myContent を設定
  attributes: {
    //RichText の値を保持する属性
    myContent: {
      type: 'string',
      default: ''
    },
  },

  edit: ( {className, attributes: { myContent }, setAttributes } ) => {
    return (
      <RichText
        className = {className}
        //値は attributes に設定した属性 myContent の値を設定
        value={ myContent }
        //onChange イベントで setAttributes を使って値(属性 myContent)を更新
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
      />
    );
  },

  save: ( {attributes: {myContent }} ) => {
    //save 関数では RichText.Content を使います
    return (
      <RichText.Content
        //値は属性から取得
        value={ myContent }
      />
    )
  },
});

上記のブロックを投稿に挿入すると、以下のように表示されます。以下の場合、「RichText サンプル」と入力後、return キーを押して改行して「改行」と入力しています。

以下はフロントエンド側の表示です。この例の場合、タグを指定する tagName プロパティを設定していないので、save 関数による出力は入力された内容がそのまま出力され、HTML タグでは囲まれていません。

プロパティ

RichText コンポーネントでは、value や onChange 以外にもプロパティが用意されています。

RichText コンポーネント プロパティ(一部抜粋)
プロパティ 説明
value RichText コンポーネントに表示される編集可能な値。通常は属性(attributes)に保存して、変更があれば setAttributes で更新
onChange value が変更された際に呼び出される関数(イベントハンドラ)を設定します。
tagName 入力エリアの HTML タグ(要素)を指定できます。但し、インライン要素はサポートされていません。
multiline デフォルトでは、Enter(return)キーを押すと改行が挿入されますが、multiline プロパティに true または 'p' を指定して Enter キーで新しい段落を作成することができます。boolean の他にタグ名を指定することで、Enter キーを押すとその要素を子要素として生成することができます。
placeholder プレースホルダーテキストを表示することができます。
keepPlaceholderOnFocus 選択(フォーカス)された際にもプレースホルダーテキストを表示する場合は true を指定します(文字が入力されると非表示になります)。デフォルトは false
formattingControls 許可するフォーマットのみを配列で指定することで制限することができます。例:[ 'bold', 'italic' ] (太字と斜体のみを許可)。bold, italic, link, strikethrough, image などを指定できます。空の配列を指定するとフォーマットを許可しません。
allowedFormats 許可するフォーマットだけを配列で指定することができます。指定する値は 'core/xxxx' の形式で指定します。例:[ 'core/bold', 'core/link' ] (太字とリンクのみを許可)

tagName プロパティ

以下は tagName プロパティを使って RichText の入力エリアを h3 要素としてレンダリングする例です。

基本的には edit 関数及び save 関数の両方に同じ要素を tagName プロパティで指定します。

edit: ( {className, attributes: { myContent }, setAttributes } ) => {
  return (
    <RichText
      className={className}
      value={ myContent }
      onChange={ (newContent) => setAttributes({ myContent: newContent }) }
      // h3 要素として表示
      tagName='h3'
    />
  );
},

save: ( {attributes:  {myContent }} ) => {
  //save 関数では RichText.Content を使います
  return (
    <RichText.Content
      value={ myContent }
      // h3 要素として表示
      tagName='h3'
    />
  )
},

multiline プロパティ

デフォルトでは、Enter(return)キーを押すと改行が挿入されますが、multiline プロパティを設定して Enter キーで新しい段落を作成することができます。

以下は入力エリアを div 要素として表示して、multiline プロパティに true または 'p' を指定して Enter キーで新しい段落(p 要素)を作成するようにした例です。

edit: ( {className, attributes: { myContent }, setAttributes } ) => {
  return (
    <RichText
      className={className}
      value={ myContent }
      //onChange イベントで setAttributes を使って値を更新
      onChange={ (newContent) => setAttributes({ myContent: newContent }) }
      // div 要素として表示
      tagName='div'
      //true または 'p' を指定して Enter キーで新しい段落(p 要素)を生成
      multiline={ true } //または 'p'
    />
  );
},

save: ( {attributes:  {myContent }} ) => {
  //save 関数では RichText.Content を使います
  return (
    <RichText.Content
      //値は属性から取得
      value={ myContent }
      // div 要素として表示
      tagName='div'
    />
  )
},

multiline プロパティには boolean の他にタグ名を指定することで、Enter キーを押すと指定した要素を子要素として生成することができます。例えば、以下は li を指定して、Enter キーを押すと li 要素を生成する例です。この例の場合、入力エリアのタグ tagName は ol 要素を指定しています。

edit: ( {className, attributes: { myContent }, setAttributes } ) => {
  return (
    <RichText
      className={className}
      value={ myContent }
      onChange={ (newContent) => setAttributes({ myContent: newContent }) }
      // ol 要素として表示
      tagName='ol'
      // 'li' を設定して Enter キーで新しい li 要素を作成
      multiline= 'li'
    />
  );
},

save: ( {attributes:  {myContent }} ) => {
  return (
    <RichText.Content
      value={ myContent }
      // ol 要素として表示
      tagName='ol'
    />
  )
},
複数の RichText コンポーネント

以下は2つの RichText コンポーネントを組み合わせてタイトルとコンテンツを入力できるブロックを作成する例です。それぞれの RichText コンポーネントどとに入力された値を保存する属性(attributes プロパティ)を設定します。

attributes プロパティではタイトルとコンテンツに入力された値を保存する属性をそれぞれに設定します。

edit 関数と save 関数では、タイトルとコンテンツのコンポーネントを div 要素でラップしています。

wdl-block-03.js
import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';

registerBlockType( 'wdl/block-03', {
  title: 'WDL Sample Block 03',
  icon: 'smiley',
  category: 'layout',
  //入力された値を保存するための属性(myTitle と myContent)を設定
  attributes: {
    //タイトル(h3)の内容を保持するための属性
    myTitle: {
      type: 'string',
      default: ''
    },
    //コンテンツ(div)の内容を保持するためのの属性
    myContent: {
      type: 'string',
      default: ''
    },
  },

  edit: ( {className, attributes: { myTitle, myContent }, setAttributes } ) => {
    return (
      <div className={className} >
        <RichText
          //値は属性から取得
          value={ myTitle }
          //onChange イベントで setAttributes を使って値を更新
          onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
          // h3 要素として表示
          tagName='h3'
          //プレースホルダーテキストを表示
          placeholder= 'タイトルを入力'
          //フォーカスされた際も文字が入力されるまでプレースホルダーテキストを表示
          keepPlaceholderOnFocus={ true }
        />
        <RichText
          //値は属性から取得
          value={ myContent }
          //onChange イベントで setAttributes を使って値を更新
          onChange={ (newContent) => setAttributes({ myContent: newContent }) }
          // div 要素として表示
          tagName='div'
          // 'p' を設定して Enter キーで新しい段落(p 要素)を生成
          multiline= 'p'
          //プレースホルダーテキストを表示
          placeholder= '文章を入力'
          //フォーカスされた際も文字が入力されるまでプレースホルダーテキストを表示
          keepPlaceholderOnFocus={ true }
        />
      </div>
    );
  },

  save: ( {attributes: {myTitle, myContent }} ) => {
    //save 関数では RichText.Content を使います
    return (
      <div>
        <RichText.Content
          //値は属性から取得
          value={ myTitle }
          // h3 要素として表示
          tagName='h3'
        />
        <RichText.Content
          //値は属性から取得
          value={ myContent }
          // div 要素として表示
          tagName='div'
        />
      </div>
    )
  },
});

作成したブロックを投稿に挿入すると、エディター画面では以下のように表示されます。

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

エディター画面でコードエディターに切り替えると、この例では属性(attributes)の source プロパティを指定していないので、属性(myTitle と myContent)の値がコメントブロック(デリミタ)に JSON 形式で保存されているのが確認できます。

source と selector

属性(attributes)の source プロパティは、(save 関数で)保存された投稿のコンテンツから特定の属性値を抽出する方法(抽出するデータのソース)を定義します。

公式ページ:属性

selector プロパティは、抽出の対象とする要素をセレクタで指定します。指定がない場合、source プロパティの定義はブロックのルートノードに対して実行されます。

また、source プロパティを指定しない場合、属性は以下のようなブロックのコメントブロック(デリミタ)に JSON 形式で保存され、ロード時に読み出されます。

source プロパティを設定すると、ブロックのデータをコメントブロックからではなく、(save 関数の出力の)属性から取得するようになります(属性の値はコメントブロックには保存されません)。

以下は前述の例のそれぞれの属性に source と selector プロパティの設定を追加した例です。

この例の場合、selector を指定する際に単に h3 や div だけではソースを特定できないため、edit 関数と save 関数で h3 及び div 要素としてレンダリングされるコンポーネントにクラスを指定します。

属性 myTitle では source に html を、selector に .myRichTextTitle(または h3.myRichTextTitle)を指定します。

属性 myContent では source に children を、selector に .myRichTextContent(または div.myRichTextContent)を指定します。

※ myContent では source に children を指定しているので、type を string から array に変更します。

import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';

registerBlockType( 'wdl/block-03', {
  title: 'WDL Sample Block 03',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myTitle: {
      //.myRichHeading の HTML から string を抽出
      type: 'string',
      default: '',
      //内部の HTML
      source: 'html',
      //クラスを使ったセレクタを指定
      selector:'.myRichTextTitle'
    },
    myContent: {
      //.myRichTextContent の子ノードから array を抽出
      type: 'array',  //array に変更
      default: '',
      //子ノード
      source: 'children',
      //クラスを使ったセレクタを指定
      selector:'.myRichTextContent'
    },
  },

  edit: ( {className, attributes: { myTitle, myContent }, setAttributes } ) => {
    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= '文章を入力'
          keepPlaceholderOnFocus={ true }
        />
      </div>
    );
  },

  save: ( {attributes: {myTitle, myContent }} ) => {
    //save 関数では RichText.Content を使います
    return (
      <div>
        <RichText.Content
          //クラスを追加
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          //クラスを追加
           className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
      </div>
    )
  },
});

source と selector は適切に指定しないと、うまく機能しないことがあります。

例えば、上記の場合、以下のように selector でクラスを指定しないとページを再読み込みするとエラーになります。

//うまく機能しない設定例
myTitle: {
  type: 'string',
  default: '',
  source: 'html',
  //この例の場合、以下では NG
  selector:'h3'
},
myContent: {
  type: 'array',
  default: '',
  source: 'children',
  //この例の場合、以下では NG
  selector:'div'
},

ブロックでは入力及び更新が可能すが、更新後ページを再読み込みすると以下のようなブロックが壊れている表示になります。

また、例えば以下のように source に children を指定して type を string にするとエラーになります。

//うまく機能しない設定例(2)
myContent: {
  // array を指定すべき
  type: 'string',
  default: '',
  source: 'children',
  selector:'.myRichTextContent'
},

コンソールに表示されるエラーを見ると原因が予想できる場合があります。

この例の場合は、source を html に変更して type を string にしても問題なく機能します。

//これは OK
myContent: {
  //.myRichTextContent の html から string を抽出
  type: 'string',   // string に変更
  default: '',
  //children から html に変更
  source: 'html', // html に変更
  selector:'.myRichTextContent'
},

SelectControl

SelectControl は <select> 要素を使ったコンポーネントで、components パッケージにあります。

以下は SelectControl コンポーネントを使って、セレクトメニューで選択された値を元にクラスを生成してスタイルを適用できるようにする例です。

セレクトメニューをクリックするとオプションが表示されます。

以下がブロック用のスクリプトです。

SelectControl は components パッケージにあるので @wordpress/components からインポートします。

スタイルを適用するので、フロントエンド(及びエディター)用とエディター用スタイルを用意してインポートします。

また、SelectControl で選択された値を保存するための属性 myStyle と、RichText コンポーネントに入力された値を保持する属性 myContent を設定し、値が変更されたらそれぞれの onChange イベント(プロパティ)で setAttributes を使って値を更新します。

wdl-block-04.js
import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';
// SelectControl を components パッケージからインポート
import { SelectControl } from '@wordpress/components';
import './style-04.scss';  //フロントエンド(及びエディター)用スタイルのインポート
import './editor-04.scss';  //エディター用スタイルのインポート

registerBlockType( 'wdl/block-04', {
  title: 'WDL Sample Block 04',
  icon: 'smiley',
  category: 'layout',
  //入力された値を保存するための属性(myStyle と myContent)を設定
  attributes: {
    //SelectControl で選択された値を保存する属性
    myStyle: {
      type: 'string',
      default: 'plain'  //初期値(デフォルト値)
    },
    // RichText の入力内容(value)を保存する属性
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
  },

  edit: ( {className, attributes: { myStyle, myContent }, setAttributes } ) => {
    return (
      <div className={className} >
        <SelectControl
          label="スタイル"
          value={myStyle}
          labelPosition='bottom'
          options={[
            {label: "スタイルを選択", value: null, disabled: true},
            {label: "Plain", value: 'plain'},
            {label: "Blue", value: 'blue'},
            {label: "Green", value: 'green'},
            {label: "Yellow", value: 'yellow'},
          ]}
          //setAttributes を使って選択された値で属性 myStyle の値を更新
          onChange={ (newVal) => setAttributes({ myStyle: newVal }) }
        />
        <RichText
          //SelectControl で選択された値(myStyle)を使ってクラスを追加
          className={`myRichTextContent textStyle-${myStyle}`}
          //値は属性から取得
          value={ myContent }
          //onChange イベントで setAttributes を使って値を更新
          onChange={ (newContent) => setAttributes({ myContent: newContent }) }
          // div 要素として表示
          tagName='div'
          // 'p' を設定して Enter キーで新しい段落(p 要素)を生成
          multiline= 'p'
          //プレースホルダーテキストを表示
          placeholder= '文章を入力'
          keepPlaceholderOnFocus={ true }
        />
      </div>
    );
  },

  save: ( {attributes: { myStyle, myContent }} ) => {
    return (
      <div>
        <RichText.Content  //save 関数では RichText.Content を使います
          //クラスを追加
           className={`myRichTextContent textStyle-${myStyle}`}
          //値は属性から取得
          value={ myContent }
          // div 要素として表示
          tagName='div'
        />
      </div>
    )
  },
});

edit 関数では、分割代入で props からクラス名の className、属性(attributes)の myStyle と myContent、属性の値を設定する関数 setAttributes を受け取ります。

SelectControl のプロパティを使って以下を設定しています。

  • label:label 要素として表示されるラベル
  • value:選択されている値(属性 myStyle の値)
  • labelPosition:ラベルの表示位置(想定通りに機能しない。バグ?)
  • options:セレクトメニューに表示される項目(select 要素で指定する option 要素に対応)
  • onChange:新しく選択された値を受け取る関数を定義(選択された値を属性 myStyle の値に設定)

RichText コンポーネントでは、SelectControl で選択された値をテンプレートリテラルでクラス属性に設定しています。例えば、blue が選択されると myRichTextContent textStyle-blue というクラスが設定されます。

edit: ( {className, attributes: { myStyle, myContent }, setAttributes } ) => {
  return (
    <div className={className} >
      <SelectControl
        label="スタイル"
        value={myStyle}
        labelPosition='bottom'
        //セレクトメニューに表示する項目
        options={[
          {label: "スタイルを選択", value: null, disabled: true},
          {label: "Plain", value: 'plain'},
          {label: "Blue", value: 'blue'},
          {label: "Green", value: 'green'},
          {label: "Yellow", value: 'yellow'},
        ]}
        //setAttributes を使って選択された値(newVal)で属性 myStyle の値を更新
        onChange={ (newVal) => setAttributes({ myStyle: newVal }) }
      />
      <RichText
        //SelectControl で選択された値(myStyle)を使ってクラスを追加
        className={`myRichTextContent textStyle-${myStyle}`}
        //値は属性から取得
        value={ myContent }
        //onChange イベントで setAttributes を使って値を更新
        onChange={ (newContent) => setAttributes({ myContent: newContent }) }
        // div 要素として表示
        tagName='div'
        // 'p' を設定して Enter キーで新しい段落(p 要素)を生成
        multiline= 'p'
        //プレースホルダーテキストを表示
        placeholder= '文章を入力'
        keepPlaceholderOnFocus={ true }
      />
    </div>
  );
},

save 関数では、セレクトメニューは不要なので入力エリアを RichText.Content を使って保存し、edit 関数同様、セレクトメニューで選択された値を使ってクラス属性を設定しています。

save: ( {attributes: { myStyle, myContent }} ) => {
  return (
    <div>
      <RichText.Content  //save 関数では RichText.Content を使います
        //セレクトメニューで選択された値(myStyle)を使ってクラス属性を設定
         className={`myRichTextContent textStyle-${myStyle}`}
        //値は属性から取得
        value={ myContent }
        // div 要素として表示
        tagName='div'
      />
    </div>
  )
},

プロパティ

SelectControl には以下のようなプロパティがあります。

プロパティ 説明
label 設定すると指定した文字列が label 要素として出力される
labelPosition label 要素の表示位置(top, side, bottom)の指定
hideLabelFromVision true を指定するとスクリーンリーダーでのみ有効(visible)になる
multiple このプロパティ(multiple 属性)を設定すると複数の項目を選択できるようになる。onChange ハンドラに渡される値は配列になる。
options メニューの項目として以下のプロパティを持つオブジェクトの配列を指定。
  • label: メニューに表示される項目の文字列
  • value: 値。選択された際にこの値が onChange ハンドラに渡される。
  • disabled: disabled 属性を設定するかどうか。
onChange 選択された options の value(値)を受け取る関数

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

この例では以下のようなスタイルを設定しています。

editor-04.scss (エディター用スタイル)
.wp-block-wdl-block-04 {
	border: 1px solid #999;
}
/* ラベルを上部に表示するための設定(何故か labelPosition が機能しないため) */
.components-base-control .components-base-control__label {
  display: block;
}
style-04.scss(フロントエンド及びエディター用スタイル)
.myRichTextContent {
  padding: 20px;
}
.myRichTextContent.textStyle-plain {
  color: #999;
  background-color: #efefef;
}
.myRichTextContent.textStyle-blue {
  color: #222B73;
  background-color: #B5DFF8;
}
.myRichTextContent.textStyle-green {
  color: #1A571A;
  background-color: #C8FBCE;
}
.myRichTextContent.textStyle-yellow {
  color: #AE9506;
  background-color: #FBF9BD;
}

以下はブロック用スクリプトとスタイルを登録する PHP です。

register-wdl-block-04.php
<?php
function wdl_block_04_enqueue() {
  $asset_file = include( get_theme_file_path('/my-blocks/build/wdl-block-04.asset.php'));

  wp_register_script(
    'wdl-block-04',
    get_theme_file_uri('/my-blocks/build/wdl-block-04.js'),
    $asset_file['dependencies'],
    $asset_file['version']
  );

  //フロント&エディター用スタイル
  wp_register_style(
    'wdl-block-04-style', //ハンドル名
    //style-04.scss は build フォルダに style-wdl-block-04.css として出力される
    //(複数のエントリーポイントを設定した場合の webpack の設定により)
    get_theme_file_uri('/my-blocks/build/style-wdl-block-04.css'),
    array(),
    filemtime(get_theme_file_path('/my-blocks/build/style-wdl-block-04.css'))
  );

  //エディター用スタイル
  wp_register_style(
    'wdl-block-04-editor-style', //ハンドル名
    //editor-04.scss は build フォルダに wdl-block-04.css として出力される
    //(複数のエントリーポイントを設定した場合の webpack の設定により)
    get_theme_file_uri('/my-blocks/build/wdl-block-04.css'),
    array('wp-edit-blocks'),  //依存スタイルのハンドル
    filemtime(get_theme_file_path('/my-blocks/build/wdl-block-04.css'))
  );

  //ブロックタイプの登録
  register_block_type(
    'wdl/block-04',
    array(
      'editor_script' => 'wdl-block-04',
      'style' => 'wdl-block-04-style',
      'editor_style' => 'wdl-block-04-editor-style',
    )
  );
}
add_action( 'init', 'wdl_block_04_enqueue' );

MediaUpload/Button

MediaUpload はメディアをアップロードするモーダルウィンドウを開くボタンを描画するコンポーネントで、block-editor パッケージにあります。

通常 components パッケージにある Button コンポーネントなどと一緒に使用します。

参考サイト:gutenberg-examples/05-recipe-card-esnext

MediaUpload コンポーネントを使ったもう少し実用的な例は以下を御覧ください。

カスタムカードブロックの作り方

以下は MediaUpload コンポーネントを使って画像を挿入することができるブロックの例です。また、RichText コンポーネントを使ってタイトルや文章、リストも挿入できるようにしています。

挿入した画像は編集画面で「画像を削除」のリンクをクリックして削除できます。

以下がブロック用のスクリプトです。

以下は MediaUpload コンポーネントの部分の抜粋です。

attributes では、MediaUpload で画像が選択された場合に更新する値を属性として設定しています。

mediaID と mediaURL は画像が選択された場合に呼び出される onSelect のハンドラに渡されるメディアのオブジェクトから取得します。mediaURL は img 要素の source 属性が対象になります。

attributes: {
  ・・・中略・・・
  //MediaUpload の value の値(選択された画像から取得)
  mediaID: {
    type: 'number',
    default: 0 //デフォルト値(リセットした場合の値を設定)
  },
  //MediaUpload の画像の URL(選択された画像から取得)
  mediaURL: {
    type: 'string',
    source: 'attribute',
    selector: 'img',
    attribute: 'src',
  },

MediaUpload コンポーネントを使う場合、ユーザがメディアライブラリを使う権限があることをチェックする MediaUploadCheck コンポーネントでラップします。

onSelect には画像が選択される際に呼び出されるコールバック関数 onSelectImage を指定しています。

render はメディアライブラリを開くボタンをレンダリングするために呼び出されるコールバック関数で、引数にはコンポーネント側で定義されているモーダルウィンドウを開く関数 open を受け取ります。

この例では Button コンポーネントをレンダリングして、その onClick ハンドラに open を指定しています。Button コンポーネント内には mediaID が設定されていない場合はクラス button button-large を追加して「画像をアップロード」と表示し、設定されていればクラス image-button を追加して画像を表示します。

15〜23行目は挿入された画像を削除するボタンの記述です。このボタンもユーザがメディアライブラリを使う権限があることをチェックする MediaUploadCheck コンポーネントでラップしています。

削除用ボタンは mediaID が0でない(画像が挿入されている)場合に表示するようにして、クリックすると画像の属性に設定している mediaID と mediaURL をリセットする関数 removeMedia を呼び出します。

また、このボタンには Button コンポーネントのプロパティ isDestructive と isLink を設定して赤い文字色のリンクのように見せています。

<MediaUploadCheck>
  <MediaUpload
    onSelect={ onSelectImage }
    allowedTypes={ ['image'] }  //許可するメディアのタイプ
    value={ mediaID }
    render={ ( { open } ) => (
      <Button
        className={ mediaID ? 'image-button' : 'button button-large' }
        onClick={ open }>
        { ! mediaID ? "画像をアップロード" : <img src={ mediaURL } alt="画像" /> }
      </Button>
    ) }
  />
</MediaUploadCheck>
{ mediaID != 0  &&
  <MediaUploadCheck>
    <Button
      onClick={removeMedia}
      isLink
      isDestructive
      className="removeImage">画像を削除</Button>
  </MediaUploadCheck>
}

▼ 追記: 2020/10/08 ▼

上記では render プロパティに直接コールバック関数を記述していますが、別途コールバック関数を定義した方が見やすいかも知れません。

また、上記の場合、Button コンポーネントの中に画像をレンダリングしていますが、以下のように関数を別途定義して、mediaID の値により画像または Button コンポーネント をレンダリングする方法もあります。

この例の場合、img の onClick に open を指定しているので、CSS で img 要素にマウスオーバーした際にクリックできることを示すため、カーソルをポインターに変えるようにします。

//メディアライブラリを開くボタンをレンダリングする関数
const getImageButton = (open) => {
  if(mediaID) {
    return (
      <img
        src={ mediaURL }
        onClick={ open }
        alt=""
      />
    );
  }
  else {
    return (
      <div className="button-container">
        <Button
          onClick={ open }
          className="button button-large"
        >
          画像をアップロード
        </Button>
      </div>
    );
  }
};

上記のコールバック関数を使う場合は、MediaUpload の render プロパティに上記のコールバック関数を指定します。

<MediaUploadCheck>
  <MediaUpload
    onSelect={ onSelectImage }
    allowedTypes={ ['image'] }
    value={ mediaID }
    render={ ({ open }) => getImageButton(open) }
  />
</MediaUploadCheck>

▲ ここまで追記 2020/10/08 ▲

onSelectImage は MediaUpload の onSelect で呼び出される関数で、選択されたメディアのオブジェクトを引数に受け取ります。以下では、メディアの url 及び id プロパティの値を取得して、属性 mediaURL と mediaID の値を更新しています。

removeMedia は画像を削除するリンクのボタンをクリックした際に呼び出される関数で、mediaID を0に、mediaURL を空にしてリセットすることで画像を削除しています(画像の削除は mediaURL を空にすることにより img 要素の src 属性が空になるためです)。

//選択された画像の情報(URLとID)を更新するコールバック関数
const onSelectImage = ( media ) => {
  setAttributes( {
    mediaURL: media.url,
    mediaID: media.id,
  } );
};

//画像を削除する(メディアをリセットする)コールバック関数
const removeMedia = () => {
  props.setAttributes({
    mediaID: 0,
    mediaURL: ''
  });
}

save 関数では、mediaURL が true の場合(空出ない場合)に画像を出力しています。

save: ( props ) => {
  const { className, attributes: { title, mediaURL, data, comments } } = props;
  return (
    <div className={ className }>
      <RichText.Content tagName="h3" value={ title } />
      {
        mediaURL && (
          <img className="my-image" src={ mediaURL } alt="画像" />
        )
      }
      <RichText.Content tagName="ul" className="data" value={ data } />
      <RichText.Content tagName="div" className="comments" value={ comments } />
    </div>
  );
},

ブロックを投稿に挿入するとエディター画面では以下のように表示されます。

以下は画像を挿入した際の表示です。エディター側では画像は button 要素内に配置されます。

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

以下はこの例のスタイルの設定です。エディター側では画像は button 要素内に配置されます。そのため、画像が表示されている場合に button 要素に追加される image-button というクラスに height: auto; を指定しています。

editor-05.scss
.wp-block-wdl-block-05 .my-image img {
  display: block;
  position: relative;
}

.wp-block-wdl-block-05 .my-image button {
  margin: 20px 10px;
  height: 50px;
  display: block;
}

/* 画像挿入時はボタンの高さを auto に */
.wp-block-wdl-block-05 .my-image button.image-button {
  height: auto;
}

.wp-block-wdl-block-05 .my-image {
  border: 1px solid #ccc;
}

.wp-block-wdl-block-05 .my-image .removeImage {
  height: 1.5rem;
}
style-05.scss
.myRichTextContent {
  padding: 20px;
}

.wp-block-wdl-block-05 .my-image {
  border: 3px solid #999;
}

.wp-block-wdl-block-05 .data {
  margin: 20px 0;
  color: #aaa;
}
.wp-block-wdl-block-05 .comments {
  border: 1px solid #ccc;
  padding: 1rem;
  margin: 20px 0;
}

.wp-block-wdl-block-05 .my-image img {
  display: block;
  z-index: 1;
  position: relative;
}
プロパティ

MediaUpload コンポーネントでは以下のようなプロパティが用意されています。

MediaUpload コンポーネントのプロパティ
プロパティ 説明
allowedTypes メディアライブラリからアップロード・選択できるメディアのタイプを配列で指定。それぞれのメディアタイプは 'image', 'audio', 'text' のような mime type または 'audio/mpeg', 'image/gif' のような完全な mime type を指定。
multiple 複数選択を許可するかどうかの真偽値。デフォルトは false
value 選択したメディアの Media ID (数値)。multiple が true の場合は media ID の配列
onClose メディアモーダルが閉じられたときに呼び出されるコールバック関数。メディアが選択されたときと、ユーザーが選択せずにモーダルを閉じたときに呼び出されます。
onSelect メディアが選択されメディアモーダルが閉じられたとき呼び出されるコールバック関数(onClose の後に呼び出されます)。選択したメディア(オブジェクト)が引数として渡されます。
title メディアモーダルに表示するタイトル。デフォルトは「Select or Upload Media」
modalClass メディアモーダルのフレームに追加される CSS クラス
addToGallery true の場合、ギャラリーメディアモーダルはユーザーが画像を追加できるメディアライブラリで直接開きます。false の場合、ギャラリーメディアモーダルは編集モードで開き、ユーザーは既存の画像を並べ替えたり、削除したり、属性を変更したりして、既存の画像を編集できます。 gallery === trueの場合にのみ適用されます。デフォルトは false
gallery true の場合、コンポーネントはギャラリーとして開始します。 デフォルトでは、メディアモーダルはギャラリー編集モードで開きますが、addToGallery を使用して変更できます。デフォルトは false
render メディアライブラリを開くボタンをレンダリングするために呼び出されるコールバック関数。コールバック関数の引数には、呼び出されるとメディアモーダルを開く open という関数が渡されます。

Button コンポーネントでは以下のようなプロパティが用意されています。

プロパティ 説明 デフォルト
disabled ボタンが無効(disabled)かどうか false
href 設定されていれば button 要素ではなく a 要素としてレンダリング undefined
isSecondary default button スタイルでレンダリング false
isPrimary primary button スタイルでレンダリング false
isTertiary テキストベースのスタイルでレンダリング false
isDestructive 破壊的な動作を示す赤いテキストベースのスタイルをレンダリング false
isSmall 小さなサイズのボタンをレンダリング false
isPressed 押されたボタンのスタイルをレンダリング false
isBusy アクションが実行されている間のアクティビティを示します false
isLink アンカースタイルのボタンをレンダリング false
focus ボタンがフォーカスされているかどうか false
target href と共に設定される場合、target 属性を a 要素にに設定
className レンダリングされたボタンに適用する追加のクラス名
icon 設定されると Icon コンポーネントを表示 null
iconSize icon と共に設定される場合、アイコンのサイズを設定 20(Dashicon の場合) それ以外は 24
showTooltip 設定されると Tooltip コンポーネントを表示 false
tooltipPosition showTooltip と共に設定される場合、ツールチップの位置を設定 top center
shortcut showTooltip と共に設定される場合、ツールチップのコンテンツにショートカットラベルを追加 undefined
label 提供されていない場合は、コンポーネントの aria-label 属性を設定。showTooltip が提供されている場合、ツールチップのコンテンツを設定

インスペクター

エディター画面でブロックを選択した際に表示される右側のサイドバーがインスペクターです。

インスペクター(設定サイドバー)はあまり使わない設定や大きなスペースが必要な設定で使用し、ブロックレベル設定でのみ使用します。

ブロック内の選択したコンテンツでのみ有効な設定などは (例: 選択したテキストの「太字」設定)、インスペクターの中ではなくツールバーに含めます。設定サイドバーは HTML モードでブロックを編集する場合にも表示されるため、ブロックレベルの(ブロック全体に関わる)設定のみを含めるようにします。

独自のインスペクターを設定していない場合は、設定サイドバーにはブロックのアイコン、名前、説明と折りたたまれた「高度な設定」が表示されます。「高度な設定」の右側のアイコンをクリックすると以下のようにブロックに CSS クラスを追加する欄が表示されます。

以下ではトグルボタンやセレクトメニュー、ラジオボタン、カラーピッカーなどをインスペクターに追加してみます。

サイドバーはパネルと呼ばれるセクションで構成されていて、パネルには開閉ボタンが表示されます。追加するコントロールはデザイン的、機能的にも WordPress のコンポーネントを使用するのが簡単です。

インスペクターをカスタマイズするには block-editor パッケージにある InspectorControls コンポーネントを使用します。edit 関数の return の中で InspectorControls コンポーネントで囲んだ内容はサイドバーに表示されます。

インスペクターにパネルを追加するには PanelBody コンポーネントを使います。PanelBody にはパネルのタイトルを指定する title やパネルの開閉状態を指定する initialOpen プロパティなどがあります。

PanelBody の中にはパネル内のコンテナである PanelRow でコンポーネントなどのコンテンツを囲んで配置します。PanelRow はコンテンツに margin や Flexbox の設定(flex-direction: row)を適用します。

各コントロールのコンポーネントではラベルやヘルプテキスト、値などをプロパティを使って設定します。値は各コントロールの種類により、value や checked、selected などの HTML タグの属性に対応したプロパティがあり、変更された場合は onChange のコールバック関数の定義で setAttributes を使って値を更新します。

また、値は attributes プロパティにそれぞれの属性を定義して保存します。

<InspectorControls> //InspectorControls で囲んだ内容はサイドバーに表示される
  <PanelBody   // 1つのパネル全体を囲む
    title="サンプルインスペクター"
    initialOpen={true}
  >
    <PanelRow>   //各コンテンツを囲むコンテナ
      <ToggleControl
        label="トグルボタン"
        help="何らかのオン・オフ"
        checked={attributes.myToggle}
        onChange={(val) => setAttributes({ myToggle: val })}
      />
    </PanelRow>
    <PanelRow>   //各コンテンツを囲むコンテナ
      <SelectControl
        label="果物"
        help="好きな果物"
        value={attributes.myFruit}
        options={[
          {label: "りんご", value: 'apple'},
          {label: "ぶどう", value: 'grape'},
          {label: "いちご", value: 'strawberry'},
        ]}
        onChange={(val) => setAttributes({ myFruit: val })}
      />
    </PanelRow>

    ・・・中略・・・

  </PanelBody>
</InspectorControls>

以下は、トグルボタン、セレクトメニュー、ラジオボタン、カラーピッカー、チェックボックスで構成された1つのパネルをインスペクターに出力する例です。この例では1つのパネルにコントロールを全てまとめていますが、PanelBody を使って複数のパネルに分けることもできます。

インスペクター以外の部分は「複数の RichText コンポーネント」の例を使用しています。

各コントロールの値は attributes プロパティにそれぞれの属性を定義し、初期値(default)とデータ型(type)を指定します。トグルボタンとチェックボックスの値は真偽値(boolean)、セレクトメニュー、ラジオボタン、カラーピッカーの値は文字列(string)になります。

58行目の InspectorControls の開始タグから112行目の終了タグの間に記述されている部分がサイドバーのインスペクターに出力されます。

wdl-block-06.js
import { registerBlockType } from '@wordpress/blocks';
//RichText と InspectorControls を block-editor パッケージからインポート
import { RichText, InspectorControls } from '@wordpress/block-editor';
//PanelBody や PanelRow、コントロール類を components パッケージからインポート
import { PanelBody, PanelRow, ToggleControl, SelectControl, CheckboxControl, RadioControl, ColorPicker }  from '@wordpress/components';

registerBlockType( 'wdl/block-06', {
  title: 'WDL Sample Block 06',
  icon: 'smiley',
  description: 'サンプルのブロック',
  category: 'layout',
  //attributes に各コントロールの値を保存する属性を定義
  attributes: {
    //トグルボタン用
    myToggle: {
      type: 'boolean',
      default: true
    },
    //セレクトメニュー用
    myFruit: {
      type: 'string',
      default: 'strawberry'
    },
    //ラジオボタン用
    myDrink: {
      type: 'string',
      default: 'wine'
    },
    //カラーピッカー用
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    //チェックボックス用
    activateLasers: {
      type: 'boolean',
      default: false
    },
    //RichText のタイトル
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    //RichText のコンテンツ
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
  },

  edit: ( { attributes, setAttributes } ) => {
    return (
      <div>
        <InspectorControls>
          <PanelBody
            title="サンプルインスペクター"
            initialOpen={true}
          >
            <PanelRow>
              <ToggleControl
                label="トグルボタン"
                help="何らかのオン・オフ"
                checked={attributes.myToggle}
                onChange={(val) => setAttributes({ myToggle: val })}
              />
            </PanelRow>
            <PanelRow>
              <SelectControl
                label="果物"
                help="好きな果物"
                value={attributes.myFruit}
                options={[
                  {label: "りんご", value: 'apple'},
                  {label: "ぶどう", value: 'grape'},
                  {label: "いちご", value: 'strawberry'},
                ]}
                onChange={(val) => setAttributes({ myFruit: val })}
              />
            </PanelRow>
            <PanelRow>
              <RadioControl
                label="飲み物"
                help="よく飲む飲み物"
                selected={ attributes.myDrink }
                options={ [
                  { label: 'ワイン', value: 'wine' },
                  { label: 'ビール', value: 'beer' },
                  { label: '酒', value: 'sale' },
                ] }
                onChange={(val) => setAttributes({ myDrink: val })}
              />
            </PanelRow>
            <PanelRow>
              <ColorPicker
                color={attributes.myColor}
                onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
                disableAlpha
              />
            </PanelRow>
            <PanelRow>
              <CheckboxControl
                label="レーザーを準備?"
                checked={attributes.activateLasers}
                onChange={(val) => setAttributes({ activateLasers: val })}
              />
            </PanelRow>
          </PanelBody>
        </InspectorControls>
        <RichText
          className='myRichTextTitle'
          value={ attributes.myTitle }
          onChange={ (newTitle) => setAttributes({ myTitle: newTitle }) }
          tagName='h3'
          placeholder= 'タイトルを入力'
          keepPlaceholderOnFocus={true}
        />
        <RichText
          className='myRichTextContent'
          value={ attributes.myContent }
          onChange={ (newContent) => setAttributes({ myContent: newContent }) }
          tagName='div'
          multiline= 'p'
          placeholder= '文章を入力'
        />
      </div>
    );
  },

  save: ( {attributes: {myTitle, myContent }} ) => {
    return (
      <div>
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
           className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
      </div>
    )
  },
});

使用したコンポーネントのプロパティ

ToggleControl
プロパティ 説明
label 指定すると label 要素でラベルを出力します。
help 指定するとヘルプテキストを出力します。
checked トグルボタンの状態を表す真偽値。トグルボタンがオンの場合、checked の値は true になり、false の場合はトグルボタンはオフ(チェックされていない状態)です。
onChange checked の値(真偽値)を引数に取る関数

SelectControl のプロパティは「SelectControl/プロパティ」を参照ください。

RadioControl
プロパティ 説明
label 指定すると label 要素でラベルを出力します。
help 指定するとヘルプテキストを出力します。
selected 選択されている option の value プロパティの値
options 項目として表示する以下のプロパティを持つオブジェクトの配列。
  • label: 表示される項目の文字列
  • value: 値。選択された際にこの値が onChange ハンドラに渡される。
onChange 選択された options の value(値)を受け取る関数
ColorPicker
プロパティ 説明
color 選択された色
onChangeComplete 選択された色の情報を持つオブジェクトを受け取る関数。この例では透明度の指定のない16進数(例 #FFFFFF)の値を使うため、hex プロパティを使って setAttributes で値を更新しています。(参考:react-color/color
disableAlpha 透明度を表す Alpha チャンネルを無効にする
CheckboxControl
プロパティ 説明
heading 指定すると label 要素で見出しをチェックボックスの上に出力します。
label 指定すると label 要素でラベルをチェックボックスの右側(横)に出力します。
help 指定するとヘルプテキストを出力します。
checked チェックボックスの状態を表す真偽値。チェックボックスがチェックされている場合、checked の値は true になり、false の場合はチェックされていない状態です。
onChange checked の値(真偽値)を引数に取る関数

save 関数

前述のコードの save 関数ではインスペクターの値は何も使っていませんが、属性に保存されたインスペクターの値を使って save 関数での出力を制御するなどが可能です。

以下はチェックボックスの状態を保存した activateLasers 属性の値を使って、チェックが入っている場合に「レーザーの準備が完了しました」という文を div 要素で表示します。

また、値を確認するために、トグルボタンがオン(myToggle が true)の場合は、myFruit と myDrink の値を表示します。

JSX 内では任意の JavaScript の式を中括弧 { } で囲んで使用することができます。通常の if 文は使えないので(式ではないので)以下では、論理 && 演算子を使った短絡評価を使って div 要素や ul 要素を出力しています。(関連項目:React 条件付きレンダー

文字の色はインラインスタイルを使って指定しています。

save: ( {attributes: {myTitle, myContent, myToggle, myFruit, myDrink, myColor, activateLasers }} ) => {
  return (
    <div>
      <RichText.Content
        className='myRichTextTitle'
        value={ myTitle }
        tagName='h3'
      />
      <RichText.Content
         className='myRichTextContent'
        value={ myContent }
        tagName='div'
      />
      { activateLasers &&  //activateLasers が true の場合以下を出力
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      { myToggle &&  //myToggle が true の場合以下を出力
        <ul>
          <li>myFruit の値: { myFruit }</li>
          <li>myDrink の値: { myDrink }</li>
        </ul>
      }
    </div>
  )
},

supports プロパティ anchor

registerBlockType の supports プロパティで anchor: true を指定するとインスペクターの「高度な設定」に HTML アンカーを設定するセクションが追加されます。

registerBlockType( 'wdl/block-06', {
  title: 'WDL Sample Block 06',
  icon: 'smiley',

  ・・・中略・・・

  //supports プロパティを使って指定
  supports: {
    anchor: true
  },

 ・・・省略・・・

「高度な設定」の開閉ボタンをクリックすると以下のように表示されます。

上記のように「HTML アンカー」に「sample06」を入力して保存すると、フロントエンド側ではブロックの要素に id 属性が追加されます。

<div id="sample06" class="wp-block-wdl-block-06">

supports プロパティ customClassName

registerBlockType の supports プロパティで customClassName: false を指定するとインスペクターの「高度な設定」から「追加 CSS クラス」を削除します。 anchor: true が指定されていなければ、「高度な設定」のオプション自体も表示されません。

supports: {
  customClassName: false
}

参考サイト:Create Custom Gutenberg Block – Part 5: Inspector Settings

公式ページ:ブロックコントロール

ツールバー

ユーザーがブロックを選択するとブロックの上にツールバーが表示され、その中にコントロールボタンが表示されます。ツールバーをカスタマイズするには block-editor パッケージの BlockControls コンポーネントを使用します。

ブロックのツールバーにコントロールボタンを追加するには edit 関数の return の中で BlockControls コンポーネントを記述し、その中で標準ツールバーの WordPress コンポーネントの1つを追加するか、独自のツールバーを追加します。

デフォルトでは、ブロックにはブロックのアイコンと変換機能用のボタン(ブロックの削除など)が表示されます。

RichText を使ったブロックでは、テキストのフォーマット用のボタンが自動的に表示されます。

デフォルトで表示されるボタンを削除することはできませんが、独自のボタンを追加することができます。

block alignment

ブロックの位置揃え(block alignment)のツールバーを表示する簡単な方法は registerBlockType の supports プロパティを使って align: true を指定します。

registerBlockType( 'wdl/block-07', {
  title: 'WDL Sample Block 07',
  icon: 'smiley',

  ・・・中略・・・

  //supports プロパティを使って指定
  supports: {
    align: true
  },

 ・・・省略・・・

以下のように block alignment ツールバーが追加されます。

但し、「幅広」と「全幅」を利用するにはテーマで align-wide の機能を有効化する必要があります。「幅広」と「全幅」が表示されない場合は、functions.php で以下のように after_setup_theme フックで add_theme_support を使って有効化します。

functions.php
function my_theme_setup() {
  add_theme_support( 'align-wide' );
}
add_action( 'after_setup_theme', 'my_theme_setup');

上記では align: true で全てを利用できるようにしましたが、left, right, center, wide, full を使って利用できる機能を配列で指定することもできます。

以下は全幅(full)以外をツールバーに表示する例です。

supports: {
  align: ['left', 'center', 'right', 'wide']
}

また、supports プロパティでは alignWide がデフォルトで true になっています。「幅広」と「全幅」を表示しないようにするには alignWide に false を指定します。

supports: {
  align: true,
  alignWide: false
},

block alignment のデフォルトの設定

ブロックの位置揃え(block alignment)はデフォルトでは設定されません。block alignment をデフォルトで設定するには attributes プロパティに align という属性を定義して、type を string に指定して default を指定します。

attributes: {
  //align という属性を定義
  align: {
    type: 'string',
    default: 'center'
  }
},
supports: {
  align: true
},

block alignment のスタイル

例えばブロックの位置揃えのツールバーで「左寄せ」を選択して設定すると、エディター画面では class="wp-block" が付与されたブロック要素の外側の要素に data-align="left" が追加されます。

そして、以下のスタイルの設定よりブロックには float: left が適用されます。

.wp-block[data-align=left]>* {
  float: left;
  margin-right: 2em /*!rtl:end:ignore*/;
}

フロントエンド側ではブロックに alignleft というクラスが追加されます。

※ 「左寄せ」及び「右寄せ」をブロックに適用すると、エディター画面ではコンテンツの配置(フロー)に問題が発生する可能性があるようです(WordPress の問題?)。

text alignment

ブロックの位置揃えではなく、テキストの位置揃えのツールバーを追加することもできます。テキストの位置揃えでは「左寄せ」「中央寄せ」「右寄せ」を選択できます。

「ブロックの位置揃」と「テキストの位置揃え」の両方を追加することができるので、場合によってはユーザに混乱を招く可能性があります。テーマにどのようなスタイルを設定しているかにもよりますが、例えば、ブロックでは「幅広」と「全幅」のみを表示して、テキストでは「左寄せ」「中央寄せ」「右寄せ」を表示するなどが対策として考えられます。

以下がテキストの位置揃えをツールバーに追加するブロックのスクリプトです。ツールバー以外の部分は「複数の RichText コンポーネント」の例を使用しています。

attributes プロパティに、テキストの位置揃え(AlignmentToolbar コンポーネント)で選択された値を保存する属性 alignment を定義し、type と default を指定します。

ツールバーにアクセスするには BlockControls コンポーネントを使い、追加する「テキストの位置揃え」のツールバー AlignmentToolbar コンポーネントを囲みます(50〜55行目)。

AlignmentToolbar の value プロパティ(値)には属性 alignment を設定し、選択された値が変更された場合は onChange プロパティのコールバック関数(onChangeAlignment)で alignment を更新します。

onChangeAlignment の定義では、選択されていない状態(undefined)では値に none を、選択されると left、center、right のいずれかが設定されます。この値を使って外側の div 要素にインラインのスタイルを設定しています。

インラインスタイルの設定では、CSS の text-align は textAlign のようにキャメルケースを使います。

wdl-block-07.js
import { registerBlockType } from '@wordpress/blocks';
//BlockControls と AlignmentToolbar を block-editor パッケージからインポート
import { RichText, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';

registerBlockType( 'wdl/block-07', {
  title: 'WDL Sample Block 07',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    // AlignmentToolbar の値(left、center、right)を保存する属性
    alignment: {
      type: 'string',
      default: 'none', // 初期値は none
    },
  },
  //ブロックの位置揃えでは「幅広」と「全幅」を有効に
  supports: {
    align: ['wide', 'full' ]
  },

  edit: ( props ) => {
    // 分割代入でプロパティを変数に代入
    const {
      attributes: { alignment, myTitle, myContent},
      className,
      setAttributes
    } = props;

    // AlignmentToolbar の onChange ハンドラを定義
    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };

    return (
      <div className={className} style={ { textAlign: alignment } }>
        <BlockControls>
          <AlignmentToolbar
            value={ alignment }
            onChange={ onChangeAlignment }
          />
        </BlockControls>
        <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>
    );
  },

  save: ( {attributes: { alignment, myTitle, myContent }} ) => {
    return (
      <div style={ { textAlign: alignment } }>
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
      </div>
    )
  },
});

例えばテキストの位置揃えを「中央寄せ」にするとエディター画面では以下のようになり、div 要素にはインラインスタイルが追加されます。

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

WordPress 標準のクラスで指定

前述の例ではインラインスタイルを追加するようにしましたが、以下は WordPress 標準のテキスト揃えのクラスを追加する例です。

WordPress 標準のテキスト揃えのクラスは has-text-align-(left|center|right) のように指定します。

以下は edit と save 関数部分の抜粋です(他は前述の例と同じです)。以下では attributes の属性 alignment に定義した AlignmentToolbar の値を使ってクラス名を作成し、テンプレートリテラルを使って div クラスに追加しています。

edit: ( props ) => {
  const {
    attributes: { alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;
  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };

  //属性 alignment の値からクラス名を作成
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
  return (
    <div className={`${alignmentClass} ${className}`}  >
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
      </BlockControls>
      <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>
  );
},

save: ( {attributes: { alignment, myTitle, myContent }} ) => {
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
  return (
    <div className={alignmentClass} >
      <RichText.Content
        className='myRichTextTitle'
        value={ myTitle }
        tagName='h3'
      />
      <RichText.Content
        className='myRichTextContent'
        value={ myContent }
        tagName='div'
      />
    </div>
  )
},

テキストの位置揃えを「中央寄せ」にするとエディター画面では以下のようになり、div 要素にはクラス has-text-align-center が追加されます。

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

カスタムボタン

Toolbar コンポーネントと Button コンポーネントを使ってツールバーに独自のボタンを追加することもできます。

Toolbar コンポーネントと Button コンポーネントは components パッケージにあります。

import { Button, Toolbar } from '@wordpress/components';

Button を Toolbar で囲み、BlockControls コンポーネントの中に配置します。

以下は前述のテキスト揃えのツールバーの例に独自のボタンを追加する例です。

edit 関数の return 部分の抜粋
return (
  <div className={`${alignmentClass} ${className}`}  >
    <BlockControls>
      <AlignmentToolbar
        value={ alignment }
        onChange={ onChangeAlignment }
      />
      <Toolbar>
        <Button
          label="カスタムボタン"
          icon="heart"
          className="my-custom-button"
          onClick={() => console.log('ハートのボタンが押されました!')}
        />
      </Toolbar>
    </BlockControls>
    ・・・省略・・・
  </div>
);

Button コンポーネントには label などのプロパティを指定することができます(Buttonプロパティ)。

icon プロパティには Dashicons にあるアイコンを指定できます。上記の場合、ハートのアイコンは dashicons-heart なので dashicons- を除いた heart を icon プロパティに指定しています。

ボタンをクリックした際に発火されるコールバック関数は onClick プロパティに指定します。上記では単にコンソールに文字列を表示しているだけです。

以下がコードの全文です。

参考サイト:Create Custom Gutenberg Block – Part 6: Toolbars

edit/save 関数を別途定義

以下のように edit 及び save 関数を registerBlockType 関数の外側で別途定義して、それらを registerBlockType 関数内で指定する書き方もよく使われます。または、edit 関数のほうが記述が多くなるので edit 関数のみを別途定義することが多いです。

import { registerBlockType } from '@wordpress/blocks';
import { RichText, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { Button, Toolbar } from '@wordpress/components';

// edit 関数を定義
const edit = ( props ) => {
  const {
    attributes: { alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;
  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
  return (
    <div className={`${alignmentClass} ${className}`}  >
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            label="カスタムボタン"
            icon="heart"
            className="my-custom-button"
            onClick={() => console.log('ハートのボタンが押されました!')}
          />
        </Toolbar>
      </BlockControls>
      <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>
  );
}

// save 関数を定義
const save = ( {attributes: { alignment, myTitle, myContent }} ) => {
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
  return (
    <div className={alignmentClass} >
      <RichText.Content
        className='myRichTextTitle'
        value={ myTitle }
        tagName='h3'
      />
      <RichText.Content
        className='myRichTextContent'
        value={ myContent }
        tagName='div'
      />
    </div>
  )
}

registerBlockType( 'wdl/block-07', {
  title: 'WDL Sample Block 07',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  //別途定義した関数を指定
  edit: edit,
  save: save
});

この例の場合、最後の部分(98〜99行目)の edit: edit, 及び save: save はキーと値が同じなのでプロパティの短縮構文を使って以下のように記述することもできます。

edit,  //edit: edit と同じ
save   //save: save と同じ

または、edit 及び save 関数を別ファイルで export を使って定義して、import で読み込む方法も使われます。例えば、create-block パッケージを使って生成される雛形のファイルはそのようになっています(但し、edit 関数は Edit のようにコンポーネントとして定義されています)。

カスタムコンポーネント

React では関数またはクラスとしてコンポーネントを定義することができます。registerBlockType() の edit 関数や save 関数はインラインの関数コンポーネントの定義のようなものです。

以下は registerBlockType の edit 関数を別のコンポーネントに割り当てて、コンポーネントの state の機能を利用する例です。

実用的ではありませんが、カスタムボタンをクリックすると state の値を元に「プレビュー」と「編集」モードを切り替えます。「プレビュー」モードは Placeholder と Disabled コンポーネントを使って内容は確認できるけれど編集ができないようにして、編集モードは通常の編集可能な状態を出力するだけです。

元になるコードは以下のような RichText を使ったタイトルと文章の入力欄及びインスペクターとカスタムボタンを含むツールバーで構成されるブロックです。

関連項目

useState を使う

React 16.8 以降では関数コンポーネントでもステートフック(useState)を使って state の機能を使うことができます。以下は useState を使ってカスタムボタンをクリックするとモードを切り替える例です。

まずは Edit コンポーネントを registerBlockType 関数の外側で関数コンポーネントととして定義します。Edit コンポーネントの定義には edit 関数の内容をそのまま記述します。

関数コンポーネントはレンダリングする内容を記述した React 要素(JSX で記述した要素)を return ステートメントで返す関数です。また、コンポーネント名は常に大文字で始めます。

import { registerBlockType } from '@wordpress/blocks';
・・・中略・・・

//Edit コンポーネントの定義
const Edit = ( props ) => {
  ・・・edit 関数の中身を記述・・・
}

//registerBlockType 関数
registerBlockType( 'wdl/block-08', {
  ・・・中略・・・

  edit : Edit,  //別途定義
  save: ( {attributes: { myColor, ... }} ) => {
    ・・・save 関数はそのまま・・・
  }
});
import { registerBlockType } from '@wordpress/blocks';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';

const Edit = ( props ) => {
  const {
    attributes: { myColor, activateLasers, alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;
  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
  return (
    <div className={ alignmentClass + ' ' + className }  >
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            label="カスタムボタン"
            icon="heart"
            className="my-custom-button"
            onClick={() => console.log('ハートのボタンが押されました!')}
          />
        </Toolbar>
      </BlockControls>
      <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>
  );
}

registerBlockType( 'wdl/block-08', {
  title: 'WDL Sample Block 08',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit : Edit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

これは必須ではありませんが、Edit コンポーネントの中のインスペクターとツールバーのコンポーネントを return の外側で関数として返すように定義して、タイトルや文章を入力する RichText のコンポーネントと分離します。

return ステートメントは配列も返すことができ、配列内のものはその記述順にレンダリングされます。これにより、return ステートメント内で関数を直接呼び出すことができます。

const Edit = ( props ) => {
  const { attributes: { myColor,...}, className,setAttributes} = props;
  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none':... } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' ...

  //インスペクターを返す関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        ・・・中略・・・
      </InspectorControls>
    );
  }

  //ツールバーを返す関数
  const getBlockControls = () => {
    return (
      <BlockControls>
        ・・・中略・・・
      </BlockControls>
    );
  }

  return (
    //配列を指定
    [
      getInspectorControls(),  //インスペクター
      getBlockControls(),  //ツールバー
      <div className={ alignmentClass + ' ' + className }  >
        <RichText
          ・・・中略・・・
        />
        <RichText
          ・・・中略・・
        />
      </div>
    ]
  );
}
import { registerBlockType } from '@wordpress/blocks';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';

const Edit = ( props ) => {
  const {
    attributes: { myColor, activateLasers, alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;

  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

  //インスペクターを返す関数を別途定義
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  //ツールバーを返す関数を別途定義
  const getBlockControls = () => {
    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            label="カスタムボタン"
            icon="heart"
            className="my-custom-button"
            onClick={() => console.log('ハートのボタンが押されました!')}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  return (
    //配列
    [
      getInspectorControls(),
      getBlockControls(),
      <div className={ alignmentClass + ' ' + 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>
    ]
  );
  }


  registerBlockType( 'wdl/block-08', {
  title: 'WDL Sample Block 08',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit : Edit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

state / useState

この例ではツールバーのカスタムボタンをクリックすると、「プレビュー」モードと「編集」モードを切り替えるようにします。

モードを切り替えるためには何らかの方法で現在の状態(この場合、プレビューモードなのか、編集モードなのかの真偽値)を保存する必要があり、そのため React の state を利用します。

state はコンポーネント自体に一時的に保存されるもので、コンポーネントの状態を制御するために使用します。attributes の属性などには保存されません。

関数コンポーネントで state を使うには、ステートフック(useState)を使います。

ステートフックを使うには useState を @wordpress/element パッケージからインポートします。

import { useState } from '@wordpress/element';

以下が useState の書式で、使用する state 変数(state)とそれを更新する関数(setState)及び state 変数の初期値(initialState)を宣言します。

const [state, setState] = useState(initialState);

Edit コンポーネントで useState を使って、現在どちらのモードなのかの状態を表す state 変数(isEditMode)とその値を更新する関数(setEditMode)及び初期値(true)を宣言します。

ツールバーのカスタムボタンをクリックしてモードを切り替えるようにするため、カスタムボタンの onClick イベントハンドラに state の値を更新する関数(setEditMode)を指定して、クリックすると値を反転するようにします。

ボタンのラベルやアイコンは、state 変数(isEditMode)の値により表示を切り替えるようにします。

import { registerBlockType } from '@wordpress/blocks';
//useState をインポート
import { useState } from '@wordpress/element';
・・・中略・・・

const Edit = ( props ) => {
  //state 変数(isEditMode)と state の値を更新する関数(setEditMode)を宣言して初期化
  const [isEditMode, setEditMode] = useState(true);

  ・・・中略・・・

  const getInspectorControls = () => {
    return (
      <InspectorControls>
        ・・・中略・・・
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            //state の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //state の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //state の値を更新する関数(setEditMode)を使って値を更新(真偽値を反転)
            onClick={() => setEditMode(!isEditMode)}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  return (
    [
       ・・・中略・・・
    ]
  );
}
・・・以下省略・・・
import { registerBlockType } from '@wordpress/blocks';
//useState をインポート
import { useState } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';

const Edit = ( props ) => {

  //state 変数(isEditMode)と state の値を更新する関数(setEditMode)を宣言して初期化
  const [isEditMode, setEditMode] = useState(true);

  const {
    attributes: { myColor, activateLasers, alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;

  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            //state の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //state の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //state の値を更新する関数(setEditMode)を使って値を更新(真偽値を反転)
            onClick={() => setEditMode(!isEditMode)}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  console.log(isEditMode);

  return (
    [
      getInspectorControls(),
      getBlockControls(),
      <div className={ alignmentClass + ' ' + 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>
    ]
  );
}

registerBlockType( 'wdl/block-08', {
  title: 'WDL Sample Block 08',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit : Edit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

最後に、Edit コンポーネントの return ステートメントで state 変数の値(isEditMode)により出力を切り替えます。

isEditMode が true の場合(編集モード)は通常の編集可能な RichText コンポーネントを出力し、false の場合(プレビューモード)は、 PlaceholderDisabled コンポーネントを使って内容は確認できるけれど編集ができないようにします。

Placeholder は通常の編集画面と区別がつくように使用しています。Placeholder を使うとそれ自体のスタイルが適用され周りにボーダーが表示されます。この例では isColumnLayout プロパティに true を指定して flex-column レイアウトにし、label="プレビュー" を指定してラベルを表示しています。Placeholder を使わなければ、通常の編集画面と同じ見た目になります。

Disabled の内側に配置したコンポーネントは編集やクリックなどの操作ができないようになります。

また、編集モードでの出力は div 要素で囲む代わりに Fragment を使っています。

import { registerBlockType } from '@wordpress/blocks';
//useState と Fragment を element パッケージからインポート
import { useState, Fragment } from '@wordpress/element';
//追加で Placeholder と Disabled を components パッケージからインポート
import { ..., Placeholder, Disabled } from '@wordpress/components';
・・・中略・・・

//Edit コンポーネントの定義(edit 関数に指定するコンポーネント)
const Edit = ( props ) => {

  //state 変数(isEditMode)と state の値を更新する関数(setEditMode)を宣言して初期化
  const [isEditMode, setEditMode] = useState(true);

 ・・・中略・・・

  const getInspectorControls = () => {
    return (
      <InspectorControls>
      ・・・中略・・・
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
      ・・・中略・・・
      </BlockControls>
    );
  }

  return (
    [
      getInspectorControls(),
      getBlockControls(),
      <div className={ alignmentClass + ' ' + 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 の場合(プレビュー モード)
          <Placeholder isColumnLayout={true} label="プレビュー">
            <Disabled>
              <RichText.Content
                tagName="h3"
                value={myTitle}
              />
              <RichText.Content
                tagName="p"
                value={myContent}
              />
            </Disabled>
          </Placeholder>
        }
      </div>
    ]
  );
}
・・・以下省略・・・
wdl-block-08.js
import { registerBlockType } from '@wordpress/blocks';
//useState と Fragment を element パッケージからインポート
import { useState, Fragment } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
//追加で Placeholder と Disabled を components パッケージからインポート
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar, Placeholder, Disabled } from '@wordpress/components';
import './style-08.scss';
import './editor-08.scss';

//Edit コンポーネントの定義(edit 関数に指定するコンポーネント)
const Edit = ( props ) => {

  //state 変数(isEditMode)と state の値を更新する関数(setEditMode)を宣言して初期化
  const [isEditMode, setEditMode] = useState(true);

  const {
    attributes: { myColor, activateLasers, alignment, myTitle, myContent},
    className,
    setAttributes
  } = props;

  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            //state の値により表示するラベルを切り替え
            label={ isEditMode ? "Preview" : "Edit" }
            //state の値により表示するアイコンを切り替え
            icon={ isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //state の値を更新する関数(setEditMode)を使って値を更新(真偽値を反転)
            onClick={() => setEditMode(!isEditMode)}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  return (
    [
      getInspectorControls(),
      getBlockControls(),
      <div className={ alignmentClass + ' ' + 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 の場合(プレビュー モード)
          <Placeholder isColumnLayout={true} label="プレビュー">
            <Disabled>
              <RichText.Content
                tagName="h3"
                value={myTitle}
              />
              <RichText.Content
                tagName="p"
                value={myContent}
              />
            </Disabled>
          </Placeholder>
        }
      </div>
    ]
  );
}

registerBlockType( 'wdl/block-08', {
  title: 'WDL Sample Block 08',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit : Edit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

上記と同じことは useState を使わなくても属性(attributes)と setAttributes を使っても同じことができます。

import { registerBlockType } from '@wordpress/blocks';
import { Fragment } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar, Placeholder, Disabled } from '@wordpress/components';
import './style-08.scss';
import './editor-08.scss';

//Edit コンポーネントの定義
const Edit = ( props ) => {

  //attributes に設定した isEditMode を props 経由で取得
  const { attributes: { isEditMode, myColor, activateLasers, alignment, myTitle, myContent}, className, setAttributes } = props;

  const onChangeAlignment = ( newAlignment ) => {
    setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
  };
  const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  const getBlockControls = () => {
    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <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 (
    [
      getInspectorControls(),
      getBlockControls(),
      <div className={ alignmentClass + ' ' + 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 の場合(プレビュー モード)
          <Placeholder isColumnLayout={true} label="プレビュー">
            <Disabled>
              <RichText.Content
                tagName="h3"
                value={myTitle}
              />
              <RichText.Content
                tagName="p"
                value={myContent}
              />
            </Disabled>
          </Placeholder>
        }
      </div>
    ]
  );
}

registerBlockType( 'wdl/block-08', {
  title: 'WDL Sample Block 08',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    //isEditMode を属性として追加
    isEditMode: {
      type: 'boolean',
      default: true
    },
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit : Edit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

クラスコンポーネント

以下は前述の例と同じことをクラスコンポーネントを使って記述する例です。前述の例と同じコードを元に、edit 関数をクラスコンポーネントに書き換えて state を使います。

特に理由がなければ、前述の関数コンポーネントと useState を使う方が構造や記述が簡潔になります。

まずは、MyBlockEdit というクラスコンポーネントを定義して、edit 関数をクラスコンポーネントに書き換えます。クラスの定義は registerBlockType の前に記述します。

クラスコンポーネントを定義するには ES6 のクラス構文を使って @wordpress/element パッケージの Component を拡張(extends)します。

レンダリングする内容を render() メソッド内に記述します。render() は最初の描画時と props や state が更新する度に呼び出される必須のメソッドで、この中で return されたコードがレンダリングされます。

また、クラスコンポーネントの定義の中で props にアクセスするには this.props とする必要があります。

import { registerBlockType } from '@wordpress/blocks';
//Component を @wordpress/element パッケージからインポート
import { Component } from '@wordpress/element';
・・・中略・・・

//Component を拡張してクラスコンポーネントを定義
class MyBlockEdit extends Component {
  // render() 内に edit 関数の内容を記述
  render() {
    // props は this.props でアクセス(この部分のみが edit 関数と異なる)
    const { ... ,className, setAttributes} = this.props;
    ・・・中略・・・

    return (
      <div className={ alignmentClass + ' ' + className }  >
        ・・・中略・・・
      </div>
    );
  }
}

registerBlockType( 'wdl/block-09', {
  title: 'WDL Sample Block 09',
  icon: 'smiley',
  category: 'layout',
  ・・・中略・・・

  edit :MyBlockEdit,
  save: ( ... ) => {
    ・・・中略・・・
  }
});
import { registerBlockType } from '@wordpress/blocks';
//Component を @wordpress/element パッケージからインポート
import { Component } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';
import './style-09.scss';
import './editor-09.scss';

class MyBlockEdit extends Component {
  //render() 内に edit 関数の内容を記述
  render() {
    //props は this.props でアクセス
    const {
      attributes: { myColor, activateLasers, alignment, myTitle, myContent},
      className,
      setAttributes
    } = this.props;  // edit 関数の記述と異なるのはこの this.props の部分だけ
    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass + ' ' + className }  >
        <InspectorControls>
          <PanelBody
            title="サンプルインスペクター"
            initialOpen={true}
          >
            <PanelRow>
              <ColorPicker
                color={myColor}
                onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
                disableAlpha
              />
            </PanelRow>
            <PanelRow>
              <CheckboxControl
                label="レーザーを準備?"
                checked={activateLasers}
                onChange={(val) => setAttributes({ activateLasers: val })}
              />
            </PanelRow>
          </PanelBody>
        </InspectorControls>
        <BlockControls>
          <AlignmentToolbar
            value={ alignment }
            onChange={ onChangeAlignment }
          />
          <Toolbar>
            <Button
              label="カスタムボタン"
              icon="heart"
              className="my-custom-button"
              onClick={() => console.log('ハートのボタンが押されました!')}
            />
          </Toolbar>
        </BlockControls>
        <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>
    );
  }
}

registerBlockType( 'wdl/block-09', {
  title: 'WDL Sample Block 09',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit :MyBlockEdit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

前述の例と同様、インスペクターとツールバーのコンポーネントを他のコンポーネントの出力から分離するためメソッドとして定義します。

メソッドに必要なプロパティ(属性や setAttributes など)は this.props で取得します。

render() メソッドでメソッドを呼び出すには this を付ける必要があります。また、return ステートメントでは配列を返すようにしています。配列内のものはその記述順にレンダリングされ、return ステートメント内で関数を直接呼び出すことができます。

class MyBlockEdit extends Component {

  //インスペクターを返すメソッドを別途定義
  getInspectorControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { myColor, activateLasers,}, setAttributes } = this.props;

    return (
      <InspectorControls>
       ・・・中略・・・
      </InspectorControls>
    );
  }

  //ツールバーを返すメソッドを別途定義
  getBlockControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment }, setAttributes } = this.props;

    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };

    return (
      <BlockControls>
        ・・・中略・・・
      </BlockControls>
    );
  }

  render() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment, myTitle, myContent}, className, setAttributes} = this.props;

    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

    return ( //配列を指定
      [
        //メソッドには this. でアクセス
        this.getInspectorControls(),
        //メソッドには this. でアクセス
        this.getBlockControls(),
        <div className={alignmentClass}>
          <RichText
            ・・・中略・・・
          />
          <RichText
            ・・・中略・・・
          />
        </div>
      ]
    );
  }
}
import { registerBlockType } from '@wordpress/blocks';
import { Component } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';
import './style-09.scss';
import './editor-09.scss';

class MyBlockEdit extends Component {

  //インスペクターを返すメソッドを別途定義
  getInspectorControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { myColor, activateLasers,}, setAttributes } = this.props;

    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  //ツールバーを返すメソッドを別途定義
  getBlockControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment }, setAttributes } = this.props;

    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };

    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            label="カスタムボタン"
            icon="heart"
            className="my-custom-button"
            onClick={() => console.log('ハートのボタンが押されました!')}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  //
  render() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment, myTitle, myContent}, className, setAttributes} = this.props;

    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

    return (  //配列を指定
      [
        //メソッドには this. でアクセス
        this.getInspectorControls(),
        //メソッドには this. でアクセス
        this.getBlockControls(),
        <div className={alignmentClass}>
          <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>
      ]
    );
  }
}

registerBlockType( 'wdl/block-09', {
  title: 'WDL Sample Block 09',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit :MyBlockEdit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

constructor

state を使用するためにコンストラクタで state を初期化します。

state を初期化するには、constructor() を利用し、コンストラクタの中で直接 this.state に初期値を設定します。また、他の文の前に super(props) を呼び出します。

constructor(props) {
  //他の文の前に super(props) を呼び出す(コンストラクタのオーバライド)
  super(props);
  // state プロパティの初期化(初期値の設定)
  this.state = { isEditMode: true }
}

ツールバーのカスタムボタンをクリックしてモードを切り替えるようにするため、カスタムボタンの onClick イベントハンドラに state の値を更新する setState メソッド(this.setState)を指定して、クリックすると値を反転するようにします。

ボタンのラベルやアイコンは、state 変数(isEditMode)の値により表示を切り替えるようにします。state 変数にアクセスするには、this.state.isEditMode とします。

<Toolbar>
  <Button
    //state の値により表示するラベルを切り替え
    label={ this.state.isEditMode ? "Preview" : "Edit" }
    //state の値により表示するアイコンを切り替え
    icon={ this.state.isEditMode ? "format-image" : "edit" }
    className="my-custom-button"
    //state の値を更新するメソッド setState を使って値を更新(真偽値を反転)
    onClick={() => this.setState({ isEditMode: !this.state.isEditMode})}
  />
</Toolbar>
import { registerBlockType } from '@wordpress/blocks';
import { Component } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar } from '@wordpress/components';
import './style-09.scss';
import './editor-09.scss';

class MyBlockEdit extends Component {

  //state を使うためコンストラクタを定義
  constructor(props) {
    //他の文の前に super(props) を呼び出す
    super(props);
    // state プロパティの初期化(初期値の設定)
    this.state = { isEditMode: true }
  }

  //インスペクターを返すメソッドを別途定義
  getInspectorControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { myColor, activateLasers,}, setAttributes } = this.props;
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  //ツールバーを返すメソッドを別途定義
  getBlockControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment }, setAttributes } = this.props;

    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };

    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            //state の値により表示するラベルを切り替え
            label={ this.state.isEditMode ? "Preview" : "Edit" }
            //state の値により表示するアイコンを切り替え
            icon={ this.state.isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //state の値を更新するメソッド setState を使って値を更新(真偽値を反転)
            onClick={() => this.setState({ isEditMode: !this.state.isEditMode})}
          />
        </Toolbar>
      </BlockControls>
    );
  }
  //
  render() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment, myTitle, myContent}, className, setAttributes} = this.props;

    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

    return ( //配列を指定
      [
        //メソッドには this. でアクセス
        this.getInspectorControls(),
        //メソッドには this. でアクセス
        this.getBlockControls(),
        <div className={alignmentClass}>
          <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>
      ]
    );
  }
}

registerBlockType( 'wdl/block-09', {
  title: 'WDL Sample Block 09',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit :MyBlockEdit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

最後に、MyBlockEdit コンポーネントの return ステートメントで state 変数の値(isEditMode)により出力を切り替えます。

this.state.isEditMode が true の場合(編集モード)は通常の編集可能な RichText コンポーネントを出力し、false の場合(プレビューモード)は、 Placeholder と Disabled コンポーネントを使って編集ができないようにします(詳細は前述の例を参照)。

Placeholder、Disabled、Fragment をインポートする必要があります。

return ( //配列を指定
  [
    //メソッドには this. でアクセス
    this.getInspectorControls(),
    //メソッドには this. でアクセス
    this.getBlockControls(),
    <div className={ alignmentClass + ' ' + className }  >
    { this.state.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>
    }
    { !this.state.isEditMode &&   // isEditMode が false の場合(プレビュー モード)
      <Placeholder isColumnLayout={true} label="プレビュー">
        <Disabled>
          <RichText.Content
            tagName="h3"
            value={myTitle}
          />
          <RichText.Content
            tagName="p"
            value={myContent}
          />
        </Disabled>
      </Placeholder>
    }
  </div>
  ]
);
import { registerBlockType } from '@wordpress/blocks';
//Component と Fragment を element パッケージからインポート
import { Component, Fragment } from '@wordpress/element';
import { RichText, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
//追加で Placeholder と Disabled を components パッケージからインポート
import { PanelBody, PanelRow, CheckboxControl, ColorPicker, Button, Toolbar, Placeholder, Disabled } from '@wordpress/components';
import './style-09.scss';
import './editor-09.scss';

class MyBlockEdit extends Component {

  //state を使うためコンストラクタを定義
  constructor(props) {
    //他の文の前に super(props) を呼び出す(コンストラクタのオーバライド)
    super(props);
    // state を初期化
    this.state = { isEditMode: true }
  }

  //インスペクターを返すメソッドを別途定義
  getInspectorControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { myColor, activateLasers,}, setAttributes } = this.props;
    return (
      <InspectorControls>
        <PanelBody
          title="サンプルインスペクター"
          initialOpen={true}
        >
          <PanelRow>
            <ColorPicker
              color={myColor}
              onChangeComplete={(val) => setAttributes({ myColor: val.hex })}
              disableAlpha
            />
          </PanelRow>
          <PanelRow>
            <CheckboxControl
              label="レーザーを準備?"
              checked={activateLasers}
              onChange={(val) => setAttributes({ activateLasers: val })}
            />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }

  //ツールバーを返すメソッドを別途定義
  getBlockControls() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment }, setAttributes } = this.props;

    const onChangeAlignment = ( newAlignment ) => {
      setAttributes( { alignment: newAlignment === undefined ? 'none' : newAlignment } )
    };

    return (
      <BlockControls>
        <AlignmentToolbar
          value={ alignment }
          onChange={ onChangeAlignment }
        />
        <Toolbar>
          <Button
            //state の値により表示するラベルを切り替え
            label={ this.state.isEditMode ? "Preview" : "Edit" }
            //state の値により表示するアイコンを切り替え
            icon={ this.state.isEditMode ? "format-image" : "edit" }
            className="my-custom-button"
            //state の値を更新するメソッド setState を使って値を更新(真偽値を反転)
            onClick={() => this.setState({ isEditMode: !this.state.isEditMode})}
          />
        </Toolbar>
      </BlockControls>
    );
  }

  render() {
    //this.props で必要なプロパティにアクセス
    const { attributes: { alignment, myTitle, myContent}, className, setAttributes} = this.props;

    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';

    return ( //配列を指定
      [
        //メソッドには this. でアクセス
        this.getInspectorControls(),
        //メソッドには this. でアクセス
        this.getBlockControls(),
        <div className={ alignmentClass + ' ' + className }  >
        { this.state.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>
        }
        { !this.state.isEditMode &&   // isEditMode が false の場合(プレビュー モード)
          <Placeholder isColumnLayout={true} label="プレビュー">
            <Disabled>
              <RichText.Content
                tagName="h3"
                value={myTitle}
              />
              <RichText.Content
                tagName="p"
                value={myContent}
              />
            </Disabled>
          </Placeholder>
        }
      </div>
      ]
    );
  }
}

registerBlockType( 'wdl/block-09', {
  title: 'WDL Sample Block 09',
  icon: 'smiley',
  category: 'layout',
  attributes: {
    myColor: {
      type: 'string',
      default: '#DDDDDD'
    },
    activateLasers: {
      type: 'boolean',
      default: false
    },
    myTitle: {
      type: 'string',
      default: '',
      source: 'html',
      selector:'.myRichTextTitle'
    },
    myContent: {
      type: 'array',
      default: '',
      source: 'children',
      selector:'.myRichTextContent'
    },
    alignment: {
      type: 'string',
      default: 'none',
    },
  },
  supports: {
    align: ['wide', 'full' ]
  },

  edit :MyBlockEdit,
  save: ( {attributes: { myColor, activateLasers, alignment, myTitle, myContent }} ) => {
    const alignmentClass = (alignment != null) ? 'has-text-align-' + alignment : '';
    return (
      <div className={ alignmentClass } >
        <RichText.Content
          className='myRichTextTitle'
          value={ myTitle }
          tagName='h3'
        />
        <RichText.Content
          className='myRichTextContent'
          value={ myContent }
          tagName='div'
        />
        { activateLasers &&
        <div className="lasers" style={ { color:myColor } } >
          レーザーの準備が完了しました
        </div>
      }
      </div>
    )
  }
});

参考サイト:Create Custom Gutenberg Block – Part 7: Create Your Own Custom Components