削除ボタンの追加
前のパートでは、新しいページの作成機能を追加しました。このパートではアプリケーションに「削除」機能を追加します。
これから構築する機能は以下のようになります。
ステップ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 } />
{/* ↓ 以下が PagesList コンポーネント内で唯一の変更 */}
<DeletePageButton pageId={ page.id }/>
</div>
</td>
</tr>
) ) }
</tbody>
</table>
);
}
これで PagesList は以下のようになります。
ステップ2: ボタンと削除アクションの接続
Gutenberg のデータでは、WordPress REST APIから deleteEntityRecord
アクションを使用してエンティティレコードを削除します。アクションはリクエストを送信し、結果を処理し、Redux ステート内にキャッシュされたデータを更新します。
以下はブラウザの開発ツール内でエンティティレコードの削除を試す方法です。
// deleteEntityRecord を呼び出すには有効なページ ID が必要。getEntityRecords を使用して最初の利用可能なページ ID を取得する。
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// そのページを削除する。
const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );
// API リクエストの成功または失敗により、promise は resolved または rejected を取得する。
REST API リクエストが終了すると、ページの一つがリストから消えていることが分かります。これは、そのリストが 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: 視覚的なフィードバックの追加
「Delete」ボタンをクリックした後、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
セレクタを使用して、エラー情報を取り出す必要があります。
// 「9」を実際のページ 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 ) {
// エラーの表示
}
}, [error] )
// ...
}
error
オブジェクトは @wordpress/api-fetch
から来たもので、エラーに関する情報を含みます。以下のプロパティを持ちます。
message
–Invalid post ID
のような人間が読めるエラーメッセージ。code
–rest_post_invalid_id
のような文字列ベースのエラーコード。すべてのエラーコードを調べるには、/v2/pages
エンドポイントのソースコード を参照する必要があります。data
(オプション) – エラーの詳細。失敗したリクエストの HTTP レスポンスコードを含むcode
プロパティを含む。
このオブジェクトをエラーメッセージに変換する多くの方法がありますが、このチュートリアルでは error.message
を表示します。
WordPress ではステータス情報を表示するパターンが確立されており、Snackbar
コンポーネントを使用します。たとえば ウィジェットエディターでは、以下のようになります。
同じタイプの通知をプラグインで使用しましょう。これには2つのパートがあります。
- 通知の表示
- 通知のディスパッチ
通知の表示
アプリケーションはページを表示する方法のみを知っていて、通知を表示する方法は知りません。それを教えてあげましょう !
便利なことに WordPress では、通知のレンダーに必要なすべての React コンポーネントが提供されています。Snackbar
コンポーネントは、単一の通知を表現します。
しかし、Snackbar
は直接使わず、SnackbarList
コンポーネントを使用します。SnackbarList
コンポーネントはスムーズなアニメーションで複数の通知を表示し、数秒後に自動的に消えます。実際、WordPress ではウィジェットエディターやその他の管理画面のページで同じコンポーネントを使用しています。
それでは独自の Notifications
コンポーネントを作成します。
import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
function Notifications() {
const notices = []; // すぐにここに戻ってきます !
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 はセレクタのリストを返す:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// 操作が成功したことをユーザーに伝える:
createSuccessNotice( "The page was deleted!", {
type: 'snackbar',
} );
} else {
// deleteEntityRecord が失敗した*後で*、直接セレクタを使用して新しいエラーを取得する。
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
// 具体的にどのように操作に失敗したかをユーザーに伝える:
createErrorNotice( message, {
type: 'snackbar',
} );
}
}
// ...
}
素晴らしい ! これで DeletePageButton
は、完全にエラーを認識するようになりました。エラーメッセージを実際に見てみましょう。無効な削除をトリガーして失敗させます。これを行う1つの方法として、pageId
に大きな数を掛けます。
function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
pageId = pageId * 1000;
// ...
}
ページを更新し、「Delete」ボタンをクリックすると、次のようなエラーメッセージが表示されるはずです。
最高です ! では、pageId = pageId * 1000;
の行を削除しましょう。
そして、実際にページを削除してみます。ブラウザをリフレッシュして「Delete」ボタンをクリックすると、以下のように表示されます。
以上です。
すべてをひとつに
これですべてのピースがそろいました。この章で行ったすべての変更が以下になります。
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 はセレクタのリストを返す:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// 操作が成功したことをユーザーに伝える:
createSuccessNotice( "The page was deleted!", {
type: 'snackbar',
} );
} else {
// この時点で、直接セレクタを使用してエラーを取得する。
// 仮に、以下のようにエラーをフェッチしたとする。
// const { lastError } = useSelect( function() { /* ... */ } );
// このとき lastError は handleDelete 内部で null になる。
// 何故か ? それは handleDelete の呼び出しまえに計算されたバージョンを参照するため。
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
// 具体的にどのように操作に失敗したかをユーザーに伝える:
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 リポジトリ内の 完成したアプリ を参照
最終更新日: