ステップ 1: 削除ボタンを追加する

DeletePageButton コンポーネントを作成し、PagesList コンポーネントのユーザーインターフェースを更新することから始めましょう:

  1. import { Button } from '@wordpress/components';
  2. import { decodeEntities } from '@wordpress/html-entities';
  3. const DeletePageButton = () => (
  4. <Button variant="primary">
  5. Delete
  6. </Button>
  7. )
  8. function PagesList( { hasResolved, pages } ) {
  9. if ( ! hasResolved ) {
  10. return <Spinner />;
  11. }
  12. if ( ! pages?.length ) {
  13. return <div>No results</div>;
  14. }
  15. return (
  16. <table className="wp-list-table widefat fixed striped table-view-list">
  17. <thead>
  18. <tr>
  19. <td>Title</td>
  20. <td style={{width: 190}}>Actions</td>
  21. </tr>
  22. </thead>
  23. <tbody>
  24. { pages?.map( ( page ) => (
  25. <tr key={page.id}>
  26. <td>{ decodeEntities( page.title.rendered ) }</td>
  27. <td>
  28. <div className="form-buttons">
  29. <PageEditButton pageId={ page.id } />
  30. {/* This is the only change in the PagesList component */}
  31. <DeletePageButton pageId={ page.id }/>
  32. </div>
  33. </td>
  34. </tr>
  35. ) ) }
  36. </tbody>
  37. </table>
  38. );
  39. }

これが現在の PagesList の見た目です:

削除ボタンの追加(Adding a delete button) - img1

ステップ 2: ボタンを削除アクションに接続する

Gutenberg データでは、deleteEntityRecord アクションを使用して WordPress REST API からエンティティレコードを削除します。リクエストを送信し、結果を処理し、Redux ステート内のキャッシュデータを更新します。

ブラウザの開発者ツールでエンティティレコードを削除する方法は次のとおりです:

  1. // We need a valid page ID to call deleteEntityRecord, so let's get the first available one using getEntityRecords.
  2. const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
  3. // Now let's delete that page:
  4. const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );
  5. // promise gets resolved or rejected when the API request succeeds or fails.

REST API リクエストが完了すると、リストからページの1つが消えたことに気付くでしょう。これは、そのリストが useSelect() フックと select( coreDataStore ).getEntityRecords( 'postType', 'page' ) セレクタによって生成されているためです。基になるデータが変更されるたびに、リストは新しいデータで再描画されます。これは非常に便利です!

DeletePageButton がクリックされたときにそのアクションをディスパッチしましょう:

  1. const DeletePageButton = ({ pageId }) => {
  2. const { deleteEntityRecord } = useDispatch( coreDataStore );
  3. const handleDelete = () => deleteEntityRecord( 'postType', 'page', pageId );
  4. return (
  5. <Button variant="primary" onClick={ handleDelete }>
  6. Delete
  7. </Button>
  8. );
  9. }

ステップ 3: ビジュアルフィードバックを追加する

削除ボタンをクリックした後、REST API リクエストが完了するまでに数秒かかることがあります。それを、前の部分で行ったのと同様に <Spinner /> コンポーネントで伝えましょう。

そのために isDeletingEntityRecord セレクタが必要です。これは、パート 3 で見た isSavingEntityRecord セレクタに似ています: true または false を返し、HTTP リクエストを発行することはありません:

  1. const DeletePageButton = ({ pageId }) => {
  2. // ...
  3. const { isDeleting } = useSelect(
  4. select => ({
  5. isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
  6. }),
  7. [ pageId ]
  8. )
  9. return (
  10. <Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
  11. { isDeleting ? (
  12. <>
  13. <Spinner />
  14. Deleting...
  15. </>
  16. ) : 'Delete' }
  17. </Button>
  18. );
  19. }

実際に動作している様子は次のとおりです:

削除ボタンの追加(Adding a delete button) - img2

ステップ 4: エラーを処理する

私たちは楽観的に 削除 操作が常に成功するだろうと仮定しました。残念ながら、内部では多くの方法で失敗する可能性のある REST API リクエストです:

  • ウェブサイトがダウンしている可能性があります。
  • 削除リクエストが無効である可能性があります。
  • 誰かがその間にページを削除した可能性があります。

これらのエラーが発生したときにユーザーに通知するために、getLastEntityDeleteError セレクタを使用してエラー情報を抽出する必要があります:

  1. // Replace 9 with an actual page ID
  2. wp.data.select( 'core' ).getLastEntityDeleteError( 'postType', 'page', 9 )

これを DeletePageButton に適用する方法は次のとおりです:

  1. import { useEffect } from 'react';
  2. const DeletePageButton = ({ pageId }) => {
  3. // ...
  4. const { error, /* ... */ } = useSelect(
  5. select => ( {
  6. error: select( coreDataStore ).getLastEntityDeleteError( 'postType', 'page', pageId ),
  7. // ...
  8. } ),
  9. [pageId]
  10. );
  11. useEffect( () => {
  12. if ( error ) {
  13. // Display the error
  14. }
  15. }, [error] )
  16. // ...
  17. }

error オブジェクトは @wordpress/api-fetch から来ており、エラーに関する情報を含んでいます。以下のプロパティがあります:

  • messageInvalid post ID のような人間が読めるエラーメッセージ。
  • coderest_post_invalid_id のような文字列ベースのエラーコード。すべての可能なエラーコードについては、/v2/pages エンドポイントのソースコード を参照する必要があります。
  • data (オプション) – エラーの詳細、失敗したリクエストの HTTP レスポンスコードを含む code プロパティを含みます。

そのオブジェクトをエラーメッセージに変換する方法はいくつかありますが、このチュートリアルでは error.message を表示します。

WordPress には Snackbar コンポーネントを使用してステータス情報を表示する確立されたパターンがあります。これは ウィジェットエディタ での見た目です:

削除ボタンの追加(Adding a delete button) - img3

私たちのプラグインでも同じタイプの通知を使用しましょう!これには2つの部分があります:

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

通知の表示

私たちのアプリケーションはページを表示する方法しか知らず、通知を表示する方法は知りません。それを教えましょう!

WordPress は、通知をレンダリングするために必要なすべての React コンポーネントを便利に提供してくれます。単一の通知を表す コンポーネント Snackbar:

削除ボタンの追加(Adding a delete button) - img4

ただし、Snackbar を直接使用することはありません。SnackbarList コンポーネントを使用します。これにより、スムーズなアニメーションを使用して複数の通知を表示し、数秒後に自動的に非表示にします。実際、WordPress はウィジェットエディタや他の wp-admin ページで使用されている同じコンポーネントを使用しています!

私たち自身の Notifications コンポーネントを作成しましょう:

  1. import { SnackbarList } from '@wordpress/components';
  2. import { store as noticesStore } from '@wordpress/notices';
  3. function Notifications() {
  4. const notices = []; // We'll come back here in a second!
  5. return (
  6. <SnackbarList
  7. notices={ notices }
  8. className="components-editor-notices__snackbar"
  9. />
  10. );
  11. }

基本的な構造は整っていますが、レンダリングされる通知のリストは空です。これをどのように埋めますか?私たちは WordPress と同じパッケージに依存します: @wordpress/notices

方法は次のとおりです:

  1. import { SnackbarList } from '@wordpress/components';
  2. import { store as noticesStore } from '@wordpress/notices';
  3. function Notifications() {
  4. const notices = useSelect(
  5. ( select ) => select( noticesStore ).getNotices(),
  6. []
  7. );
  8. const { removeNotice } = useDispatch( noticesStore );
  9. const snackbarNotices = notices.filter( ({ type }) => type === 'snackbar' );
  10. return (
  11. <SnackbarList
  12. notices={ snackbarNotices }
  13. className="components-editor-notices__snackbar"
  14. onRemove={ removeNotice }
  15. />
  16. );
  17. }
  18. function MyFirstApp() {
  19. // ...
  20. return (
  21. <div>
  22. {/* ... */}
  23. <Notifications />
  24. </div>
  25. );
  26. }

このチュートリアルはページの管理に焦点を当てており、上記のスニペットについて詳しくは説明しません。@wordpress/notices の詳細に興味がある場合は、ハンドブックページ が良い出発点です。

これで、発生した可能性のあるエラーについてユーザーに通知する準備が整いました。

通知のディスパッチ

SnackbarNotices コンポーネントが整ったので、通知をディスパッチする準備ができました!方法は次のとおりです:

  1. import { useEffect } from 'react';
  2. import { store as noticesStore } from '@wordpress/notices';
  3. function DeletePageButton( { pageId } ) {
  4. const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
  5. // useSelect returns a list of selectors if you pass the store handle
  6. // instead of a callback:
  7. const { getLastEntityDeleteError } = useSelect( coreDataStore )
  8. const handleDelete = async () => {
  9. const success = await deleteEntityRecord( 'postType', 'page', pageId);
  10. if ( success ) {
  11. // Tell the user the operation succeeded:
  12. createSuccessNotice( "The page was deleted!", {
  13. type: 'snackbar',
  14. } );
  15. } else {
  16. // We use the selector directly to get the fresh error *after* the deleteEntityRecord
  17. // have failed.
  18. const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
  19. const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
  20. // Tell the user how exactly the operation has failed:
  21. createErrorNotice( message, {
  22. type: 'snackbar',
  23. } );
  24. }
  25. }
  26. // ...
  27. }

素晴らしい!DeletePageButton はエラーを完全に認識しています。エラーメッセージが実際にどのように表示されるか見てみましょう。無効な削除をトリガーし、失敗させます。これを行う1つの方法は、pageId を大きな数で掛けることです:

  1. function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
  2. pageId = pageId * 1000;
  3. // ...
  4. }

ページをリフレッシュし、任意の Delete ボタンをクリックすると、次のエラーメッセージが表示されるはずです:

削除ボタンの追加(Adding a delete button) - img5

素晴らしい!これで pageId = pageId * 1000; 行を削除できます。

実際にページを削除してみましょう。ブラウザをリフレッシュし、削除ボタンをクリックした後に表示されるはずの内容は次のとおりです:

削除ボタンの追加(Adding a delete button) - img6

以上です!

すべてを接続する

すべての要素が整いました、素晴らしい!この章で行ったすべての変更は次のとおりです:

  1. import { useState, useEffect } from 'react';
  2. import { useSelect, useDispatch } from '@wordpress/data';
  3. import { Button, Modal, TextControl } from '@wordpress/components';
  4. function MyFirstApp() {
  5. const [searchTerm, setSearchTerm] = useState( '' );
  6. const { pages, hasResolved } = useSelect(
  7. ( select ) => {
  8. const query = {};
  9. if ( searchTerm ) {
  10. query.search = searchTerm;
  11. }
  12. const selectorArgs = ['postType', 'page', query];
  13. const pages = select( coreDataStore ).getEntityRecords( ...selectorArgs );
  14. return {
  15. pages,
  16. hasResolved: select( coreDataStore ).hasFinishedResolution(
  17. 'getEntityRecords',
  18. selectorArgs,
  19. ),
  20. };
  21. },
  22. [searchTerm],
  23. );
  24. return (
  25. <div>
  26. <div className="list-controls">
  27. <SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
  28. <PageCreateButton/>
  29. </div>
  30. <PagesList hasResolved={ hasResolved } pages={ pages }/>
  31. <Notifications />
  32. </div>
  33. );
  34. }
  35. function SnackbarNotices() {
  36. const notices = useSelect(
  37. ( select ) => select( noticesStore ).getNotices(),
  38. []
  39. );
  40. const { removeNotice } = useDispatch( noticesStore );
  41. const snackbarNotices = notices.filter( ( { type } ) => type === 'snackbar' );
  42. return (
  43. <SnackbarList
  44. notices={ snackbarNotices }
  45. className="components-editor-notices__snackbar"
  46. onRemove={ removeNotice }
  47. />
  48. );
  49. }
  50. function PagesList( { hasResolved, pages } ) {
  51. if ( !hasResolved ) {
  52. return <Spinner/>;
  53. }
  54. if ( !pages?.length ) {
  55. return <div>No results</div>;
  56. }
  57. return (
  58. <table className="wp-list-table widefat fixed striped table-view-list">
  59. <thead>
  60. <tr>
  61. <td>Title</td>
  62. <td style={ { width: 190 } }>Actions</td>
  63. </tr>
  64. </thead>
  65. <tbody>
  66. { pages?.map( ( page ) => (
  67. <tr key={ page.id }>
  68. <td>{ page.title.rendered }</td>
  69. <td>
  70. <div className="form-buttons">
  71. <PageEditButton pageId={ page.id }/>
  72. <DeletePageButton pageId={ page.id }/>
  73. </div>
  74. </td>
  75. </tr>
  76. ) ) }
  77. </tbody>
  78. </table>
  79. );
  80. }
  81. function DeletePageButton( { pageId } ) {
  82. const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
  83. // useSelect returns a list of selectors if you pass the store handle
  84. // instead of a callback:
  85. const { getLastEntityDeleteError } = useSelect( coreDataStore )
  86. const handleDelete = async () => {
  87. const success = await deleteEntityRecord( 'postType', 'page', pageId);
  88. if ( success ) {
  89. // Tell the user the operation succeeded:
  90. createSuccessNotice( "The page was deleted!", {
  91. type: 'snackbar',
  92. } );
  93. } else {
  94. // We use the selector directly to get the error at this point in time.
  95. // Imagine we fetched the error like this:
  96. // const { lastError } = useSelect( function() { /* ... */ } );
  97. // Then, lastError would be null inside of handleDelete.
  98. // Why? Because we'd refer to the version of it that was computed
  99. // before the handleDelete was even called.
  100. const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
  101. const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
  102. // Tell the user how exactly the operation have failed:
  103. createErrorNotice( message, {
  104. type: 'snackbar',
  105. } );
  106. }
  107. }
  108. const { deleteEntityRecord } = useDispatch( coreDataStore );
  109. const { isDeleting } = useSelect(
  110. select => ( {
  111. isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
  112. } ),
  113. [ pageId ]
  114. );
  115. return (
  116. <Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
  117. { isDeleting ? (
  118. <>
  119. <Spinner />
  120. Deleting...
  121. </>
  122. ) : 'Delete' }
  123. </Button>
  124. );
  125. }

次は何ですか?