WordPressのブロック開発メモ その6 スロット

ブロックを扱う上で結構重要な概念がスロットです。
前回で説明したInspectorControlsBlockControlsなど(以下参照)、

WordPressのブロック開発メモ その5 ツールバーとサイドバー

コンポーネントを書いている場所と、実際に表示されている場所が異なるようなタイプのコンポーネントを実現する仕組みです。

スロットにはSlotFillのペアがあります。

SlotとFillは配置する側と配置される側の仕組みを提供します。
どういうことかというと、Fillコンポーネントの子要素は特定のルールに乗っ取ってSlotの場所にレンダリングされます、
Fill自身はその場に表示されません。

試した方が早いので。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { Fill, Slot, SlotFillProvider } from '@wordpress/components';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <SlotFillProvider>

                <p>Slot Fill のテストです。</p>

                <p>--- ここからがSlotですよ ---</p>
                <Slot />

                <p>--- ここからがFillですよ ---</p>
                <Fill>Fill A!</Fill>
                <Fill>Fill B!</Fill>
                <p>Not Fill C!</p>
                <Fill>Fill D!</Fill>

            </SlotFillProvider>

        </div>
    );
}

どうなりました?

ちょっと見づらいですがFillは本来あるべき場所に表示されてません。
代わりにSlotのある場所に表示されています。

このようにFillSlot側に表示されます。

それぞれSlot(配置する側)とFill(配置される側(DOMが))の関係にあります。
内部的にはFillcreatePortal()を使っているようです。

SlotFillProviderはスロットを使用するコンテクストです。
コンポーネントツリーの結構上位のほうにすでに配置されているっぽいので一見なくても使えそうですが問題も出てきます。
詳しくは最後のほうで紹介します。

前回使ったBlockControls,やInspectorControlsは自身がFillでラップされるコンポーネントになってます。
面倒くさいので詳しく調べてませんがどこかにそれらをホストするSlotが配置されているはずです(いい加減なこといってたらごめんなさい)

仕様?

いくつか適当なコードを作っていきます。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { SlotFillProvider, Button, Slot, Fill } from '@wordpress/components';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                </div>
                <Fill>
                    Meeeee!
                </Fill>
            </SlotFillProvider>
        </div>
    );
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                </div>
                <Fill>Meeeee!</Fill>
                <Fill>Nooooooooo!</Fill>
            </SlotFillProvider>
        </div>
    );
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                    <Button variant="link">Click <Slot /></Button>
                    <Button variant="primary">Click <Slot /></Button>
                </div>
                <Fill>Meeeee!</Fill>
                <Fill>Nooooooooo!</Fill>
            </SlotFillProvider>
        </div>
    );
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                    <Button variant="link">Click <Slot name="no" /></Button>
                    <Button variant="primary">Click <Slot /></Button>
                </div>
                <Fill>Meeeee!</Fill>
                <Fill name="no">Nooooooooo!</Fill>
            </SlotFillProvider>
        </div>
    );
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                    <Button variant="link">Click <Slot name="no" /></Button>
                    <Button variant="primary">Click <Slot name="no" /></Button>
                </div>
                <Fill>Meeeee!</Fill>
                <Fill name="no">Nooooooooo!</Fill>
            </SlotFillProvider>
        </div>
    );
}

なるほど、名前無しのFillは同じく名前なしのSlotに配置されるようです。

次に別コンポーネントにFillをまとめてみました。

const MyFill = props =>
{
    return (
        <>
            <Fill name="message">
                Hello Kurage!
            </Fill>

            <Fill name="description">
                クラゲは海中をプカプカ浮いてます。中には電撃も放つ電気クラゲ科もいます。嘘です。
            </Fill>
        </>
    )
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>

                <div>
                    <p>Message</p>
                    <div>
                        <h2>
                            <Slot name="message" />
                        </h2>
                    </div>

                    <p>Description</p>
                    <div>
                        <p>
                            <Slot name="description" />
                        </p>
                    </div>

                </div>

                <MyFill />

            </SlotFillProvider>
        </div>
    );
}

自身を含めた子孫にFillがあればそれがスロットに置き換わります。
MyFill側で自在に表示する内容を変更出来るようになります。
以前のBlockControlsInspectorControlsがどうなってるのか大体想像つくと思います。

複数の同名のFillがある場合

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">Click <Slot /></Button>
                    <Button variant="primary">
                        Click
                        <Slot name="no">
                            {fills => (<>{fills}</>)}
                        </Slot>
                    </Button>
                </div>
                <Fill>Meeeee!</Fill>
                <Fill name="no">Nooooooooo!</Fill>
                <Fill name="no">Wiiiiiiiiiiiii!</Fill>
                <Fill name="no">Weeeeeeeeeeeeeeeeeeee!</Fill>
            </SlotFillProvider>
        </div>
    );
}

