前回はREST API
についてでした。
今回はブロックの作成と、前回のREST API
を応用した遊びをやってみようと思います。
WordPressのブロック開発メモ その11 REST API へのアクセス
ブロックの作成
コードからブロックを作成&追加することが出来ます。
まずはコアであらかじめ用意していある段落ブロック
で実験してみます。
段落ブロックはcore/paragraph
という名前で登録されています。
段落ブロックに入力するテキストの属性はcontent
になってます。
ブロックを作成するにはcreateBlock()
を、
ブロックを追加するにはinsertBlock()
を使います。
edit.tsx
import React from 'react';
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, createBlock } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { Button, TextControl } from '@wordpress/components';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
const [text, setText] = useState('');
// @ts-ignore
const { insertBlock } = dispatch(blockEditorStore);
const addParagraph = () =>
{
const paraBlock = createBlock(
'core/paragraph',
{
content: text
}
);
insertBlock(paraBlock);
}
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<p></p>
<div>
<TextControl value={text} onChange={setText} />
</div>
<Button variant="primary" onClick={() => addParagraph()}>ADD</Button>
</div>
);
}
自作ブロックを追加し、
TextControlに入力してADD
ボタンをクリックします。
Paragraph A
を入力してADD
ボタンを押すParagraph B
を入力してADD
ボタンを押すParagraph C
を入力してADD
ボタンを押す
この処理を終えると以下のように表示されます。
3つの段落が追加されていることが確認出来ます。
以下のように@wordpress/block-editor
からストアを取得します。
import { store as blockEditorStore } from '@wordpress/block-editor';
insertBlock
を取り出しておきます。
const { insertBlock } = dispatch(blockEditorStore);
ボタンをクリックしたら以下のようにcreateBlock()
で段落ブロック
を作成します。
作成しただけでは意味はないので、それをinsertBlock()
でストアに追加します。
const paraBlock = createBlock(
'core/paragraph',
{
content: text
}
);
insertBlock(paraBlock);
するとエディタの一番下に段落ブロックが追加されます。
createBlock()
の引数は第一引数がブロック名で、第二引数が属性(attributes
)の値、第三引数が子ブロックです。
ブロックは入れ子になっています。
createBlock()
がどんな値を返すんでしょう!?
ブラウザから覗いてみます。
wp.blocks.createBlock('core/paragraph');
自作ブロックの追加。
自作ブロックも追加出来ます。
属性にstring
型のmessage
を追加しました。
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "create-block/kurage-worker",
"version": "0.1.0",
"title": "Kurage Worker",
"category": "widgets",
"icon": "smiley",
"description": "Example block scaffolded with Create Block tool.",
"supports": {
"html": false
},
"attributes": {
"message": {
"type": "string",
"default": ""
}
},
"textdomain": "kurage-worker",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
props.ts
export default interface KurageExampleBlockProps
{
message: string;
}
edit.tsx
import React from 'react';
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, createBlock } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { Button, TextControl } from '@wordpress/components';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
const [text, setText] = useState('');
// @ts-ignore
const { insertBlock } = dispatch(blockEditorStore);
const copy = () =>
{
const block = createBlock(
'create-block/kurage-worker',
{
message: text
}
);
insertBlock(block);
}
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<p>{props.attributes.message}</p>
<div>
<TextControl value={text} onChange={setText} />
</div>
<Button variant="primary" onClick={() => copy()}>分身の術!</Button>
</div>
);
}
自作のブロックも同じように追加出来ました。
ブロック一覧の取得
ブロックを作成し、それをストアに追加する、
ということは逆にストアにあるブロック一覧を取得することもできるはずです。
とりあえずセレクタにgetBlocks()
がありますが、その前に自作ブロックの追加です。
さて、ブロック一覧を取得してみます。
wp.data.select('core/block-editor').getBlocks();
core/paragraph
やcreate-block/kurage-worker
などのブロック名が確認出来ます。
このclientId
がブロックを識別するもののようで、ストア内に親子情報などのデータを確認出来ました。
ブロックは入れ子が出来るツリー状態になってますが、このセレクタでは子ブロックまでは取得出来ません。
ツリー構造を再現してみる。
まず三つの段落ブロック
をグループ化します。
すると段落がグループ化されている(ブロック名はcore/group
)
ブロック一覧を覗いてみる
- トップレベルのブロックのみ取得出来ていることが分かる。
- グループブロック(
core/group
)のinnerBlocks
に三つの段落ブロック
があることが分かる。
getBlocks()
の引数に親ブロックのclientId
を渡すことでその子ブロック一覧を取得出来る。
この方法を使って再帰してみる。
function rec(pid = '', flat = [], level = 0)
{
const children = wp.data.select('core/block-editor').getBlocks(pid);
for(const child of children)
{
flat.push([child, level]);
rec(child.clientId, flat, level + 1);
}
return flat;
}
const flat = rec();
flat.map(([child, level]) => ' '.repeat(level) + `${child.name} ${child.clientId}`);
core/group
の子に三つのcore/paragraph
が存在する。
深さの分だけ空白を入れてツリー構造を再現してます。
シリアライズとデシリアライズ
ブロックデータをテキストに変換したり、戻したりすることが出来ます。
シリアライズ
edit.tsx
import React from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, serialize } from '@wordpress/blocks';
import { select } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import KurageExampleBlockProps from './props';
import './editor.scss';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
// @ts-ignore
const { getBlocks } = select(blockEditorStore);
const blocks = getBlocks();
const str = serialize(blocks);
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<p>{props.attributes.message}</p>
<pre>
{str}
</pre>
</div>
);
}
この投稿にある全ブロックを階層ごと文字列化してます。
@wordpress/blocks
からserialize
を引っ張ってきます。
serialize()
にブロックの配列を渡すことで文字列化出来ます。
わざわざブロックに表示せずコンソールログに出力したほうがよかったかも・・・。
次はcreateBlock()
で作成したものをシリアライズしてみます。
edit.tsx
import React from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, createBlock, serialize } from '@wordpress/blocks';
import { select } from '@wordpress/data';
import KurageExampleBlockProps from './props';
import './editor.scss';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
const blocks = [
createBlock('create-block/kurage-worker', { message: 'Kurage' }),
createBlock('core/paragraph', { content: 'Group Begin'}),
createBlock('core/group', {}, [
createBlock('core/paragraph', { content: 'Child A'}),
createBlock('core/paragraph', { content: 'Child B'}),
createBlock('core/paragraph', { content: 'Child C'}),
]),
createBlock('core/paragraph', { content: 'Group End'}),
];
const str = serialize(blocks);
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<p>{props.attributes.message}</p>
<pre>
{str}
</pre>
</div>
);
}
デシリアライズ
こんどは逆に文字列をブロックのリストに変換するparse()
を使ってみます。
edit.tsx
import React from 'react';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, createBlock, parse, serialize } from '@wordpress/blocks';
import { dispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { Button } from '@wordpress/components';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
// @ts-ignore
const { insertBlocks } = dispatch(blockEditorStore);
const parseTest = () =>
{
const blocks = [
createBlock('create-block/kurage-worker', { message: 'Kurage' }),
createBlock('core/paragraph', { content: 'Group Begin'}),
createBlock('core/group', {}, [
createBlock('core/paragraph', { content: 'Child A'}),
createBlock('core/paragraph', { content: 'Child B'}),
createBlock('core/paragraph', { content: 'Child C'}),
]),
createBlock('core/paragraph', { content: 'Group End'}),
];
const str = serialize(blocks);
const newBlocks = parse(str);
insertBlocks(newBlocks);
}
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<Button variant="primary" onClick={() => parseTest()}>実験!</Button>
</div>
);
}
ブロックを作成、シリアライズして、元に戻して、追加するというめんどいことやってます。
ブロックの配列を文字列に変換
const str = serialize(blocks);
文字列を解析しブロックの配列に変換
const newBlocks = parse(str);
ブロックエディタにブロックをまとめて追加
insertBlocks(newBlocks);
別の投稿をグループブロックに追加する例
ちょっと応用してみましょう。
まったく別の投稿をブロック内に追加してみるテストです。
まず、現在ブロック開発用とは別の投稿を適当に作成します。
前もって投稿IDを調べておきます。
この投稿でのIDは436
だった。
この別の投稿
をREST API
から取得してみましょう。
現在作成しているクラゲブロックの子に表示するという実験をしてみます。
apiFetch()
を使ってもいいですし、@wordpress/block-editor
のストアから引っ張ってきてもいいですが、
(await wp.apiFetch({path: wp.url.addQueryArgs('/wp/v2/posts/436', { context: 'edit'})}))?.content?.raw
wp.data.select('core').getEntityRecord('postType', 'post', 436);
ちょっと内部動作を考えると複雑なのでapiFetch()
を使うことにします。
注意すべきはREST APIで投稿を取得する時context
をedit
モードで取得すること。
ブロックへの変換を自前でしたいので生のデータを受け取るために。
どういうこと?
クエリでcontext
を指定しないとREST APIはデフォルトでcontext
をview
と判断する
/wp/v2/posts/436
そこでcontext
をedit
に変更する。
/wp/v2/posts/436?context=edit
両者の違いをブラウザから見てみると、
投稿のcontent
から取得出来る内容に違いがある。
edit
モードでアクセスするとraw
(生のデータ)が取得出来るようになる。
ちなみにapiFetch()
と違い、@wordpress/block-editor
でgetEntityRecord()
はデフォルトでedit
になっている。
では早速コード。
edit.tsx
import React from 'react';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps, createBlock, parse, serialize } from '@wordpress/blocks';
import { dispatch, select } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { Button } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
// @ts-ignore
const { insertBlocks } = dispatch(blockEditorStore);
// @ts-ignore
const { canInsertBlocks } = select(blockEditorStore);
// @ts-ignore
const { "data-block": clientId } = useBlockProps();
const parseTest = async () =>
{
// @ts-ignore
const result = await apiFetch({
path: addQueryArgs('/wp/v2/posts/436', { context: 'edit' })
});
// @ts-ignore
const rawContent = result?.content?.raw;
const newBlocks = parse(rawContent);
const groupBlock = createBlock(
'core/group',
{},
newBlocks
);
if(canInsertBlocks([clientId]))
{
insertBlocks([groupBlock], 0, clientId);
}
else
{
console.log('追加できませんでしたー!');
}
}
return (
<div {...useBlockProps()}>
<h2>Hello Kurage!</h2>
<p>ID: {clientId as any}</p>
<Button variant="primary" onClick={() => parseTest()}>実験!</Button>
<InnerBlocks />
</div>
);
}
現在のブロックのclientId
を取得します。
取得方法が分からなかったのですが、以下のように取得出来ました。
useBlockProps()
の戻り値のdata-block
をclientId
で受け取ります。
const { "data-block": clientId } = useBlockProps();
ボタンをクリックしたときの関数を定義します。
非同期関数になっていることに注意してください。
面倒なのでローディングとかの面倒なことは考えてません。
それを考えるならgetEntityRecord()
の利用などを検討してください。
const parseTest = async () =>
投稿ID436
の投稿をedit
モードで取得します。
// @ts-ignore
const result = await apiFetch({
path: addQueryArgs('/wp/v2/posts/436', { context: 'edit' })
});
以下の階層で生の投稿データを取得します。
// @ts-ignore
const rawContent = result?.content?.raw;
投稿データを解析しブロックの配列を作成
const newBlocks = parse(rawContent);
そのブロックの配列をコアのグループブロックに追加
const groupBlock = createBlock(
'core/group',
{},
newBlocks
);
canInsertBlocks()
でこのブロックが入れ子出来るブロック化をチェックします。
クラゲブロック(create-block/kurage-worker
)は入れ子に対応しているのでtrue
になります。
insertBlocks()
でさっきのグループブロックを追加します。
この時、第三引数でクラゲブロックのID(clientId
)を指定することで子のブロックに追加出来るようになります。
第二引数はインデックス(とりあえず先頭の0)を指定します。
if(canInsertBlocks([clientId]))
{
insertBlocks([groupBlock], 0, clientId);
}
入れ子に対応するためInnerBlocks
を追加します。
<InnerBlocks />
表示側にもInnerBlocks.Content
を追加します。
save.tsx
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import { BlockSaveProps } from '@wordpress/blocks';
import React from 'react';
import KurageExampleBlockProps from './props';
export default (props: BlockSaveProps<KurageExampleBlockProps>) =>
{
const p = useBlockProps.save();
return (
<div { ...useBlockProps.save() }>
<p>Hello Kurage !!</p>
<InnerBlocks.Content />
</div>
);
}
これで準備は整いました。
早速実験していきます。
一旦エディタ内のブロックを全部削除してクラゲブロックを一つだけ追加した状態にします(整理整頓)。
クラゲブロックは入れ子が出来る状態に変更してあります。
実験!
ボタンをクリックします。
二重実行防止とか一切考えてませんので注意してください。
すると別の投稿が子に追加されました。
リストを見ると思惑通りの構造になってます。
ちなみに別の投稿のデータベース上のデータです。
今回は投稿IDを固定してハードコーディングしましたが、
投稿一覧を引っ張ってきてリストから選ぶみたいに応用も出来そうです。
次はブロックの変換についてです。