ブロックを扱う上で結構重要な概念がスロットです。
前回で説明したInspectorControls
やBlockControls
など(以下参照)、
WordPressのブロック開発メモ その5 ツールバーとサイドバー
コンポーネントを書いている場所と、実際に表示されている場所が異なるようなタイプのコンポーネントを実現する仕組みです。
スロットにはSlot
とFill
のペアがあります。
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
のある場所に表示されています。
このようにFill
はSlot
側に表示されます。
それぞれSlot(配置する側)とFill(配置される側(DOMが))の関係にあります。
内部的にはFill
はcreatePortal()
を使っているようです。
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
側で自在に表示する内容を変更出来るようになります。
以前のBlockControls
やInspectorControls
がどうなってるのか大体想像つくと思います。
複数の同名の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.props
にclickLabel: '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>
);
}
配置される側のMyCustomBlock
はMyCustomBlock.Slot
配下に移動しているのが分かります。
纏めると以下のようになります。
const { Fill, Slot } = createSlotFill('MyCustomBlock');
const MyCustomBlock = (props: any) => <Fill>{props.children}</Fill>;
MyCustomBlock.Slot = Slot;
createSlotFill()
でFill
とSlot
を作ります。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;
これがお作法的なやりかた見たいです。
前回のBlockControls
やInspectorControls
などもこの作法で作られています、
他にも本体にはいくつかこうした感じのコンポーネントが用意してあります。
[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;
今回はこのMyCustomBlock
をInspectorControls
に見立てます。
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>
);
MyCustomBlockHost
はSlot
をホストします。
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()
で作成したSlotFillContext
のProvider
になります。
以下が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 スロットとプラグイン
もうスロットはおなか一杯な人は、次回は高階コンポーネント
についてです。