ステップ 1: 編集ボタンを追加する
Edit フォームには Edit ボタンが必要ですので、まずは PagesList
コンポーネントに追加しましょう:
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
const PageEditButton = () => (
<Button variant="primary">
Edit
</Button>
)
function PagesList( { hasResolved, pages } ) {
if ( ! hasResolved ) {
return <Spinner />;
}
if ( ! pages?.length ) {
return <div>No results</div>;
}
return (
<table className="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<td>Title</td>
<td style={{width: 120}}>Actions</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={page.id}>
<td>{ decodeEntities( page.title.rendered ) }</td>
<td>
<PageEditButton pageId={ page.id } />
</td>
</tr>
) ) }
</tbody>
</table>
);
}
PagesList
の唯一の変更は、Actions とラベル付けされた追加の列です:
ステップ 2: 編集フォームを表示する
ボタンは見栄えが良いですが、まだ何も機能していません。編集フォームを表示するには、まずそれを作成する必要があります:
import { Button, TextControl } from '@wordpress/components';
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
return (
<div className="my-gutenberg-form">
<TextControl
value=''
label='Page title:'
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
Save
</Button>
<Button onClick={ onCancel } variant="tertiary">
Cancel
</Button>
</div>
</div>
);
}
次に、ボタンが作成したフォームを表示するようにします。このチュートリアルはウェブデザインに焦点を当てていないため、最小限のコードで二つを接続するコンポーネントを使用します: Modal
。PageEditButton
をそれに応じて更新しましょう:
import { Button, Modal, TextControl } from '@wordpress/components';
function PageEditButton({ pageId }) {
const [ isOpen, setOpen ] = useState( false );
const openModal = () => setOpen( true );
const closeModal = () => setOpen( false );
return (
<>
<Button
onClick={ openModal }
variant="primary"
>
Edit
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="Edit page">
<EditPageForm
pageId={pageId}
onCancel={closeModal}
onSaveFinished={closeModal}
/>
</Modal>
) }
</>
)
}
今、Edit ボタンをクリックすると、次のモーダルが表示されるはずです:
素晴らしい!これで基本的なユーザーインターフェースが整いました。
ステップ 3: ページの詳細でフォームを埋める
EditPageForm
が現在編集中のページのタイトルを表示するようにしたいです。page
プロップを受け取らず、pageId
のみを受け取ることに気づいたかもしれません。それは問題ありません。Gutenberg Data は、任意のコンポーネントからエンティティレコードに簡単にアクセスできるようにします。
この場合、getEntityRecord
セレクタを使用する必要があります。レコードのリストは getEntityRecords
コールのおかげで既に利用可能で、追加の HTTP リクエストは発生しません – キャッシュされたレコードをすぐに取得できます。
ブラウザの開発者ツールで試す方法は次のとおりです:
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ); // Replace 9 with an actual page ID
EditPageForm
をそれに応じて更新しましょう:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const page = useSelect(
select => select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId ),
[pageId]
);
return (
<div className="my-gutenberg-form">
<TextControl
label='Page title:'
value={ page.title.rendered }
/>
{ /* ... */ }
</div>
);
}
今は次のように見えるはずです:
ステップ 4: ページタイトルフィールドを編集可能にする
Page title フィールドには一つの問題があります: 編集できません。固定の value
を受け取りますが、入力中に更新されません。onChange
ハンドラーが必要です。
他の React アプリでこのようなパターンを見たことがあるかもしれません。これは “controlled component” として知られています:
function VanillaReactForm({ initialTitle }) {
const [title, setTitle] = useState( initialTitle );
return (
<TextControl
value={ title }
onChange={ setTitle }
/>
);
}
Gutenberg Data でエンティティレコードを更新するのは似ていますが、ローカル(コンポーネントレベル)状態に setTitle
を使用する代わりに、Redux 状態に更新を保存する editEntityRecord
アクションを使用します。ブラウザの開発者ツールで試す方法は次のとおりです:
// We need a valid page ID to call editEntityRecord, so let's get the first available one using getEntityRecords.
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// Update the title
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', pageId, { title: 'updated title' } );
この時点で、editEntityRecord
が useState
よりも優れているのは どう かと尋ねるかもしれません。その答えは、他では得られないいくつかの機能を提供するからです。
まず、データを取得するのと同じくらい簡単に変更を保存でき、すべてのキャッシュが正しく更新されることを保証します。
次に、editEntityRecord
を介して適用された変更は、undo
および redo
アクションを介して簡単に元に戻すことができます。
最後に、変更が Redux 状態に存在するため、それらは「グローバル」であり、他のコンポーネントからアクセスできます。たとえば、PagesList
が現在編集中のタイトルを表示するようにできます。
その最後のポイントに関して、getEntityRecord
を使用して、先ほど更新したエンティティレコードにアクセスするとどうなるか見てみましょう:
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
編集が反映されていません。何が起こっているのでしょうか?
<PagesList />
は getEntityRecord()
によって返されたデータをレンダリングします。getEntityRecord()
が更新されたタイトルを反映している場合、ユーザーが TextControl
に入力した内容は <PagesList />
内にも即座に表示されるはずです。これは望ましくありません。編集は、ユーザーが保存することを決定するまでフォームの外に漏れ出すべきではありません。
Gutenberg Data は、Entity Records と Edited Entity Records の間に区別を設けることでこの問題を解決します。Entity Records は API からのデータを反映し、ローカル編集を無視しますが、Edited Entity Records はすべてのローカル編集が適用されたものです。両者は同時に Redux 状態に共存します。
getEditedEntityRecord
を呼び出すとどうなるか見てみましょう:
wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title
// "updated title"
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
// { "rendered": "<original, unchanged title>", "raw": "..." }
ご覧のとおり、エンティティレコードの title
はオブジェクトですが、編集されたエンティティレコードの title
は文字列です。
これは偶然ではありません。title
、excerpt
、content
のようなフィールドは shortcodes や dynamic blocks を含む可能性があり、サーバー上でのみレンダリングできます。そのため、REST API は raw
マークアップ と rendered
文字列の両方を公開します。たとえば、ブロックエディタでは、content.rendered
がビジュアルプレビューとして使用され、content.raw
がコードエディタを埋めるために使用される可能性があります。
では、なぜ編集されたエンティティレコードの content
が文字列なのでしょうか?JavaScript は任意のブロックマークアップを適切にレンダリングできないため、raw
マークアップのみを rendered
部分なしで保存します。そして、それが文字列であるため、フィールド全体が文字列になります。
EditPageForm
をそれに応じて更新できます。useDispatch
フックを使用してアクションにアクセスし、useSelect
を使用してセレクタにアクセスするのと同様にします:
import { useDispatch } from '@wordpress/data';
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const page = useSelect(
select => select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
[ pageId ]
);
const { editEntityRecord } = useDispatch( coreDataStore );
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title } );
return (
<div className="my-gutenberg-form">
<TextControl
label="Page title:"
value={ page.title }
onChange={ handleChange }
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
Save
</Button>
<Button onClick={ onCancel } variant="tertiary">
Cancel
</Button>
</div>
</div>
);
}
onChange
ハンドラーを追加して editEntityRecord
アクションを介して編集を追跡し、その後 getEditedEntityRecord
にセレクタを変更して page.title
が常に変更を反映するようにしました。
今は次のように見えます:
ステップ 5: フォームデータを保存する
ページタイトルを編集できるようになったので、保存できることも確認しましょう。Gutenberg データでは、saveEditedEntityRecord
アクションを使用して WordPress REST API に変更を保存します。リクエストを送信し、結果を処理し、Redux 状態内のキャッシュデータを更新します。
ブラウザの開発者ツールで試すことができる例は次のとおりです:
// Replace 9 with an actual page ID
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', 9, { title: 'updated title' } );
wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'page', 9 );
上記のスニペットは新しいタイトルを保存しました。以前とは異なり、getEntityRecord
は現在更新されたタイトルを反映しています:
// Replace 9 with an actual page ID
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ).title.rendered
// "updated title"
エンティティレコードは、REST API リクエストが完了した後に保存された変更を反映するように更新されます。
EditPageForm
は動作する Save ボタンを持つように見えます:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const { saveEditedEntityRecord } = useDispatch( coreDataStore );
const handleSave = () => saveEditedEntityRecord( 'postType', 'page', pageId );
return (
<div className="my-gutenberg-form">
{/* ... */}
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary">
Save
</Button>
{/* ... */}
</div>
</div>
);
}
動作しますが、まだ修正すべき点が一つあります: フォームモーダルは自動的に閉じません。onSaveFinished
を呼び出さなかったからです。幸運なことに、saveEditedEntityRecord
は保存操作が完了すると解決される Promise を返します。それを EditPageForm
で利用しましょう:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleSave = async () => {
await saveEditedEntityRecord( 'postType', 'page', pageId );
onSaveFinished();
};
// ...
}
ステップ 6: エラーを処理する
私たちは楽観的に save 操作が常に成功するだろうと仮定しました。残念ながら、さまざまな理由で失敗する可能性があります:
- ウェブサイトがダウンしている
- 更新が無効である
- ページがその間に他の誰かによって削除された可能性がある
これらのいずれかが発生した場合にユーザーに通知するために、二つの調整を行う必要があります。更新が失敗した場合、フォームモーダルを閉じたくありません。saveEditedEntityRecord
が返す Promise は、更新が実際に成功した場合にのみ更新されたレコードで解決されます。何かがうまくいかないと、空の値で解決されます。それを使用してモーダルを開いたままにしましょう:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const handleSave = async () => {
const updatedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( updatedRecord ) {
onSaveFinished();
}
};
// ...
}
素晴らしい!次に、エラーメッセージを表示しましょう。失敗の詳細は getLastEntitySaveError
セレクタを使用して取得できます:
// Replace 9 with an actual page ID
wp.data.select( 'core' ).getLastEntitySaveError( 'postType', 'page', 9 )
それを EditPageForm
で使用する方法は次のとおりです:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const { lastError, page } = useSelect(
select => ({
page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId )
}),
[ pageId ]
)
// ...
return (
<div className="my-gutenberg-form">
{/* ... */}
{ lastError ? (
<div className="form-error">
Error: { lastError.message }
</div>
) : false }
{/* ... */}
</div>
);
}
素晴らしい!EditPageForm
は現在エラーを完全に認識しています。
そのエラーメッセージを実際に見てみましょう。無効な更新をトリガーして失敗させます。投稿タイトルは壊れにくいので、date
プロパティを -1
に設定しましょう – それは確実なバリデーションエラーです:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title, date: -1 } );
// ...
}
ページをリフレッシュし、フォームを開き、タイトルを変更して保存を押すと、次のエラーメッセージが表示されるはずです:
素晴らしい!これで handleChange
の以前のバージョンを復元 し、次のステップに進むことができます。
ステップ 7: ステータスインジケーター
私たちのフォームにはもう一つの問題があります: 視覚的なフィードバックがありません。Save ボタンが機能したかどうかは、フォームが消えるかエラーメッセージが表示されるまで確信できません。
これを明確にし、ユーザーに二つの状態を伝えます: Saving と No changes detected。関連するセレクタは isSavingEntityRecord
と hasEditsForEntityRecord
です。getEntityRecord
とは異なり、これらは HTTP リクエストを発行せず、現在のエンティティレコードの状態のみを返します。
EditPageForm
でそれらを使用しましょう:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const { isSaving, hasEdits, /* ... */ } = useSelect(
select => ({
isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
// ...
}),
[ pageId ]
)
}
isSaving
と hasEdits
を使用して、保存中にスピナーを表示し、編集がない場合は保存ボタンをグレーアウトできます:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
return (
// ...
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary" disabled={ ! hasEdits || isSaving }>
{ isSaving ? (
<>
<Spinner/>
Saving
</>
) : 'Save' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
Cancel
</Button>
</div>
// ...
);
}
編集がない場合やページが現在保存中の場合、save ボタンを無効にします。これは、ユーザーが誤ってボタンを二回押すのを防ぐためです。
また、進行中の save を中断することは @wordpress/data
ではサポートされていないため、cancel ボタンも条件付きで無効にしました。
実際に動作している様子は次のとおりです:
すべてをつなげる
すべての要素が揃いました、素晴らしい!この章で構築したすべてを一つの場所にまとめました:
import { useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';
function PageEditButton( { pageId } ) {
const [ isOpen, setOpen ] = useState( false );
const openModal = () => setOpen( true );
const closeModal = () => setOpen( false );
return (
<>
<Button onClick={ openModal } variant="primary">
Edit
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="Edit page">
<EditPageForm
pageId={ pageId }
onCancel={ closeModal }
onSaveFinished={ closeModal }
/>
</Modal>
) }
</>
);
}
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const { page, lastError, isSaving, hasEdits } = useSelect(
( select ) => ( {
page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ),
isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
} ),
[ pageId ]
);
const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore );
const handleSave = async () => {
const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( savedRecord ) {
onSaveFinished();
}
};
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } );
return (
<div className="my-gutenberg-form">
<TextControl
label="Page title:"
value={ page.title }
onChange={ handleChange }
/>
{ lastError ? (
<div className="form-error">Error: { lastError.message }</div>
) : (
false
) }
<div className="form-buttons">
<Button
onClick={ handleSave }
variant="primary"
disabled={ ! hasEdits || isSaving }
>
{ isSaving ? (
<>
<Spinner/>
Saving
</>
) : 'Save' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
Cancel
</Button>
</div>
</div>
);
}
次は何ですか?
- 前の部分: ページのリストを構築する
- 次の部分: 新しいページフォームの構築(近日公開)
- (オプション)完成したアプリ をブロック開発の例リポジトリでレビューする