ステップ 1: 削除ボタンを追加する
DeletePageButton
コンポーネントを作成し、PagesList
コンポーネントのユーザーインターフェースを更新することから始めましょう:
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
const DeletePageButton = () => (
<Button variant="primary">
Delete
</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: 190}}>Actions</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={page.id}>
<td>{ decodeEntities( page.title.rendered ) }</td>
<td>
<div className="form-buttons">
<PageEditButton pageId={ page.id } />
{/* ↓ This is the only change in the PagesList component */}
<DeletePageButton pageId={ page.id }/>
</div>
</td>
</tr>
) ) }
</tbody>
</table>
);
}
これが現在の PagesList の見た目です:
ステップ 2: ボタンを削除アクションに接続する
Gutenberg データでは、deleteEntityRecord
アクションを使用して WordPress REST API からエンティティレコードを削除します。リクエストを送信し、結果を処理し、Redux ステート内のキャッシュデータを更新します。
ブラウザの開発者ツールでエンティティレコードを削除する方法は次のとおりです:
// We need a valid page ID to call deleteEntityRecord, so let's get the first available one using getEntityRecords.
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// Now let's delete that page:
const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );
// promise gets resolved or rejected when the API request succeeds or fails.
REST API リクエストが完了すると、リストからページの1つが消えたことに気付くでしょう。これは、そのリストが useSelect()
フックと select( coreDataStore ).getEntityRecords( 'postType', 'page' )
セレクタによって生成されているためです。基になるデータが変更されるたびに、リストは新しいデータで再描画されます。これは非常に便利です!
DeletePageButton
がクリックされたときにそのアクションをディスパッチしましょう:
const DeletePageButton = ({ pageId }) => {
const { deleteEntityRecord } = useDispatch( coreDataStore );
const handleDelete = () => deleteEntityRecord( 'postType', 'page', pageId );
return (
<Button variant="primary" onClick={ handleDelete }>
Delete
</Button>
);
}
ステップ 3: ビジュアルフィードバックを追加する
削除ボタンをクリックした後、REST API リクエストが完了するまでに数秒かかることがあります。それを、前の部分で行ったのと同様に <Spinner />
コンポーネントで伝えましょう。
そのために isDeletingEntityRecord
セレクタが必要です。これは、パート 3 で見た isSavingEntityRecord
セレクタに似ています: true
または false
を返し、HTTP リクエストを発行することはありません:
const DeletePageButton = ({ pageId }) => {
// ...
const { isDeleting } = useSelect(
select => ({
isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
}),
[ pageId ]
)
return (
<Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
{ isDeleting ? (
<>
<Spinner />
Deleting...
</>
) : 'Delete' }
</Button>
);
}
実際に動作している様子は次のとおりです:
ステップ 4: エラーを処理する
私たちは楽観的に 削除 操作が常に成功するだろうと仮定しました。残念ながら、内部では多くの方法で失敗する可能性のある REST API リクエストです:
- ウェブサイトがダウンしている可能性があります。
- 削除リクエストが無効である可能性があります。
- 誰かがその間にページを削除した可能性があります。
これらのエラーが発生したときにユーザーに通知するために、getLastEntityDeleteError
セレクタを使用してエラー情報を抽出する必要があります:
// Replace 9 with an actual page ID
wp.data.select( 'core' ).getLastEntityDeleteError( 'postType', 'page', 9 )
これを DeletePageButton
に適用する方法は次のとおりです:
import { useEffect } from 'react';
const DeletePageButton = ({ pageId }) => {
// ...
const { error, /* ... */ } = useSelect(
select => ( {
error: select( coreDataStore ).getLastEntityDeleteError( 'postType', 'page', pageId ),
// ...
} ),
[pageId]
);
useEffect( () => {
if ( error ) {
// Display the error
}
}, [error] )
// ...
}
error
オブジェクトは @wordpress/api-fetch
から来ており、エラーに関する情報を含んでいます。以下のプロパティがあります:
message
–Invalid post ID
のような人間が読めるエラーメッセージ。code
–rest_post_invalid_id
のような文字列ベースのエラーコード。すべての可能なエラーコードについては、/v2/pages
エンドポイントのソースコード を参照する必要があります。data
(オプション) – エラーの詳細、失敗したリクエストの HTTP レスポンスコードを含むcode
プロパティを含みます。
そのオブジェクトをエラーメッセージに変換する方法はいくつかありますが、このチュートリアルでは error.message
を表示します。
WordPress には Snackbar
コンポーネントを使用してステータス情報を表示する確立されたパターンがあります。これは ウィジェットエディタ での見た目です:
私たちのプラグインでも同じタイプの通知を使用しましょう!これには2つの部分があります:
- 1. 通知の表示
- 2. 通知のディスパッチ
通知の表示
私たちのアプリケーションはページを表示する方法しか知らず、通知を表示する方法は知りません。それを教えましょう!
WordPress は、通知をレンダリングするために必要なすべての React コンポーネントを便利に提供してくれます。単一の通知を表す コンポーネント Snackbar
:
ただし、Snackbar
を直接使用することはありません。SnackbarList
コンポーネントを使用します。これにより、スムーズなアニメーションを使用して複数の通知を表示し、数秒後に自動的に非表示にします。実際、WordPress はウィジェットエディタや他の wp-admin ページで使用されている同じコンポーネントを使用しています!
私たち自身の Notifications
コンポーネントを作成しましょう:
import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
function Notifications() {
const notices = []; // We'll come back here in a second!
return (
<SnackbarList
notices={ notices }
className="components-editor-notices__snackbar"
/>
);
}
基本的な構造は整っていますが、レンダリングされる通知のリストは空です。これをどのように埋めますか?私たちは WordPress と同じパッケージに依存します: @wordpress/notices
。
方法は次のとおりです:
import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
function Notifications() {
const notices = useSelect(
( select ) => select( noticesStore ).getNotices(),
[]
);
const { removeNotice } = useDispatch( noticesStore );
const snackbarNotices = notices.filter( ({ type }) => type === 'snackbar' );
return (
<SnackbarList
notices={ snackbarNotices }
className="components-editor-notices__snackbar"
onRemove={ removeNotice }
/>
);
}
function MyFirstApp() {
// ...
return (
<div>
{/* ... */}
<Notifications />
</div>
);
}
このチュートリアルはページの管理に焦点を当てており、上記のスニペットについて詳しくは説明しません。@wordpress/notices
の詳細に興味がある場合は、ハンドブックページ が良い出発点です。
これで、発生した可能性のあるエラーについてユーザーに通知する準備が整いました。
通知のディスパッチ
SnackbarNotices コンポーネントが整ったので、通知をディスパッチする準備ができました!方法は次のとおりです:
import { useEffect } from 'react';
import { store as noticesStore } from '@wordpress/notices';
function DeletePageButton( { pageId } ) {
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
// useSelect returns a list of selectors if you pass the store handle
// instead of a callback:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// Tell the user the operation succeeded:
createSuccessNotice( "The page was deleted!", {
type: 'snackbar',
} );
} else {
// We use the selector directly to get the fresh error *after* the deleteEntityRecord
// have failed.
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
// Tell the user how exactly the operation has failed:
createErrorNotice( message, {
type: 'snackbar',
} );
}
}
// ...
}
素晴らしい!DeletePageButton
はエラーを完全に認識しています。エラーメッセージが実際にどのように表示されるか見てみましょう。無効な削除をトリガーし、失敗させます。これを行う1つの方法は、pageId
を大きな数で掛けることです:
function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
pageId = pageId * 1000;
// ...
}
ページをリフレッシュし、任意の Delete
ボタンをクリックすると、次のエラーメッセージが表示されるはずです:
素晴らしい!これで pageId = pageId * 1000;
行を削除できます。
実際にページを削除してみましょう。ブラウザをリフレッシュし、削除ボタンをクリックした後に表示されるはずの内容は次のとおりです:
すべてを接続する
すべての要素が整いました、素晴らしい!この章で行ったすべての変更は次のとおりです:
import { useState, useEffect } from 'react';
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';
function MyFirstApp() {
const [searchTerm, setSearchTerm] = useState( '' );
const { pages, hasResolved } = useSelect(
( select ) => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
const selectorArgs = ['postType', 'page', query];
const pages = select( coreDataStore ).getEntityRecords( ...selectorArgs );
return {
pages,
hasResolved: select( coreDataStore ).hasFinishedResolution(
'getEntityRecords',
selectorArgs,
),
};
},
[searchTerm],
);
return (
<div>
<div className="list-controls">
<SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
<PageCreateButton/>
</div>
<PagesList hasResolved={ hasResolved } pages={ pages }/>
<Notifications />
</div>
);
}
function SnackbarNotices() {
const notices = useSelect(
( select ) => select( noticesStore ).getNotices(),
[]
);
const { removeNotice } = useDispatch( noticesStore );
const snackbarNotices = notices.filter( ( { type } ) => type === 'snackbar' );
return (
<SnackbarList
notices={ snackbarNotices }
className="components-editor-notices__snackbar"
onRemove={ removeNotice }
/>
);
}
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: 190 } }>Actions</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={ page.id }>
<td>{ page.title.rendered }</td>
<td>
<div className="form-buttons">
<PageEditButton pageId={ page.id }/>
<DeletePageButton pageId={ page.id }/>
</div>
</td>
</tr>
) ) }
</tbody>
</table>
);
}
function DeletePageButton( { pageId } ) {
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
// useSelect returns a list of selectors if you pass the store handle
// instead of a callback:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// Tell the user the operation succeeded:
createSuccessNotice( "The page was deleted!", {
type: 'snackbar',
} );
} else {
// We use the selector directly to get the error at this point in time.
// Imagine we fetched the error like this:
// const { lastError } = useSelect( function() { /* ... */ } );
// Then, lastError would be null inside of handleDelete.
// Why? Because we'd refer to the version of it that was computed
// before the handleDelete was even called.
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
// Tell the user how exactly the operation have failed:
createErrorNotice( message, {
type: 'snackbar',
} );
}
}
const { deleteEntityRecord } = useDispatch( coreDataStore );
const { isDeleting } = useSelect(
select => ( {
isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
} ),
[ pageId ]
);
return (
<Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
{ isDeleting ? (
<>
<Spinner />
Deleting...
</>
) : 'Delete' }
</Button>
);
}
次は何ですか?
- 前の部分: 作成ページフォームの構築
- (オプション) 完成したアプリ を block-development-examples リポジトリで確認する