削除ボタンの追加

Topics

  • ステップ1: 「削除」ボタンの追加
  • ステップ2: ボタンと削除アクションの接続
  • ステップ3: 視覚的なフィードバックの追加
  • ステップ4: エラー処理
    • 通知の表示
    • 通知のディスパッチ
  • すべてをひとつに
  • 次は ?
  • 前のパートでは、新しいページの作成機能を追加しました。このパートではアプリケーションに「削除」機能を追加します。

    これから構築する機能は以下のようになります。

    ステップ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 は以下のようになります。

    Top ↑

    ステップ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>
    	);
    }
    
    

    Top ↑

    ステップ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>
    	);
    }
    
    

    実際の動作の様子です。

    Top ↑

    ステップ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つのパートがあります。

    1. 通知の表示
    2. 通知のディスパッチ

    Top ↑

    通知の表示

    アプリケーションはページを表示する方法のみを知っていて、通知を表示する方法は知りません。それを教えてあげましょう !

    便利なことに 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 の詳細に興味があれば、ハンドブックページ から始めると良いでしょう。

    これで、発生した可能性のあるエラーをユーザーに伝える準備ができました。

    Top ↑

    通知のディスパッチ

    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」ボタンをクリックすると、以下のように表示されます。

    以上です。

    Top ↑

    すべてをひとつに

    これですべてのピースがそろいました。この章で行ったすべての変更が以下になります。

    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>
    	);
    }
    
    

    Top ↑

    次は ?

    原文

    最終更新日: