1. ホーム
  2. WordPress

【WordPress】ToggleControl・SelectControl・RangeControl の使い方|カスタムブロックの設定UIを作る方法

Share

カスタムブロックに属性(attributes)を用意しても、それを編集者が触れる UI がなければ値は変わりません。設定サイドバーにオン・オフのスイッチやプルダウン、スライダーを並べて、コードを書かない人でも見た目を調整できるようにするのが @wordpress/components のコントロールです。この記事では、そのなかでも使用頻度の高い ToggleControl・SelectControl・RangeControl の3つを取り上げ、それぞれの props の意味から、block.json の属性定義・edit.js の UI・save.js の出力までを一本のブロックとして組み立てる流れを解説します。カスタムブロック開発を始めたばかりの方に向けた内容です。

設定コントロールは属性と編集者をつなぐ部品

ブロックの状態はすべて属性に入っています。「見出しを表示するか」「枠線のスタイルはどれか」「内側の余白は何 px か」といった情報を block.json の attributes に定義し、edit の中で setAttributes() を呼んで書き換え、save がその値を使って HTML を組み立てる、という流れです。

このとき setAttributes() を呼ぶきっかけになるのが設定コントロールです。ToggleControl はスイッチが切り替わったとき、SelectControl は選択肢が選ばれたとき、RangeControl はスライダーが動いたときに、それぞれ新しい値を渡してくれます。つまりコントロールは「属性を編集者の操作で書き換えるための入力欄」であり、値の管理そのものはこちらが属性で行う、という関係になります。

置き場所は @wordpress/block-editor の InspectorControls の中、さらに PanelBody で囲んだ内側が基本です。InspectorControls が右サイドバーへの差し込み口、PanelBody が開閉できるグループの枠、その中身が今回の3つのコントロールという構成になります。

3つのコントロールに共通する形

3つとも @wordpress/components からインポートして使います。書き方はほぼ共通で、「表示するラベル」「現在の値」「値が変わったときに呼ばれる関数」の3点を渡すだけです。ただし現在の値を渡す prop 名が ToggleControl だけ異なるので、そこが最初のつまずきどころになります。

コントロール対応する属性の型現在の値を渡す proponChange に渡ってくる値
ToggleControlbooleanchecked切り替え後の true / false
SelectControlstringvalue選ばれた選択肢の value(文字列)
RangeControlnumbervalueスライダーの数値

そのほか、3つに共通して使える prop が label・help・disabled・className です。label はコントロールの上(ToggleControl はスイッチの横)に出る名前で、help はその下に小さく表示される補足説明です。設定の意図が名前だけで伝わらないときは help に一文添えておくと、あとから触る人が迷いません。

src/edit.js(インポート)
import { InspectorControls } from '@wordpress/block-editor';
import {
    PanelBody,
    ToggleControl,
    SelectControl,
    RangeControl,
} from '@wordpress/components';

InspectorControls だけがパッケージ違いです。3つのコントロールを @wordpress/block-editor から読み込もうとすると undefined になり、描画した瞬間にエディターが真っ白になるので、インポート元は分けて書いてください。

__nextHasNoMarginBottom を付けておく

この3つには __nextHasNoMarginBottom という prop があります。@wordpress/components のコントロールは長らく下方向のマージンを自前で持っていましたが、余白はレイアウト側で管理するほうが扱いやすいため、それを取り除く方向へ移行が進んでいます。この prop はその新しいスタイルを先に有効にするためのオプトインです。

付けずに使うと、WordPress 6.7 以降のブロックエディターではブラウザーのコンソールに「__nextHasNoMarginBottom を true にして新しいスタイルへ移行してほしい」という趣旨の非推奨(deprecated)警告が出ます。動作が止まるわけではありませんが、新しく書くコードでは付けておくと、将来デフォルトが切り替わったときに見た目がずれずに済みます。JSX では __nextHasNoMarginBottom と prop 名だけを書けば true を渡したことになります。以降のコード例でもすべて付けています。

ToggleControl — 真偽値の設定をスイッチで切り替える

ToggleControl は、オン・オフを切り替えるスイッチを描画します。「見出しを表示する」「リンクを新しいタブで開く」のように、あるかないかで決まる設定に使います。組み合わせる属性は type: "boolean" です。

src/block.json(属性の定義)
"attributes": {
  "showTitle": {
    "type": "boolean",
    "default": true
  }
}
src/edit.js(抜粋)
<ToggleControl
    __nextHasNoMarginBottom
    label={ __( '見出しを表示', 'my-blocks' ) }
    help={
        showTitle
            ? __( '見出しを表示しています', 'my-blocks' )
            : __( '見出しを隠しています', 'my-blocks' )
    }
    checked={ showTitle }
    onChange={ ( value ) => setAttributes( { showTitle: value } ) }
/>

ポイントは現在の状態を checked で渡すことです。ほかのコントロールと同じつもりで value={ showTitle } と書いてしまうと、スイッチは常にオフの見た目のまま、クリックしても表示が変わらない(正確には状態が反映されない)という挙動になります。onChange には切り替え後の真偽値がそのまま渡ってくるので、受け取った値をそのまま属性に入れれば十分で、! showTitle のように自分で反転させる必要はありません。

上の例のように help は関数(あるいは現在の状態から算出した文字列)を渡して、オン・オフで文言を変えることもできます。コアブロックでも「代替テキストを設定する」などの説明で使われている書き方です。

SelectControl — 決まった選択肢から1つ選ばせる

SelectControl はプルダウン(select 要素)を描画します。「枠線のスタイル」「見出しレベル」のように候補が決まっている設定に向いています。選択肢は options に { label, value } のオブジェクトの配列で渡します。label が画面に表示される文字、value が実際に属性へ保存される値です。

src/edit.js(抜粋)
<SelectControl
    __nextHasNoMarginBottom
    label={ __( 'ボックスのスタイル', 'my-blocks' ) }
    value={ boxStyle }
    options={ [
        { label: __( '実線', 'my-blocks' ), value: 'solid' },
        { label: __( '破線', 'my-blocks' ), value: 'dashed' },
        { label: __( '塗りつぶし', 'my-blocks' ), value: 'filled' },
    ] }
    onChange={ ( value ) => setAttributes( { boxStyle: value } ) }
/>

よく使う props は次のとおりです。

prop説明
options選択肢の配列。各項目は label(表示名)と value(保存される値)を持つ。disabled: true を付けた項目は選べなくなる
value現在選ばれている値。options のどれかの value と一致させる
onChange選択されたときに呼ばれる関数。選ばれた value が渡る
multipletrue にすると複数選択できるリストになる。value には配列を渡し、onChange にも配列が渡る
helpプルダウンの下に表示される補足説明
hideLabelFromVisiontrue にするとラベルを画面から隠し、スクリーンリーダーにだけ読ませる

options を使わず、子要素として option や optgroup を直接書くこともできます。選択肢をグループ分けして見せたいときはそちらが便利です。

数値を選ばせるときは文字列で戻ることに注意

SelectControl は HTML の select 要素をそのまま使うため、onChange に渡ってくるのは常に文字列です。options に value: 2 と数値で書いていても、選択された時点で '2' という文字列になります。

そのため、属性を type: "number" で定義している場合は変換が必要です。変換せずに文字列を入れると、属性の型と実際の値が食い違い、ブロックを読み込み直したときに値が既定値へ戻ってしまうことがあります。

src/edit.js(数値の選択肢を扱う)
<SelectControl
    __nextHasNoMarginBottom
    label={ __( '見出しレベル', 'my-blocks' ) }
    value={ String( headingLevel ) }
    options={ [
        { label: 'H2', value: '2' },
        { label: 'H3', value: '3' },
        { label: 'H4', value: '4' },
    ] }
    // 文字列で渡ってくるので数値に変換して保存する
    onChange={ ( value ) =>
        setAttributes( { headingLevel: Number( value ) } )
    }
/>

value 側も String() で文字列に揃えているのがポイントです。数値のまま渡しても多くの環境では選択状態になりますが、比較の型を揃えておくほうが確実です。

RangeControl — 数値をスライダーで調整する

RangeControl はスライダーと数値入力欄を組み合わせたコントロールで、余白・角丸・不透明度のように連続した数値を指定させたいときに使います。範囲は min と max、刻みは step で決めます。

src/edit.js(抜粋)
<RangeControl
    __nextHasNoMarginBottom
    label={ __( '内側の余白(px)', 'my-blocks' ) }
    value={ spacingSize }
    onChange={ ( value ) => setAttributes( { spacingSize: value } ) }
    min={ 0 }
    max={ 80 }
    step={ 4 }
    allowReset
    initialPosition={ 16 }
/>
prop説明
min / max指定できる数値の下限と上限。既定値は 0 と 100
step値が変化する最小単位。'any' を指定すると刻みの制限がなくなる
allowResettrue にすると値をリセットするボタンが表示される(既定は false)
resetFallbackValueリセットボタンを押したときに戻す値。指定しなければリセット時は undefined になる
initialPositionvalue が未設定のときにスライダーのつまみを置く位置
withInputFieldスライダーの隣に数値入力欄を出すかどうか
marksstep に合わせた目盛りを表示する。配列を渡すと任意の位置にラベル付きの目盛りを置ける

