BeAct Co., Ltd.

BLOG
社員ブログ

WordPress の「カスタムコードブロック」: 2

こんにちは! 今日もコーディングを楽しんでいますか?

さてこんかいは、前の続きです。いよいよ WordPress のカスタムコードブロックの「実装」に入っていきます。難しい部分も多いですが、一緒に学んでいきましょう。

参考: https://ja.wordpress.org/team/handbook/block-editor/getting-started/tutorial/

※注意

こんかいから、 React についての知識が必要になります。もし本文中で「?」となったときは、いったん公式リファレンスから React の基本を学習していただくことをオススメします。


edit と save とは?

editsave は、Gutenberg エディタのすべてのコードブロックにおいて中核となる、「編集」と「保存」とを司る関数です。それぞれの関数は、以下の表に示すような役割を持っています。

関数タイミング役割
edit記事の編集時に使用エディタ上の UI 描画
save記事の保存時に使用記事を HTML に変換して保存

edit

Gutenberg エディタで記事を編集する時の、カスタムコードブロックの UI を描画する関数です。

ブロックの見た目や機能を、編集者に提供します。また、必要に応じてツールバーのカスタマイズも可能です。

edit 関数で提供するUIの例

save

edit で編集した記事を保存するさい、 HTML 形式に変換する関数です。edit はあくまでも編集画面でのコードブロック UI 描画を司りますが、公開する記事にはそのような UI や描画更新などは必要ありませんので、「編集結果としての HTML」を WordPress に保存する役目を負います。

それぞれの実装を見てみよう

スキャフォールドされた、 edit.jssave.js を見てみましょう。コメント中の英語は、なるべく日本語に翻訳しています。

/**
 * テキストの翻訳を取得。
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
 */
import { __ } from '@wordpress/i18n';

/**
 * ブロックラッパー要素を指定する React フック。
 * class 名など、必要なプロパティをすべて提供する。
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps } from '@wordpress/block-editor';

/**
 * JavaScript ファイルから CSS、SASS、SCSS ファイルを参照することで、Webpack で処理できるようにする。
 * これらのファイルはすべて、エディタに適用される CSS に含まれる。
 *
 * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
 */
import './editor.scss';

/**
 * edit 関数には、エディタにおけるブロックの構造を記述する。
 * これにより、そのブロックが使用されたときの UI が描画される。
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @return {Element} 描画する HTML 要素
 */
export default function Edit() {
	return (
		<p { ...useBlockProps() }>
			{ __(
				'Customblock Information – hello from the editor!',
				'customblock-information'
			) }
		</p>
	);
}
/**
 * ブロックラッパー要素を指定する React フック。
 * class 名など、必要なプロパティをすべて提供する。
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
 */
import { useBlockProps } from '@wordpress/block-editor';

/**
 * save 関数には、編集画面で設定された属性(type など)をもとに、
 * 最終的な HTML 構造を定義する処理を記述する。
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
 *
 * @return {Element} 描画する HTML 要素
 */
export default function save() {
	return (
		<p { ...useBlockProps.save() }>
			{ 'Customblock Information – hello from the saved content!' }
		</p>
	);
}

マークアップは JSX で

まずはそれぞれのコードの後半、 export default function Edit() {} などの return している部分に注目してください。コードは JavaScript のはずなのに、なんだか HTML のようなマークアップ言語っぽいものが書かれていますね。これは React が採用した JSX という記法で、最終的には JavaScript の「式」として評価されるものです。

実は前回、プロジェクトをスキャフォールドしたさい、 @wordpress/scripts というモジュールが自動的に導入されています。そしてこのモジュールは、 edit() 関数と save() 関数が出力する JSX を、HTML のように解釈させるのです! つまり、この JSX のマークアップこそが、編集時および保存時に表示される内容になる、というわけです。

どうして return に () がつくの?

JSX は、JavaScript の構文上「ひとつの式」として扱われる必要があります。もし () で JSX 全体を囲わないと、途中の改行によって (JSX ではなく) JavaScript の構文ルールが働き、その位置が「式の終わり」とみなされてしまいます。これを防ぐために、 JSX は () で囲って return させているのです。

より詳しい情報は、 JSX の基礎を参照してください。

どうして edit は Edit() で、 save は save() なの?
React の仕様を、一部取り込んでいるためです。技術的背景に興味のある方は、ここを開いてみてください。

edit による出力は、記事の執筆者の操作に合わせて再描画できるよう、 React コンポーネント でなければなりません。そして React コンポーネントは、パスカルケースで命名されます。WordPress 開発陣はこれを尊重し、 Edit() と記述しているのです。

一方の save による保存内容は、もはやリアルタイムの再描画を行わないため、 React コンポーネントではありません。そのため、 JavaScript では一般的なプラクティスである「関数はキャメルケースで命名」が採用されています。

いずれにしても、これらは WordPress 開発陣によるコーディングルールに過ぎません。両者とも export default でエクスポートしている以上、インポートする側も自由な名前で受け取ることができます。とくに React に慣れていない方には、 import する名称にパスカルケースとキャメルケースが混在していると、見た目の統一感が崩れて混乱するかもしれません。そういう場合は、(ご自身の理解できる範囲で)自由に名前をつけても問題ありません

editを触ってみよう!

どこが表示されるのかがわかったところで、まずは edit を触ってみましょう、 edit.js 内のコードを、整理してみます。

/** ブロックラッパー要素を指定する React フック。 */
import { useBlockProps } from '@wordpress/block-editor';
/** スタイルシートです。 */
import './editor.scss';

export default function Edit() {
	return (
		// ここが「ブロックラッパー」。
		<p { ...useBlockProps() }>
			{ '「情報」カスタムコードブロックを作るぞ!' }
		</p>
	);
}

useBlockProps()

JSX 内には、中カッコ {} で囲われた部分に JavaScript の「式」を書くことができます(式には関数も含まれます)。これを利用して、目的であるカスタムコードブロックの最も外側に当たる要素useBlockProps() の実行結果をスプレッドさせます。するとこの要素は「ブロックラッパー」となり、カスタムコードブロックとしての機能を持つようになります。

では、このように変えた結果を見てみましょう。 npm run start を行なっていない場合は、実行してください。ビルド後は「反映」を忘れずに!

みごと! '「情報」カスタムコードブロックを作るぞ!' というテキストノードをもつ <p> 要素が、コードブロックとして登場しました!

演習

「ブロックラッパー」を <p> 以外のタグで作ってみましょう。

useBlockProps()について

useBlockProps() は、 React フックと呼ばれる組み込み関数の一つです。フックを簡単に説明すると「React コンポーネントファイルから、 React の機能を呼び出す関数」のことです(命名の慣習として、 use で始まります)。組み込みのフックも多数ありますが、自作することもできます。

この useBlockProps() は、WordPress 開発陣が作成したもので、要素をブロックラッパーとして定義するためのフックです。このフックの引数には、オブジェクト(いわゆる key/value 値)を与えることができます。たとえば、以下のコード例の attr のような引数を与えたら、どうなるでしょうか?

/** ブロックラッパー要素を指定する React フック。 */
import { useBlockProps } from '@wordpress/block-editor';
/** スタイルシートです。 */
import './editor.scss';

export default function Edit() {
	/** 引数としたいオブジェクト。 */
	const attr = {
		attr1: 'Attr-1',
		attr2: 'Attr-2',
	};

	return (
		// `attr` を、引数として与えると…
		<p { ...useBlockProps(attr) }>
			{ '「情報」カスタムコードブロックを作るぞ!' }
		</p>
	);
}

答えは、こうです!

<p> 要素の属性として、オブジェクトの key/value ごとに key="value" として HTML 属性を定義できました! これが useBlockProps() フックの機能の一つです。

注意

以降の文章は、 React フックについての知識をすでにお持ちであるという前提で記述します。

Edit() に props が渡される仕組みと、その使い方

useBlockProps() の引数の役割(ブロックラッパー要素の HTML 属性を定義する)を説明しましたが、大本である Edit() …… edit 関数にも、引数を与えることができます。そしてこれこそが、カスタムコードブロックを作るうえで最も重要です。少し複雑な話になりますが、ゆっくり理解していってくださいね。

とりあえず、この関数の引数の型をご紹介しましょう。

/** 模式的な表現です */
Edit(props?: BlockEditProps<T>): React.JSX.Element;

はて? BlockEditProps<T> ? 聞いたことのない型ですね。少しずつ紐解いてみましょう。

BlockEditProps<T>

さて、ここで React フックの基本である useState() を思い出してみましょう。実はこの構造が、Edit() 関数に渡される props の構造ととてもよく似ているのです。

// React の useState
const [count, setCount] = useState(0);

// Gutenberg ブロックの属性。
const { attributes, setAttributes } = props;

useState() フックは以下のように、特定の値と、それを更新する関数のタプルを返します。そしてこの関数を実行するたびに、コンポーネントが再描画される……という機能をもつフックです。模式的な型はこうなります。

/** 模式的な表現です */
interface useState {
  <T extends unknown>(initial: T): [val: Readonly<T>, (mutation: T) => void];
}

これは、以下のように使います。

// [年齢, 年齢の変更関数] のタプル
const [age, setAge] = useState(20);

setAge(40); // age の値が更新されてコンポーネントが再描画される。

さて、次に BlockEditProps<T> の型定義(の一部)を、模式的に表してみます。

/** 模式的な表現です */
interface BlockEditProps<T extends Record<string, any>> {
  readonly attributes: Readonly<T>;
  readonly setAttributes: (attrs: Partial<T>) => void;
};

ここでは特に重要なプロパティ、カスタムコードブロック属性値 である attributes と、属性値更新関数 setAttributes() にスポットを当てています。

  • attributes: T
    • このカスタムコードブロックに設定できる属性名と、その値(key/value 値)。
  • setAttributes(attrs: Partial<T>)
    • 実際に、属性に値をセットする関数。attributes の一部または全部を書き換えます。

attributes は読み取り専用のプロパティで、直接書き換えることはできません。代わりに setAttributes() を通して変更します。

function Edit<T extends Record<string, any>>(blockEditProps: BlockEditProps<T>) {
  // {属性, 属性の変更関数} のオブジェクト。
  const {attributes, setAttributes} = blockEditProps;

  setAttributes({prop1: '1'}); // attributes.prop1 の値が書き換わり、コンポーネントが再描画される。
}

よく見てみると、 useState() のコードとそっくりですね! そうです。 Edit() の引数には attributessetAttributes が含まれており、 setAttributes() を使うことで、あたかも useState() を使ったかのように、値の更新に合わせて UI を再描画できるのです!

この仕組みによって、カスタムコードブロックによる「記事の編集」を可能としているわけなのです。

カスタムコードブロック属性値の定義方法

上の説明のとおり、型引数 T とは、カスタムコードブロック属性値( attributes )の「型」を指しています。では、この定義がどこからやってきたのか、ということなのですが……。

定義は block.json の中!

意外かもしれませんが、この属性値の型定義は block.json の中に記述するのです!

{
  // ... 前略
  "attributes": {
    "属性名1": {
      "この属性の性質": "性質を示す値…"
    },
    "属性名2": {
      "この属性の性質": "性質を示す値…"
    }
  }
}

この属性値 "attributes" の内容こそが、このカスタムコードブロック属性値の定義になります。

Gutenberg のビルドプロセス(JSX の変換や依存関係の解決など)では、block.json の属性定義をもとに、ブロックの挙動を自動的に構築します。block.json に定義した attributes は、このビルドプロセスで読み込まれ、

  • 保存時/復元時の属性名とデシリアライズ処理
  • useBlockProps()setAttributes() による属性の反映

といった動作仕様を WordPress に伝えます。JavaScript では型安全性は得られませんが、属性の「仕様書」として必須の役割を果たします。

では引き続き、この定義について深堀りしていきましょう。

属性の型情報の定義

さて、話の最初に戻ってみましょう。作りたいのは「情報ブロック」で、次の機能(値による再描画の仕掛け)を持たせたいと考えています。

  • 「情報」「注意」「警告」という種類があり、自由に(リアクティブに)切り替えたい。
    • それぞれ、 のようなアイコンを表示させたい。

そこで、「情報」「注意」「警告」を type という名前の属性名とし、その取りうる値は infowarningerror 。そして、デフォルト値は info である。と決めてみます。

すると、block.json の "attributes" は、このように定義されることでしょう。

{
  // ... 前略
  "attributes": {
    "type": {
      "enum": ["info", "warning", "error"],
      "default": "info"
    }
  }
}

こうなってしまえば、カスタムコードブロック属性値の模式的な型情報は、

/** 模式的な表現です */
type 属性値 = {
  type: 'info' | 'warning' | 'error';
};

となり、未定義の場合は {type: 'info'} という型になります。この場合の BlockEditProps<属性値> は、

function Edit(props: BlockEditProps<{type: 'info' | 'warning' | 'error'}>) {
  const {attributes, setAttributes} = props;

  console.log(attributes.type); // 'info', 'warning', 'error' のいずれか。

  setAttributes({type: 'error'}); // 属性が変更されることで再描画が発生する。
};

と、 BlockEditProps<{type: 'info' | 'warning' | 'error'}> のようになります。

コードの実装

お待たせしました。いよいよ実装です。まず作りたいカスタムコードブロックの仕様を確認します。

  • ブログ記事においては「余談」に当たるので、余談要素である <aside> で表現したい。
  • 「情報」「注意」「警告」という種類があり、自由に(リアクティブに)切り替えたい。
    • それぞれ、 のようなアイコンを表示させたい。
  • ブロックの中は、自由に編集をさせたい。
    • ただし、情報ブロックそのものを入れ子にはさせたくない。

よろしいでしょうか? それでは、実際のコードをご紹介しましょう。コード中の説明も、しっかり読んでくださいね!

注意

@wordpress/icons という npm モジュールが導入されていない場合は、コンソールから

npm i -D @wordpress/icons

のコマンドを実行して、導入してください。

edit.js

import {
  useBlockProps, // ブロックラッパーの React フック。
  InnerBlocks, // このカスタムブロックエディタ内に、段落などを入れられるようにする。
  BlockControls, // このカスタムブロックのそのものに対する UI を扱えるようにする。
} from '@wordpress/block-editor';

import {
  ToolbarGroup, // このカスタムブロックを、ツールバーから変更できるようにする。
  ToolbarButton, // 上記ツールバーに追加するボタン。
} from '@wordpress/components';

import {
  Icon, // WordPress のビルトインアイコンを扱えるようにする。
  cautionFilled, // 丸に「!」のアイコン 。
  error, // 三角に「!」のアイコン。
  notAllowed, // 「通行止め」のアイコン。
} from '@wordpress/icons';

// .scss ファイルを .css に変換するため、ここで import する。
import './editor.scss';

/** 属性名とアイコンとの key/value 値。 */
const ICONS = {
  info: { label: '情報', icon: cautionFilled },
  warning: { label: '注意', icon: error },
  error: { label: '警告', icon: notAllowed },
};

/**
 * React Edit Component
 *
 * @param {BlockEditProps<{type:"info"|"warning"|"error"}>} param
 * @returns {React.JSX.Element}
 */
export function Edit({ attributes, setAttributes }) {
  const { type } = attributes; // info, warning, error のいずれか。

  return (
    <>
      {/* ツールバー */}
      <BlockControls>
        <ToolbarGroup>
          {Object.entries(ICONS).map(([key, { label, icon }]) => (
            {/* ツールバーに3種類のボタンを描画し、押したら再描画させる。 */}
            <ToolbarButton
              key={key}
              icon={icon}
              label={label}
              isPressed={type === key}
              onClick={() => setAttributes({ type: key, icon })}
            />
          ))}
        </ToolbarGroup>
      </BlockControls>

      {/* エディターのブロック本体。 */}
      <aside {...useBlockProps({ className: `info-block ${type}` })}>
        {/* アイコンを描画する。 */}
        <Icon icon={ICONS[type].icon} className="info-icon" size={28} />
        <div className={'content'}>
          {/* このカスタムコードブロックは、いかなるコードブロックも入れ子にできる。 */}
          <InnerBlocks allowedBlocks={null} />
        </div>
      </aside>
    </>
  );
}

save.js

import {
  useBlockProps, // ブロックラッパーの React フック。
  InnerBlocks, // このカスタムブロックエディタ内に、段落などを入れられるようにする。
} from '@wordpress/block-editor';

import {
  Icon, // WordPress のビルトインアイコンを扱えるようにする。
  cautionFilled, // 丸に「!」のアイコン 。
  error, // 三角に「!」のアイコン。
  notAllowed, // 「通行止め」のアイコン。
} from '@wordpress/icons';

/** 属性名とアイコンとの key/value 値。 */
const ICONS = {
  info: cautionFilled,
  warning: error,
  error: notAllowed,
};

/**
 * JSX Element
 *
 * @param {BlockSaveProps<{type:"info"|"warning"|"error"}>} param
 * @returns {React.JSX.Element}
 */
export function save({ attributes }) {
  const { type } = attributes;
  return (
    <aside {...useBlockProps.save({ className: `info-block ${type}` })}>
      {/* アイコンを描画する。 */}
      <Icon icon={ICONS[type]} className="info-icon" size={28} />
      <div className={'content'}>
        {/* edit 関数で編集した内容を、反映させる。 */}
        <InnerBlocks.Content />
      </div>
    </aside>
  );
}

index.js

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

import { Edit } from './edit';
import { save } from './save';
import metadata from './block.json';

// edit 関数と save 関数を、登録する。
registerBlockType(metadata.name, {
  /**
   * @see ./edit.js
   */
  edit: Edit,

  /**
   * @see ./save.js
   */
  save,
});

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "create-block/customblock-information",
  "version": "0.1.0",
  "title": "情報ブロック",
  "category": "text",
  "icon": "info-outline",
  "description": "情報、警告、エラーメッセージを表示するブロック",
  "parent": ["core/post-content"],
  "example": {},
  "textdomain": "customblock-information",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "attributes": {
    "type": {
      "enum": ["info", "warning", "error"],
      "default": "info"
    }
  }
}

以下、 block.json 中で、今まで説明していなかった属性を解説します。

  • "parent": ["core/post-content"],
    • 「このカスタムコードブロックは、記事本体 ("core/post-content") の直下にのみ配置可能で、他のブロックの中に入れ子で挿入することはできない」という指定です。
  • "editorScript": "file:./index.js",
    • 編集時に動作させる JavaScript の指定です。
  • "editorStyle": "file:./index.css",
    • 編集時に参照するスタイルです。 SCSS で記述していても、 CSS にトランスパイルされるため、拡張子は .css を指定してください。
  • "style": "file:./style-index.css",
    • 編集時と閲覧時に参照するスタイルです。編集時は "editorStyle" での指定と合わせて参照されます。

editor.scss

// いちばん外側のセレクタが、カスタムコードブロックです。
.wp-block-create-block-customblock-information {
  div.block-editor-block-list__layout {
    & > *:first-child {
      margin-top: 0;
    }
    & > *:last-child {
      margin-bottom: 0;
    }
  }
}

style.scss

// いちばん外側のセレクタが、カスタムコードブロックです。
.wp-block-create-block-customblock-information {
  box-sizing: border-box;
  padding: 0.5em;
  border: 1px solid;
  border-radius: 0.5rem;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;

  & > div > *:first-child {
    margin-top: 0;
  }
  & > div > *:last-child {
    margin-bottom: 0;
  }
  & > .info-icon {
    margin-right: 0.2em;
  }

  &.info {
    background: #beddff;
    border-color: #2b7cd3;

    & > .info-icon {
      color: #0645ad;
      fill: currentColor;
    }
  }

  &.warning {
    background: #fff3cd;
    border-color: #ffc107;

    & > .info-icon {
      color: #856404;
      fill: currentColor;
    }
  }

  &.error {
    background: #f8d7da;
    border-color: #dc3545;

    & > .info-icon {
      color: #721c24;
      fill: currentColor;
    }
  }
}

スキャフォールド時に、これら以外のファイルができていると思いますが、それらはすべて不要ですので削除してください。

演習

このコードで、ビルドを行ってみてください。ビルド後は「反映」を忘れずに!

まとめ

こんかいは、WordPress のカスタムコードブロックを実装するうえで重要となる edit 関数と save 関数の役割を学びました。それぞれが編集時・保存時の UI を司り、JSX を用いたマークアップを通じて、記事に表示される内容が決定されることがわかりました。

また、useBlockProps() によってブロックの外枠が定義されること、Edit() 関数の引数として attributessetAttributes() が渡され、ブロック属性を自由に制御できる仕組みも理解しました。

後半では、block.json による属性の定義、React コンポーネントを用いたブロック構築、そして InnerBlocks を利用した実践的なコード例までを紹介しました。

次回は、こんかいの内容では触れきれなかった以下のトピックについて、掘り下げてゆく予定です。

  • 各 JavaScript ファイル(index.js, edit.js, save.js)で import しているモジュールの整理
  • プロジェクトルートにある PHP ファイルの意味と仕組み
  • npm run build および npm run plugin-zip の使いどころと注意点
  • 作成したカスタムコードブロックを、実際の WordPress 環境へ導入する手順

「作って終わり」ではなく、「動かして使う」ための最終段階へ、いよいよ進んでゆきます。どうぞお楽しみに!