カスタムブロックを作っていて最初に必要になるのが、「ユーザーが入力した文字をどこに持たせるか」という仕組みです。WordPress のブロックでは、この入れ物を attributes(属性)と呼び、値を書き換えるときは setAttributes という関数を使います。そして文字入力の UI としてもっともよく使われるのが RichText コンポーネントです。この記事では、attributes の宣言の仕方から setAttributes での更新、RichText で編集できるテキスト欄を作るところまでを、実際のコードとあわせて解説します。WordPress 6 系、apiVersion 3 を前提としています。
目次
attributes はブロックが持つデータの置き場所
attributes は、そのブロックが保持するデータの定義です。見出しに入力された文字列、選択したサイズ、チェックボックスのオン・オフといった値は、すべて属性としてブロックに紐づいて保存されます。React を触ったことがあれば state に近いものと考えると分かりやすいのですが、決定的に違うのは投稿の本文と一緒に保存され、次に投稿を開いたときに復元される点です。つまり属性は一時的な状態ではなく、記事データそのものになります。
属性は block.json の attributes に宣言します。ここに書いていない名前の値を setAttributes で渡しても保存されないので、まずは宣言から始めます。
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p.notice-box__text",
"default": ""
},
"level": {
"type": "number",
"default": 3
},
"isDismissible": {
"type": "boolean",
"default": false
}
}
ひとつの属性は、値の型を表す type を中心に、初期値の default、保存済みの HTML から値を読み戻すための source と selector を組み合わせて書きます。type に指定できる型は JSON スキーマに沿った次の種類です。
type | 入る値 |
|---|---|
string | 文字列。テキスト、URL、色コード、選択肢のキーなど、いちばん出番が多い |
number | 数値。小数を含む。見出しレベルや列数など |
integer | 整数のみ。小数を許したくない値に使う |
boolean | true / false。トグルスイッチの状態など |
array | 配列。項目のリストなど、同じ形のデータを並べるとき |
object | オブジェクト。画像の id と url をまとめて持つときなど |
null | 値なし。単独ではほとんど使わない |
default を書いておくと、ブロックを挿入した直後からその値が入った状態になります。指定しなかった場合の初期値は undefined です。文字列の属性で default を省略すると、value に undefined が渡って警告が出ることがあるので、テキスト系の属性には "" を入れておくと安心です。
source と selector で保存済みの HTML から値を読み戻す
属性の保存先は 2 通りあります。source を書かない場合は、値がブロックコメントの中の JSON として保存されます。投稿本文を「コードエディター」で見ると <!-- wp:webool/notice-box {"isDismissible":true} --> のように書き込まれているのが確認できます。一方 source を書いた場合は、値は JSON には入らず、save が出力した HTML そのものが保存先になります。投稿を開き直すときは、selector で指定した要素をたどって値が拾い直されます。
source | 値をどこから読み取るか |
|---|---|
| 指定なし | HTML ではなく、ブロックコメントに書かれた JSON から読み取る。表示に直接出ない設定値向き |
"html" | selector の要素の内側の HTML。<strong> やリンクを含んだままの文字列が入る。RichText と組み合わせるのはこれ |
"text" | selector の要素のテキスト。タグは取り除かれ、装飾のない文字列だけが入る |
"attribute" | selector の要素の属性値。取り出す属性名を attribute キーで指定する(例: img の src) |
selector は save が返す HTML の中を検索する CSS セレクターです。省略するとブロックのルート要素が対象になります。"attribute" を使うときは、次のように attribute キーもあわせて書きます。
"imageUrl": {
"type": "string",
"source": "attribute",
"selector": "img.notice-box__image",
"attribute": "src"
}
どちらを選ぶかの判断は単純で、フロントの HTML に文字や属性として現れる値には source を付け、現れない設定値には付けないと考えれば大きく外れません。なお、フロント表示を PHP で組み立てる動的ブロックでは save が HTML を残さないため、読み戻す先がありません。この場合はすべて source なしで定義します。
edit で値を受け取り setAttributes で書き換える
宣言した属性は、edit の引数として渡されるオブジェクトの中に入っています。読むときは attributes、書き換えるときは setAttributes を使います。
import { useBlockProps } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
// 分割代入で必要な属性だけ取り出す
const { content, isDismissible } = attributes;
const blockProps = useBlockProps( { className: 'notice-box' } );
return (
<div { ...blockProps }>
<p>{ content }</p>
<button
onClick={ () =>
// 渡したキーだけが更新される
setAttributes( { isDismissible: ! isDismissible } )
}
>
閉じるボタン: { isDismissible ? 'あり' : 'なし' }
</button>
</div>
);
}
setAttributes は、渡したキーだけを既存の属性に上書きする関数です。上の例では isDismissible だけを渡していますが、content が消えることはありません。属性が 10 個あっても、変更したいものだけを含んだオブジェクトを渡せば十分です。
そしてこの関数を経由することに意味があります。setAttributes を呼ぶと、エディターの内部データが更新されて画面が再描画され、同時に投稿が「未保存の変更あり」の状態になります。attributes.content = '新しい値' のように直接代入しても、この一連の処理は何も起きません。画面は変わらず、保存もされず、原因の分かりにくい不具合になります。属性の更新は必ず setAttributes から行ってください。
RichText で編集できるテキスト欄を作る
<input> や <textarea> をそのまま置くこともできますが、それだと太字やリンクといった書式が使えず、コアのブロックと操作感がそろいません。編集画面で本文らしく文字を入力させたいときは、@wordpress/block-editor の RichText コンポーネントを使います。カーソルを置くとツールバーに書式ボタンが出て、選択範囲を太字にしたりリンクを貼ったりできる、あの入力欄です。
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const { content } = attributes;
const blockProps = useBlockProps( { className: 'notice-box' } );
return (
<div { ...blockProps }>
<RichText
tagName="p"
className="notice-box__text"
value={ content }
onChange={ ( newContent ) =>
setAttributes( { content: newContent } )
}
placeholder="お知らせの本文を入力"
/>
</div>
);
}
やっていることは 2 つだけです。value に属性の値を渡して表示し、入力があるたびに呼ばれる onChange の中で setAttributes を呼んで属性を更新する。RichText 自身は値を持たず、表示も更新もこちらが用意した属性に委ねられています。
よく使う props
value と onChange のほかに、実際の開発でよく指定するのは次のあたりです。
| prop | 説明 |
|---|---|
value | 表示する値。HTML を含む文字列を渡す |
onChange | 入力のたびに呼ばれる関数。新しい値が引数で渡ってくる |
tagName | 出力される要素名。"p" や "h2" など。省略時は div |
placeholder | 値が空のときに薄く表示する案内文 |
className | 要素に付けるクラス名。selector と対応させる |
allowedFormats | 使える書式を配列で限定する(例: [ 'core/bold', 'core/link' ])。空配列を渡すと書式ボタンが出なくなる |
withoutInteractiveFormatting | true にするとリンクなど操作を伴う書式を無効にする |
disableLineBreaks | true にすると Enter キーでの改行を禁止する。見出し用の1行入力に向く |
allowedFormats は、ブロックのデザインを保ちたいときに効きます。たとえば見出し部分に色や見出しレベルの書式まで入れられると崩れてしまう、という場面では、太字とリンクだけに絞ってしまうのが簡単です。書式をいっさい使わせたくなければ allowedFormats={ [] } と空の配列を渡します。
<RichText
tagName="h3"
className="notice-box__heading"
value={ heading }
onChange={ ( value ) => setAttributes( { heading: value } ) }
placeholder="見出しを入力"
allowedFormats={ [ 'core/bold', 'core/link' ] }
disableLineBreaks
/>
save 側は RichText.Content を使う
RichText が扱う値は、これは<strong>重要</strong>です のようにタグを含んだ HTML 文字列です。これを save で <p>{ content }</p> と書いてしまうと、React が文字列をエスケープしてタグがそのまま画面に出てしまいます。保存側では、値を HTML として書き出す RichText.Content を使います。
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { content } = attributes;
const blockProps = useBlockProps.save( { className: 'notice-box' } );
return (
<div { ...blockProps }>
{/* edit 側と tagName・className をそろえる */}
<RichText.Content
tagName="p"
className="notice-box__text"
value={ content }
/>
</div>
);
}
この save が出力する HTML は <p class="notice-box__text">…</p> です。block.json で "selector": "p.notice-box__text" と書いておいたので、次に投稿を開いたときはこの要素の中身が content に読み戻されます。edit の tagName と className、save の tagName と className、そして selector。この 3 か所は必ずそろえてください。
なお動的ブロックで PHP から出力する場合、RichText の値はタグを含む文字列なので esc_html() を通すとタグが表示されてしまいます。投稿本文と同じ範囲のタグを許可する wp_kses_post() を使うのが定石です。
<div <?php echo get_block_wrapper_attributes(); ?>>
<p class="notice-box__text">
<?php echo wp_kses_post( $attributes['content'] ); ?>
</p>
</div>
入力した値が保存されないときに見るところ
文字は入力できるのに、投稿を開き直すと消えている。あるいは編集画面が赤い枠で囲まれてエラーになる。属性まわりのつまずきは、原因がだいたい決まっています。
block.json に属性を宣言し忘れている
setAttributes( { subtitle: value } ) と書いても、block.json の attributes に subtitle がなければその値は保存されません。エラーも出ないので気づきにくく、「入力中は表示されるのにリロードすると消える」という症状になります。属性を増やしたときは、JavaScript 側だけでなく block.json を直したか、そして直したものがビルド後の build/block.json に反映されているかを確認してください。
save の出力と selector が食い違っている
source を付けた属性の値は、save が出力した HTML から読み戻されます。したがって selector に書いた要素が save の出力に存在しなければ、保存はできても復元できません。よくあるのは、selector を p.notice-box__text にしたまま save 側の tagName を "div" に変えてしまったり、クラス名だけリファクタリングしてしまったりするケースです。selector は save の出力に対する CSS セレクターだと意識しておくと、この食い違いは起きにくくなります。
配列やオブジェクトを直接書き換えている
array や object の属性でつまずきやすいのがこれです。attributes.items.push( newItem ) のように中身を直接いじってから setAttributes( { items: attributes.items } ) と渡しても、渡しているのは同じ配列なので変更が検知されず、画面が更新されないことがあります。属性は書き換えずに、新しい配列やオブジェクトを作って渡してください。
// NG: 元の配列を書き換えている
items.push( { label: '新しい項目' } );
setAttributes( { items } );
// OK: 新しい配列を作って渡す
setAttributes( { items: [ ...items, { label: '新しい項目' } ] } );
// OK: 一部だけ差し替えるときも新しい配列にする
setAttributes( {
items: items.map( ( item, i ) =>
i === index ? { ...item, label: newLabel } : item
),
} );
attributes や save を変えたあとに検証エラーが出る
「このブロックには、想定されていないエラーが含まれています」という表示は、投稿に保存されている HTML と、いまの save が生成する HTML が一致しないときに出ます。ブロックを開くたびに WordPress は保存済みの HTML と save の出力を突き合わせていて、そこにずれがあると壊れたものとして扱われるためです。tagName を変えた、クラス名を変えた、要素を1つ足した。どれもこのずれを生みます。
まだ誰も使っていない開発中のブロックなら、該当のブロックを削除して入れ直すのがいちばん早い解決です。すでに公開済みの投稿で使っているなら、古い attributes と save の組み合わせを deprecated として登録し、過去の HTML も読めるようにしたうえで新しい形へ移行します。attributes と save は、公開したあとは気軽に変えられないものだと考えておくとよいでしょう。
まとめ
attributes はブロックが保持するデータの定義で、block.json に type と default、必要に応じて source と selector を書いて宣言します。source を付けない属性はブロックコメントの JSON に、付けた属性は save が出力した HTML に保存され、selector をたどって読み戻されます。edit では attributes から値を受け取り、更新は必ず setAttributes を通します。直接代入しても再描画も保存も起きません。テキスト入力には RichText を使い、value と onChange をつないで、save 側では RichText.Content で書き出します。このとき tagName・className・selector をそろえておくことが、値が消えたり検証エラーになったりしないための一番のポイントです。