値が未設定(undefined)のときの扱い

RangeControl で気をつけたいのは undefined の扱いです。属性に default を書かなければ初期状態の値は undefined で、allowReset のリセットボタンを押したときも(resetFallbackValue を指定していなければ)undefined に戻ります。initialPosition はあくまでつまみの見た目の位置を決めるだけで、属性に値が入るわけではありません。

そのため save 側では「値が入っていない場合」を必ず考慮します。undefined を素朴に文字列連結すると padding: undefinedpx という壊れた CSS が出力されてしまうので、値の有無で分岐させてください。逆に、常に何らかの値が入っていてほしい設定なら、block.json の属性に default を書いておくほうが分岐が減って楽になります。

3つを組み合わせた実践例:見出し付きボックスブロック

ここまでの3つを1つのブロックにまとめてみます。作るのは「見出しの表示・非表示(ToggleControl)」「枠のスタイル(SelectControl)」「内側の余白(RangeControl)」を設定できるボックスブロックです。まずは属性の定義から。

src/block.json
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-blocks/notice-box",
  "title": "お知らせボックス",
  "category": "design",
  "icon": "info",
  "textdomain": "my-blocks",
  "attributes": {
    "heading": {
      "type": "string",
      "default": ""
    },
    "showTitle": {
      "type": "boolean",
      "default": true
    },
    "boxStyle": {
      "type": "string",
      "default": "solid"
    },
    "spacingSize": {
      "type": "number"
    }
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

heading に source を指定していない点に触れておきます。source: "html" を使うと保存済みの HTML から値を読み戻せますが、今回は showTitle が false のときに見出しの要素そのものを出力しません。読み戻す先の要素が HTML に無いと値が失われてしまうため、source を付けずにブロックコメントへ値を保存する形にしています。条件によって出力を消す属性は、HTML から読み戻さないと覚えておくと安全です。

続いて edit.js です。InspectorControls の中に PanelBody を置き、その中に3つのコントロールを並べます。

src/edit.js
import { __ } from '@wordpress/i18n';
import {
    useBlockProps,
    InspectorControls,
    RichText,
} from '@wordpress/block-editor';
import {
    PanelBody,
    ToggleControl,
    SelectControl,
    RangeControl,
} from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { heading, showTitle, boxStyle, spacingSize } = attributes;

    // 未設定のときは padding を出力しない
    const boxProps = useBlockProps( {
        className: `notice-box is-${ boxStyle }`,
        style:
            spacingSize === undefined
                ? undefined
                : { padding: `${ spacingSize }px` },
    } );

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( '表示設定', 'my-blocks' ) }>
                    <ToggleControl
                        __nextHasNoMarginBottom
                        label={ __( '見出しを表示', 'my-blocks' ) }
                        checked={ showTitle }
                        onChange={ ( value ) =>
                            setAttributes( { showTitle: value } )
                        }
                    />
                    <SelectControl
                        __nextHasNoMarginBottom
                        label={ __( 'ボックスのスタイル', 'my-blocks' ) }
                        value={ boxStyle }
                        options={ [
                            { label: __( '実線', 'my-blocks' ), value: 'solid' },
                            { label: __( '破線', 'my-blocks' ), value: 'dashed' },
                            { label: __( '塗りつぶし', 'my-blocks' ), value: 'filled' },
                        ] }
                        onChange={ ( value ) =>
                            setAttributes( { boxStyle: value } )
                        }
                    />
                    <RangeControl
                        __nextHasNoMarginBottom
                        label={ __( '内側の余白(px)', 'my-blocks' ) }
                        help={ __( '未設定のときは CSS の値が使われます', 'my-blocks' ) }
                        value={ spacingSize }
                        onChange={ ( value ) =>
                            setAttributes( { spacingSize: value } )
                        }
                        min={ 0 }
                        max={ 80 }
                        step={ 4 }
                        allowReset
                        initialPosition={ 16 }
                    />
                </PanelBody>
            </InspectorControls>

            <div { ...boxProps }>
                { showTitle && (
                    <RichText
                        tagName="h3"
                        className="notice-box__title"
                        value={ heading }
                        onChange={ ( value ) =>
                            setAttributes( { heading: value } )
                        }
                        placeholder={ __( '見出しを入力', 'my-blocks' ) }
                    />
                ) }
                <p className="notice-box__body">
                    { __( '本文はここに入ります', 'my-blocks' ) }
                </p>
            </div>
        </>
    );
}

3つのコントロールはいずれも onChange でアロー関数を渡し、その中で setAttributes() を呼んでいます。ここで onChange={ setAttributes( { ... } ) } と書いてしまうと、関数ではなく実行結果を渡すことになり、レンダリングのたびに属性を更新しようとして正しく動きません。必ずアロー関数で包むのが約束事です。

最後に save.js で、選ばれた値を実際の HTML に反映します。edit と同じ組み立て方にしておくと、エディターの見た目と公開ページが一致します。

src/save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const { heading, showTitle, boxStyle, spacingSize } = attributes;

    const boxProps = useBlockProps.save( {
        className: `notice-box is-${ boxStyle }`,
        style:
            spacingSize === undefined
                ? undefined
                : { padding: `${ spacingSize }px` },
    } );

    return (
        <div { ...boxProps }>
            { showTitle && (
                <RichText.Content
                    tagName="h3"
                    className="notice-box__title"
                    value={ heading }
                />
            ) }
            <p className="notice-box__body">本文はここに入ります</p>
        </div>
    );
}

ToggleControl の値は { showTitle && ( ... ) } という条件付きレンダリングに、SelectControl の値は is-solid のようなクラス名に、RangeControl の値はインラインスタイルに、という形で使い分けています。SelectControl の値をクラス名にしておけば、あとは CSS 側で .notice-box.is-dashed などのスタイルを用意するだけで見た目を切り替えられます。

なお、余白や枠線が「そのブロックならではの設定」でないなら、自前のコントロールを作らず block.json の supports に spacing や border を書いてエディター標準の UI に任せるほうが手軽で、テーマとの相性もよくなります。自作コントロールは、標準の UI では表現できない設定に絞るのがおすすめです。

コントロールを操作しても表示が変わらないとき

ToggleControl に value を渡している

いちばん多いのがこれです。ToggleControl の状態は checked で渡すため、value={ showTitle } と書くとスイッチには何も伝わりません。onChange 自体は動くので属性は更新されるのですが、スイッチの見た目が変わらないため「クリックしても反応しない」ように見えます。エラーも出ないので気づきにくい不具合です。

onChange を渡していない、または属性名が違う

value(または checked)だけを渡して onChange を書き忘れると、値が外から固定された状態になり、操作しても何も起きません。onChange の中で更新している属性名が block.json の定義と1文字でも違う場合も同じ症状になります。setAttributes() は定義されていないキーを黙って無視するため、タイプミスに気づくのが遅れがちです。まずは onChange の中に console.log( value ) を入れて、値そのものが来ているかを切り分けてください。

SelectControl の値を数値と比較している

プルダウンから返ってくる値は文字列なので、if ( headingLevel === 2 ) のような厳密等価での比較は常に false になります。属性が type: "number" なら onChange で Number() に通し、逆に文字列で扱うなら比較する側も '2' と文字列で書く、というように型を片方に統一するのが確実です。

RangeControl の値が undefined のまま CSS に渡っている

default のない数値属性は初期値が undefined です。これを padding: `${ spacingSize }px` のように連結すると undefinedpx という無効な値になり、ブラウザーがその宣言を丸ごと捨てるため「スライダーを動かすまで何も効かない」状態になります。値の有無で分岐するか、属性に default を設定してください。

エディターでは変わるのに公開ページで変わらない

edit.js にだけ反映して save.js を書き換えていないケースです。属性は保存されているので、エディターを開き直すと設定は残っています。フロントエンドの HTML を見てクラス名や style 属性が出ているかを確認すれば、どちら側の問題かすぐ分かります。あわせて、wp-scripts のビルドが走っているかも確認してください。npm run start を止めたまま編集していると、古い build の JavaScript が読み込まれ続けます。

また、save.js の出力を変えると、すでに投稿へ保存されている HTML と食い違って「このブロックには、想定されていないエラーが含まれています」というブロック検証エラーが出ます。開発中で使用中の記事がなければブロックを置き直すのが手っ取り早く、公開済みの記事がある場合は deprecated に古い save を登録して移行します。

まとめ

ToggleControl・SelectControl・RangeControl は、いずれも @wordpress/components からインポートし、InspectorControls と PanelBody の中に置いて使います。書き方は「label でラベル、現在の値、onChange で属性を更新」という共通の形で、現在の値を渡す prop が ToggleControl だけ checked、ほかは value という違いだけ押さえておけば迷いません。

属性の型との対応は ToggleControl が boolean、SelectControl が string(数値を扱うなら Number() で変換)、RangeControl が number です。RangeControl は未設定やリセット時に undefined になるため、save 側で値の有無を分岐させるか、block.json に default を書いておきましょう。block.json で属性を定義し、edit.js でコントロールを置き、save.js で出力に反映する——この3つのファイルを常にセットで考えるのが、設定 UI を作るときのいちばんの近道です。

参考ページ