WordPressのブロック開発メモ その12 ブロックの作成とシリアライズ/デシリアライズ

前回は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/paragraphcreate-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で投稿を取得する時contexteditモードで取得すること。
ブロックへの変換を自前でしたいので生のデータを受け取るために。

どういうこと?

クエリでcontextを指定しないとREST APIはデフォルトでcontextviewと判断する

/wp/v2/posts/436

そこでcontexteditに変更する。

/wp/v2/posts/436?context=edit

両者の違いをブラウザから見てみると、

投稿のcontentから取得出来る内容に違いがある。
editモードでアクセスするとraw(生のデータ)が取得出来るようになる。

ちなみにapiFetch()と違い、@wordpress/block-editorgetEntityRecord()はデフォルトで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-blockclientIdで受け取ります。

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を固定してハードコーディングしましたが、
投稿一覧を引っ張ってきてリストから選ぶみたいに応用も出来そうです。

次はブロックの変換についてです。

WordPressのブロック開発メモ その13 ブロックの変換

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