- この内容は好奇心的なもので実践向きではありません!
Gutenbergのデータモジュールにコアなストアがあります。
コアストア(core
)にはREST APIを扱いやすくするEntitiesな機能(何という名前かわからない)があります。
REST APIの読み込みや書き込み、それだけではなくUndo/Redoの機能もあります。
例えば投稿記事に「段落ブロック」を追加して、Hello World
と入力したとします。
タイトルはEntities Undo/Redo test
にしたとします。
そして保存します。
Entitiesを使えば簡単にREST APIから引っ張ってこれます。
投稿記事のIDが「1153」の場合はブラウザのコンソールから以下のコードを実行します。
wp.data.select('core').getEntityRecord('postType', 'post', 1153)
ではタイトルをコードから編集したらどうでしょう!?
wp.data.dispatch('core').editEntityRecord('postType', 'post', 1153, { title: 'Kurage Beam!' })
これを実行するとタイトルが変更されます。
さて、タイトルがKurage Beam!
に変更されました。
では、この変更をコードから「元に戻す」にはどうすればいいでしょう!?
簡単です。
wp.data.dispatch('core').undo()
これで元のEntities Undo/Redo test
に戻ります。
今度は「やり直す」場合はどうでしょう!?
wp.data.dispatch('core').redo()
これだけです。
実はeditEntityRecord()
は内部でUndo/Redoのための変更内容を管理してます。
実際には@wordpress/undo-manager
というパッケージで管理してます。
現在のスナップショットはgetEditedEntityRecord()
で取得出来ます。
今回はその中のtitle
プロパティだけに注目します。
タイトルがKurage Beam!
の状態で連続して以下のコードを実行してみましょう。
wp.data.select('core').getEditedEntityRecord('postType', 'post', 1153).title
'Kurage Beam!'
wp.data.dispatch('core').undo()
wp.data.select('core').getEditedEntityRecord('postType', 'post', 1153).title
'Entities Undo/Redo test'
このようにコアストアではデータのUndo/Redoを管理出来ることが分かりました。
でもちょっと待ってください!?
そういえば、ブロックエディタで入力した内容もUndo/Redo出来ますよね!?
ブロックを作成するばあいコンポーネントはattributes
およびsetAttributes
を受け取ります。
ブロックで編集するということは、このattributes
を編集するということです。
ブロックを作成する時、どのようなコードを書くでしょうか?
export default ({attributes, setAttributes, clientId}) =>
{
return <h2>Hello World</h2>;
}
attributes
を使ってデータを取得し、setAttributes
によってデータを変更します。
何が不思議なのかというとこれらブロックの入力データはコアストア(core
)ではなく、
ブロックエディタストア(core/block-editor
)で管理されている点です。
ブロック一覧を取得してみましょう!
wp.data.select('core/block-editor').getBlocks()
段落ブロックを一つ追加しただけなのでブロックは一つしか取得出来ていません。
段落の内容はcontent
プロパティにあてられていて、そこからHello World
の文字列が確認できます。
ではsetAttributes
とは何なんでしょうか!?
このプロパティはブロックエディタコンポーネントの高階コンポーネント(createHigherOrderComponent()
)でプロパティを追加しています。
厳密にはBlockEditBlock
でwithDispatch()
してます(ずいぶん前に紹介したのでここでは省きます)。
厳密にはsetAttributes()
はupdateBlockAttributes()
を呼び出しています。
どういうことか、コードで見てみましょう。
wp.data.dispatch('core/block-editor').updateBlockAttributes("2aa59917-fe2f-4dbe-b114-246623f9bea4", { content: 'Hey Kurage!' })
段落ブロックの内容がHey Kurage!
に変更になっていることが確認できます。
ただし、段落の元のcontentはリッチエディタなのにただの文字列突っ込んでる点はご了承ください。
第一引数はgetBlocks()
から取得出来たclientId
です。
第二引数で変更するプロパティをリテラルオブジェクトで渡します。
では本題に入ります。
何故core(厳密にはeditEntityRecord())
の機能なのにcore/block-editor
ストアの変更を追跡できているのか!?
結論からいいます。
core/block-editor
のストアの変更を購読(subscribe)しており、その変更を随時core
のeditEntityRecord()
に渡しているみたいです。
逆にUndo/Redoの処理も随時監視していてcore/block-editor
に反映させているようです。
ただし、めっちゃ複雑です。
@wordpress/core-data
, @wordpress/editor
, @wordpress/block-editor
にまたがる壮大なコードになってます。
それを思いっきり簡素化してみました。
実際には誤作動おきます。
はい、こちら。
// packages/editor/src/components/editor/index.js
const Editor = () =>
{
const post = useSelect(s => s('core').getEntityRecord('postType', 'post', 1150);
<EditorProvider post={post} ...>
...
</EditorProvider>
}
// packages/editor/src/components/provider/index.js
const EditorProvider = () =>
{
const [ blocks, onInput, onChange ] = useEntityBlockEditor('postType', 'post', 1150);
return <BlockEditorProvider value={blocks} onChange={onChange} onInput={onInput} .../>
}
// packages/core-data/src/entity-provider.js
const useMyEntityBlockEditor = (kind, name, id) =>
{
const { editEntityRecord } = useDispatch('core');
const edited = useSelect(
// @ts-ignore
s => s('core').getEditedEntityRecord(kind, name, id),
[kind, name, id]
);
const value = useMemo(
() => edited.kurageState,
[kind, name, id, edited.kurageState]
);
const onChange = useCallback((state: number) => {
const edits = { kurageState: state };
editEntityRecord(kind, name, id, edits);
}, [kind, name, id])
return [value, onChange];
}
// packages/block-editor/src/components/provider/index.js
const BlockEditorProvider = (props) =>
{
// packages/block-editor/src/components/provider/use-block-sync.js
useBlockSync(props);
return ...
}
const useBlockSync = ({ clientId, onChange, onInput }) =>
{
const registory = useRegistry();
registry.subscribe(() =>
{
const blocks = getBlocks( clientId )
onChange(blocks, selection)
or
onInput(blocks, selection)
}, blockEditorStore)
}
まずはuseBlockSync({ clientId, onChange, onInput })
です。
この関数はcore/block-editor
を監視し、変更をonChangeコールバック
に渡したりします。
一方const [ blocks, onInput, onChange ] = useEntityBlockEditor('postType', 'post', 1150);
は
onChangeコールバック
を返しますが、このコールバックの引数で渡された内容をeditEntityRecord()
で編集します。
この流れは「core/block-editor
→ core
」の流れです。
その逆「core
→ core/block-editor
」はvalue
を監視することで行います。
ただし、このままでは無限ループにはならずとも思い通りのコードにはなりません。
実際にはとても複雑なしょりがされていて、ずべてのコードを追い切れているわけではありません。
もう値を上げました。
以下は一応動いた程度の実装をしたものです(多分バグとか含んでる)。
既に一番最初のUndo
では値が更新されないなどのバグがあります。
理由は分かってますが変更はしません。
import { useEntityBlockEditor } from "@wordpress/core-data";
import { combineReducers, createReduxStore, register, useDispatch, useRegistry, useSelect } from "@wordpress/data";
import { createContext, useCallback, useEffect, useMemo, useRef } from "@wordpress/element";
import React from "react";
import { Button } from "@wordpress/components";
// ------------------------- My Store
const stateManager = (state= 1, action) =>
{
switch(action.type)
{
case 'SET_STATE_MANAGER':
return action.value;
}
return state;
}
export const setStateManager = (value: number) =>
{
return ({
type: 'SET_STATE_MANAGER',
value
})
}
export const getStateManager = (state) =>
{
return state.stateManager;
}
const reducer = combineReducers({ stateManager });
export const store = createReduxStore(
'kurage/undo-tora',
{
reducer,
selectors: { getStateManager },
actions: { setStateManager }
}
);
register(store);
// ------------------------- @wordpress/core-data
const useMyEntityBlockEditor = (kind, name, id) =>
{
const { editEntityRecord } = useDispatch('core');
const edited = useSelect(
// @ts-ignore
s => s('core').getEditedEntityRecord(kind, name, id),
[kind, name, id]
);
const value = useMemo(
() => edited.kurageState,
[kind, name, id, edited.kurageState]
);
const onChange = useCallback((state: number) => {
const edits = { kurageState: state };
editEntityRecord(kind, name, id, edits);
}, [kind, name, id])
return [value, onChange];
}
// ------------------------- @wordpress/editor
const MyEditor = ({postType, postId, children}) =>
{
// @ts-ignore
const post = useSelect(s => s('core').getEntityRecord('postType', postType, postId), []);
return <MyEditorProvider post={post}>{children}</MyEditorProvider>
}
const MyEditorProvider = (props) =>
{
const { post, children, Component = MyBlockEditorProvider } = props;
const [ value, onChange ] = useMyEntityBlockEditor('postType', post.type, post.id);
return <Component value={value} onChange={onChange}>{children}</Component>
}
// ------------------------- @wordpress/block-editor
const MyBlockEditorProvider = (props) =>
{
useMyBlockSync(props);
return <>{ props.children }</>
}
const useMyBlockSync = ({ value, onChange }) =>
{
const registry = useRegistry();
// @ts-ignore
const { getStateManager } = registry.select(store);
// @ts-ignore
const { setStateManager} = registry.dispatch(store);
const inValue = useRef(null);
const outValue = useRef(null);
useEffect(() => {
if(value === undefined)
{
return;
}
if(outValue.current !== value)
{
outValue.current = null;
// @ts-ignore
registry.batch(() => {
inValue.current = value;
setStateManager(value);
});
}
}, [value]);
useEffect(() => {
// @ts-ignore
const unsub = registry.subscribe(() => {
if(inValue.current !== null)
{
inValue.current = null;
return;
}
const sm = getStateManager();
outValue.current = sm;
onChange(sm);
}, store);
return () => unsub();
}, [registry])
}
// ------------------------- Test Code
export const UndoTest = () =>
{
const state = useSelect(s => s(store).getStateManager(), []);
const { setStateManager } = useDispatch(store);
const btnClick = () =>
{
setStateManager(state + 1)
}
return (
<MyEditor postType="post" postId={1150}>
<h2>state: {state}</h2>
<Button variant="primary" onClick={btnClick}>カウント</Button>
</MyEditor>
);
}
独自ストアをUndo/Redo
に参加させてみました。
決してコピペしていいコードではありません。
何が起こるか・・・?