SlotでFillのリストを受け取ることもできます。

Slot側からデータを渡す

Slot側のプロパティをFillで受け取ることも出来ます。

import { wordpress } from '@wordpress/icons';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>
                <div>
                    <Button variant="primary">
                        <Slot name="no" fillProps={ { clickLabel: 'Click me!' } }>
                            {fills => (<>{fills}</>)}
                        </Slot>
                    </Button>
                </div>
                <Fill name="no">
                    { (props => (<> <Icon icon={wordpress} /> {props.clickLabel} </>)) as any }
                </Fill>
            </SlotFillProvider>
        </div>
    );
}

  • Slot.propsclickLabel: 'Click me!'を指定。
  • Fillの内側でそのデータをpropsで取得

createSlotFill()

Slot, Fillの両方に名前付けるの面倒な時はcreateSlotFill()を使うと便利です。
名前を付けた状態のSlot及びFillを返してくれます。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { createSlotFill, SlotFillProvider } from '@wordpress/components';

const { Fill, Slot } = createSlotFill('me');

const FillItem = () => <Fill><p>Hello Kurage!</p></Fill>

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>

                <div style={{borderWidth: 1, borderStyle: 'solid'}}>
                    <Slot />
                </div>

                <p>---------</p>

                <FillItem />

            </SlotFillProvider>
        </div>
    );
}

お作法

Slot/Fillを使うときはcreateSlotFill()を使ってコンポーネントとそこから生やすSlotでセットにするのがお作法の用です。

// Slot & Fill ペア
const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>

                <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>
                    <MyCustomBlock.Slot />
                </div>

                <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>

                    <p>A</p>
                    <MyCustomBlock><p>AAAAAAAAAAAA</p></MyCustomBlock>

                    <p>B</p>
                    <MyCustomBlock><p>BBBBBBBBBBBBB</p></MyCustomBlock>

                    <p>C</p>
                    <MyCustomBlock><p>CCCCCCCCCCCCCC</p></MyCustomBlock>

                </div>

            </SlotFillProvider>
        </div>
    );
}

配置される側のMyCustomBlockMyCustomBlock.Slot配下に移動しているのが分かります。

纏めると以下のようになります。

const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;
  • createSlotFill()FillSlotを作ります。
  • MyCustomBlockは子要素がFillにラップされるコンポーネント
  • MyCustomBlock.Slotは生やしたSlotにSlotコンポーネント代入。

そして利用する時は、

配置する場所に次のように。

配置される側は次のように

配置されちゃうよ!

仕上げにSlot/Fillは次のように別ファイルに押し出す

import { createSlotFill } from "@wordpress/components";
import React from "react";

// Slot & Fill ペア
const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;

export default MyCustomBlock;

これがお作法的なやりかた見たいです。

前回のBlockControlsInspectorControlsなどもこの作法で作られています、

他にも本体にはいくつかこうした感じのコンポーネントが用意してあります。

[center-ad]

ここから先はもっと奥に踏み込んだ内容です。
コードが長いので面倒なら飛ばしてください。

まねまね

これまでの知識でちょっと大きめのコード書きました。

面倒な方はこの項目は飛ばしてください。

InspectorControlsっぽいものを再現してみました。
実際にはこんな単純じゃないですが。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { Button, CheckboxControl, createSlotFill } from '@wordpress/components';
import { useState } from '@wordpress/element'

// Slot & Fill ペア
const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;

// Slotを描画するホスト
const MyCustomBlockHost = props => (
    <div style={{ borderWidth: 1, borderStyle: 'dotted', borderColor: 'red', margin: 3 }}>
        <h2> Costom Blocks! </h2>
        <MyCustomBlock.Slot>
            { fills => <div>{fills}</div>}
        </MyCustomBlock.Slot>
    </div>
);

// ブロックA
const BlockA = () => <MyCustomBlock><Button variant="primary">A Block! Click me!</Button></MyCustomBlock>;

// ブロックB
const BlockB = () => (
    <>
        <p>B Block</p>
        <MyCustomBlock><p>I am B</p></MyCustomBlock>
    </>
);

// ブロックC
const BlockC = () => {
    const [check, setCheck] = useState(true);
    return (
        <>
            <p>C - Block start</p>

            <MyCustomBlock>
                <CheckboxControl checked={check} onChange={v => setCheck(v)} label="Check C"/>
            </MyCustomBlock>

            <p>C - Block end</p>
        </>
    )
}

const BlockList = () =>
{
    return (
        <div style={{ borderWidth: 1, borderStyle: 'solid', borderColor: 'green', margin: 3 }}>
            <h2>Blocks</h2>
            <BlockA />
            <BlockB />
            <BlockC />
        </div>
    )
}

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>
            <SlotFillProvider>

                <MyCustomBlockHost />

                <BlockList />

            </SlotFillProvider>
        </div>
    );
}

Slot/Fillのペア作成

const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;

今回はこのMyCustomBlockInspectorControlsに見立てます。

Slotをホスト

const MyCustomBlockHost = props => (
    <div style={{ borderWidth: 1, borderStyle: 'dotted', borderColor: 'red', margin: 3 }}>
        <h2> Costom Blocks! </h2>
        <MyCustomBlock.Slot>
            { fills => <div>{fills}</div>}
        </MyCustomBlock.Slot>
    </div>
);

MyCustomBlockHostSlotをホストします。
Fill一覧をここに出力しますね。

ブロック一覧

const BlockList = () =>
{
    return (
        <div style={{ borderWidth: 1, borderStyle: 'solid', borderColor: 'green', margin: 3 }}>
            <h2>Blocks</h2>
            <BlockA />
            <BlockB />
            <BlockC />
        </div>
    )
}

このBlockListを編集ページとみなし、
それぞれ(BlockA, BlockB, BlockC)をeditブロックとみなします。

とりあえずBlockCを見ます。

const BlockC = () => {
    const [check, setCheck] = useState(true);
    return (
        <>
            <p>C - Block start</p>

            <MyCustomBlock>
                <CheckboxControl checked={check} onChange={v => setCheck(v)} label="Check C"/>
            </MyCustomBlock>

            <p>C - Block end</p>
        </>
    )
}

それぞれのブロックは順番に緑枠に表示されていますが、
MyCustomBlockで囲まれた部分は赤枠に表示されていることが分かります。

ブロックの一部分をサイドバーやツールバーに表示する仕組みでした。

SlotFillProviderはいるの? いらないの?

今までSlotFillProviderを使ってきました。
でも、これ無くても表示はされるっぽいです。

正確な情報ではないので鵜呑みにはしないでください。

これの正体はcreateContext()で作成したSlotFillContextProviderになります。

以下がSlotFillContextです。

以下がSlotFillProviderの一部です。

constructor() {
    super( ...arguments );

    this.registerSlot = this.registerSlot.bind( this );
    this.registerFill = this.registerFill.bind( this );
    this.unregisterSlot = this.unregisterSlot.bind( this );
    this.unregisterFill = this.unregisterFill.bind( this );
    this.getSlot = this.getSlot.bind( this );
    this.getFills = this.getFills.bind( this );
    this.hasFills = this.hasFills.bind( this );
    this.subscribe = this.subscribe.bind( this );

    this.slots = {};
    this.fills = {};
    this.listeners = [];
    this.contextValue = {
        registerSlot: this.registerSlot,
        unregisterSlot: this.unregisterSlot,
        registerFill: this.registerFill,
        unregisterFill: this.unregisterFill,
        getSlot: this.getSlot,
        getFills: this.getFills,
        hasFills: this.hasFills,
        subscribe: this.subscribe,
    };
}

render() {
    return (
        <SlotFillContext.Provider value={ this.contextValue }>
            { this.props.children }
        </SlotFillContext.Provider>
    );
}

コンポーネントの階層を表示してくれるやつで編集ページの階層を調べると、

ブロックエディタよりかなり上位に、というかてっぺん付近にこのコンテクストプロバイダらしきノードがあります。

一見使わなくてよさそうに見えますが!

次のような場合どうなるでしょう!?

SlotFillProviderを除外しました。

const { Fill, Slot } = createSlotFill('MyCustomBlock');
export const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

                <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>
                    <MyCustomBlock.Slot />
                </div>

                <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>

                    <p>A</p>
                    <MyCustomBlock><p>AAAAAAAAAAAA</p></MyCustomBlock>

                    <p>B</p>
                    <MyCustomBlock><p>BBBBBBBBBBBBB</p></MyCustomBlock>

                </div>

        </div>
    );
}

そして同じブロックを追加して3つにした場合です。
それぞれMyCustomBlockの子要素3つ分が最後のブロックのスロットに集中してます。

まー、コンテクストのスコープが他のブロックと合わさってしまったので当然の結果でした。

もしスロットをより詳しく知りたい人は、スロットを裏で支えるプラグインについて解説してます。

WordPressのブロック開発メモ その6-1 スロットとプラグイン

もうスロットはおなか一杯な人は、次回は高階コンポーネントについてです。

WordPressのブロック開発メモ その7 HOC(高階コンポーネント)

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク