ステップ 1: 編集ボタンを追加する

Edit フォームには Edit ボタンが必要ですので、まずは PagesList コンポーネントに追加しましょう:

  1. import { Button } from '@wordpress/components';
  2. import { decodeEntities } from '@wordpress/html-entities';
  3. const PageEditButton = () => (
  4. <Button variant="primary">
  5. Edit
  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: 120}}>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. <PageEditButton pageId={ page.id } />
  29. </td>
  30. </tr>
  31. ) ) }
  32. </tbody>
  33. </table>
  34. );
  35. }

PagesList の唯一の変更は、Actions とラベル付けされた追加の列です:

編集フォームの構築(Building an edit form) - img1

ステップ 2: 編集フォームを表示する

ボタンは見栄えが良いですが、まだ何も機能していません。編集フォームを表示するには、まずそれを作成する必要があります:

  1. import { Button, TextControl } from '@wordpress/components';
  2. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  3. return (
  4. <div className="my-gutenberg-form">
  5. <TextControl
  6. value=''
  7. label='Page title:'
  8. />
  9. <div className="form-buttons">
  10. <Button onClick={ onSaveFinished } variant="primary">
  11. Save
  12. </Button>
  13. <Button onClick={ onCancel } variant="tertiary">
  14. Cancel
  15. </Button>
  16. </div>
  17. </div>
  18. );
  19. }

次に、ボタンが作成したフォームを表示するようにします。このチュートリアルはウェブデザインに焦点を当てていないため、最小限のコードで二つを接続するコンポーネントを使用します: ModalPageEditButton をそれに応じて更新しましょう:

  1. import { Button, Modal, TextControl } from '@wordpress/components';
  2. function PageEditButton({ pageId }) {
  3. const [ isOpen, setOpen ] = useState( false );
  4. const openModal = () => setOpen( true );
  5. const closeModal = () => setOpen( false );
  6. return (
  7. <>
  8. <Button
  9. onClick={ openModal }
  10. variant="primary"
  11. >
  12. Edit
  13. </Button>
  14. { isOpen && (
  15. <Modal onRequestClose={ closeModal } title="Edit page">
  16. <EditPageForm
  17. pageId={pageId}
  18. onCancel={closeModal}
  19. onSaveFinished={closeModal}
  20. />
  21. </Modal>
  22. ) }
  23. </>
  24. )
  25. }

今、Edit ボタンをクリックすると、次のモーダルが表示されるはずです:

編集フォームの構築(Building an edit form) - img2

素晴らしい!これで基本的なユーザーインターフェースが整いました。

ステップ 3: ページの詳細でフォームを埋める

EditPageForm が現在編集中のページのタイトルを表示するようにしたいです。page プロップを受け取らず、pageId のみを受け取ることに気づいたかもしれません。それは問題ありません。Gutenberg Data は、任意のコンポーネントからエンティティレコードに簡単にアクセスできるようにします。

この場合、getEntityRecord セレクタを使用する必要があります。レコードのリストは getEntityRecords コールのおかげで既に利用可能で、追加の HTTP リクエストは発生しません – キャッシュされたレコードをすぐに取得できます。

ブラウザの開発者ツールで試す方法は次のとおりです:

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

EditPageForm をそれに応じて更新しましょう:

  1. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  2. const page = useSelect(
  3. select => select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId ),
  4. [pageId]
  5. );
  6. return (
  7. <div className="my-gutenberg-form">
  8. <TextControl
  9. label='Page title:'
  10. value={ page.title.rendered }
  11. />
  12. { /* ... */ }
  13. </div>
  14. );
  15. }

今は次のように見えるはずです:

編集フォームの構築(Building an edit form) - img3

ステップ 4: ページタイトルフィールドを編集可能にする

Page title フィールドには一つの問題があります: 編集できません。固定の value を受け取りますが、入力中に更新されません。onChange ハンドラーが必要です。

他の React アプリでこのようなパターンを見たことがあるかもしれません。これは “controlled component” として知られています:

  1. function VanillaReactForm({ initialTitle }) {
  2. const [title, setTitle] = useState( initialTitle );
  3. return (
  4. <TextControl
  5. value={ title }
  6. onChange={ setTitle }
  7. />
  8. );
  9. }

Gutenberg Data でエンティティレコードを更新するのは似ていますが、ローカル(コンポーネントレベル)状態に setTitle を使用する代わりに、Redux 状態に更新を保存する editEntityRecord アクションを使用します。ブラウザの開発者ツールで試す方法は次のとおりです:

  1. // We need a valid page ID to call editEntityRecord, so let's get the first available one using getEntityRecords.
  2. const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
  3. // Update the title
  4. wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', pageId, { title: 'updated title' } );

この時点で、editEntityRecorduseState よりも優れているのは どう かと尋ねるかもしれません。その答えは、他では得られないいくつかの機能を提供するからです。

まず、データを取得するのと同じくらい簡単に変更を保存でき、すべてのキャッシュが正しく更新されることを保証します。

次に、editEntityRecord を介して適用された変更は、undo および redo アクションを介して簡単に元に戻すことができます。

最後に、変更が Redux 状態に存在するため、それらは「グローバル」であり、他のコンポーネントからアクセスできます。たとえば、PagesList が現在編集中のタイトルを表示するようにできます。

その最後のポイントに関して、getEntityRecord を使用して、先ほど更新したエンティティレコードにアクセスするとどうなるか見てみましょう:

  1. wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title

編集が反映されていません。何が起こっているのでしょうか?

<PagesList />getEntityRecord() によって返されたデータをレンダリングします。getEntityRecord() が更新されたタイトルを反映している場合、ユーザーが TextControl に入力した内容は <PagesList /> 内にも即座に表示されるはずです。これは望ましくありません。編集は、ユーザーが保存することを決定するまでフォームの外に漏れ出すべきではありません。

Gutenberg Data は、Entity RecordsEdited Entity Records の間に区別を設けることでこの問題を解決します。Entity Records は API からのデータを反映し、ローカル編集を無視しますが、Edited Entity Records はすべてのローカル編集が適用されたものです。両者は同時に Redux 状態に共存します。

getEditedEntityRecord を呼び出すとどうなるか見てみましょう:

  1. wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title
  2. // "updated title"
  3. wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
  4. // { "rendered": "<original, unchanged title>", "raw": "..." }

ご覧のとおり、エンティティレコードの title はオブジェクトですが、編集されたエンティティレコードの title は文字列です。

これは偶然ではありません。titleexcerptcontent のようなフィールドは shortcodesdynamic blocks を含む可能性があり、サーバー上でのみレンダリングできます。そのため、REST API は raw マークアップ rendered 文字列の両方を公開します。たとえば、ブロックエディタでは、content.rendered がビジュアルプレビューとして使用され、content.raw がコードエディタを埋めるために使用される可能性があります。

では、なぜ編集されたエンティティレコードの content が文字列なのでしょうか?JavaScript は任意のブロックマークアップを適切にレンダリングできないため、raw マークアップのみを rendered 部分なしで保存します。そして、それが文字列であるため、フィールド全体が文字列になります。

EditPageForm をそれに応じて更新できます。useDispatch フックを使用してアクションにアクセスし、useSelect を使用してセレクタにアクセスするのと同様にします:

  1. import { useDispatch } from '@wordpress/data';
  2. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  3. const page = useSelect(
  4. select => select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
  5. [ pageId ]
  6. );
  7. const { editEntityRecord } = useDispatch( coreDataStore );
  8. const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title } );
  9. return (
  10. <div className="my-gutenberg-form">
  11. <TextControl
  12. label="Page title:"
  13. value={ page.title }
  14. onChange={ handleChange }
  15. />
  16. <div className="form-buttons">
  17. <Button onClick={ onSaveFinished } variant="primary">
  18. Save
  19. </Button>
  20. <Button onClick={ onCancel } variant="tertiary">
  21. Cancel
  22. </Button>
  23. </div>
  24. </div>
  25. );
  26. }

onChange ハンドラーを追加して editEntityRecord アクションを介して編集を追跡し、その後 getEditedEntityRecord にセレクタを変更して page.title が常に変更を反映するようにしました。

今は次のように見えます:

編集フォームの構築(Building an edit form) - img4

ステップ 5: フォームデータを保存する

ページタイトルを編集できるようになったので、保存できることも確認しましょう。Gutenberg データでは、saveEditedEntityRecord アクションを使用して WordPress REST API に変更を保存します。リクエストを送信し、結果を処理し、Redux 状態内のキャッシュデータを更新します。

ブラウザの開発者ツールで試すことができる例は次のとおりです:

  1. // Replace 9 with an actual page ID
  2. wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', 9, { title: 'updated title' } );
  3. wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'page', 9 );

上記のスニペットは新しいタイトルを保存しました。以前とは異なり、getEntityRecord は現在更新されたタイトルを反映しています:

  1. // Replace 9 with an actual page ID
  2. wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ).title.rendered
  3. // "updated title"

エンティティレコードは、REST API リクエストが完了した後に保存された変更を反映するように更新されます。

EditPageForm は動作する Save ボタンを持つように見えます:

  1. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  2. // ...
  3. const { saveEditedEntityRecord } = useDispatch( coreDataStore );
  4. const handleSave = () => saveEditedEntityRecord( 'postType', 'page', pageId );
  5. return (
  6. <div className="my-gutenberg-form">
  7. {/* ... */}
  8. <div className="form-buttons">
  9. <Button onClick={ handleSave } variant="primary">
  10. Save
  11. </Button>
  12. {/* ... */}
  13. </div>
  14. </div>
  15. );
  16. }

動作しますが、まだ修正すべき点が一つあります: フォームモーダルは自動的に閉じません。onSaveFinished を呼び出さなかったからです。幸運なことに、saveEditedEntityRecord は保存操作が完了すると解決される Promise を返します。それを EditPageForm で利用しましょう:

  1. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  2. // ...
  3. const handleSave = async () => {
  4. await saveEditedEntityRecord( 'postType', 'page', pageId );
  5. onSaveFinished();
  6. };
  7. // ...
  8. }

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

私たちは楽観的に save 操作が常に成功するだろうと仮定しました。残念ながら、さまざまな理由で失敗する可能性があります:

  • ウェブサイトがダウンしている
  • 更新が無効である
  • ページがその間に他の誰かによって削除された可能性がある

これらのいずれかが発生した場合にユーザーに通知するために、二つの調整を行う必要があります。更新が失敗した場合、フォームモーダルを閉じたくありません。saveEditedEntityRecord が返す Promise は、更新が実際に成功した場合にのみ更新されたレコードで解決されます。何かがうまくいかないと、空の値で解決されます。それを使用してモーダルを開いたままにしましょう:

  1. function EditPageForm( { pageId, onSaveFinished } ) {
  2. // ...
  3. const handleSave = async () => {
  4. const updatedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
  5. if ( updatedRecord ) {
  6. onSaveFinished();
  7. }
  8. };
  9. // ...
  10. }

素晴らしい!次に、エラーメッセージを表示しましょう。失敗の詳細は getLastEntitySaveError セレクタを使用して取得できます:

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

それを EditPageForm で使用する方法は次のとおりです:

  1. function EditPageForm( { pageId, onSaveFinished } ) {
  2. // ...
  3. const { lastError, page } = useSelect(
  4. select => ({
  5. page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
  6. lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId )
  7. }),
  8. [ pageId ]
  9. )
  10. // ...
  11. return (
  12. <div className="my-gutenberg-form">
  13. {/* ... */}
  14. { lastError ? (
  15. <div className="form-error">
  16. Error: { lastError.message }
  17. </div>
  18. ) : false }
  19. {/* ... */}
  20. </div>
  21. );
  22. }

素晴らしい!EditPageForm は現在エラーを完全に認識しています。

そのエラーメッセージを実際に見てみましょう。無効な更新をトリガーして失敗させます。投稿タイトルは壊れにくいので、date プロパティを -1 に設定しましょう – それは確実なバリデーションエラーです:

  1. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  2. // ...
  3. const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title, date: -1 } );
  4. // ...
  5. }

ページをリフレッシュし、フォームを開き、タイトルを変更して保存を押すと、次のエラーメッセージが表示されるはずです:

編集フォームの構築(Building an edit form) - img5

素晴らしい!これで handleChange の以前のバージョンを復元 し、次のステップに進むことができます。

ステップ 7: ステータスインジケーター

私たちのフォームにはもう一つの問題があります: 視覚的なフィードバックがありません。Save ボタンが機能したかどうかは、フォームが消えるかエラーメッセージが表示されるまで確信できません。

これを明確にし、ユーザーに二つの状態を伝えます: SavingNo changes detected。関連するセレクタは isSavingEntityRecordhasEditsForEntityRecord です。getEntityRecord とは異なり、これらは HTTP リクエストを発行せず、現在のエンティティレコードの状態のみを返します。

EditPageForm でそれらを使用しましょう:

  1. function EditPageForm( { pageId, onSaveFinished } ) {
  2. // ...
  3. const { isSaving, hasEdits, /* ... */ } = useSelect(
  4. select => ({
  5. isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
  6. hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
  7. // ...
  8. }),
  9. [ pageId ]
  10. )
  11. }

isSavinghasEdits を使用して、保存中にスピナーを表示し、編集がない場合は保存ボタンをグレーアウトできます:

  1. function EditPageForm( { pageId, onSaveFinished } ) {
  2. // ...
  3. return (
  4. // ...
  5. <div className="form-buttons">
  6. <Button onClick={ handleSave } variant="primary" disabled={ ! hasEdits || isSaving }>
  7. { isSaving ? (
  8. <>
  9. <Spinner/>
  10. Saving
  11. </>
  12. ) : 'Save' }
  13. </Button>
  14. <Button
  15. onClick={ onCancel }
  16. variant="tertiary"
  17. disabled={ isSaving }
  18. >
  19. Cancel
  20. </Button>
  21. </div>
  22. // ...
  23. );
  24. }

編集がない場合やページが現在保存中の場合、save ボタンを無効にします。これは、ユーザーが誤ってボタンを二回押すのを防ぐためです。

また、進行中の save を中断することは @wordpress/data ではサポートされていないため、cancel ボタンも条件付きで無効にしました。

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

編集フォームの構築(Building an edit form) - img6

編集フォームの構築(Building an edit form) - img7

すべてをつなげる

すべての要素が揃いました、素晴らしい!この章で構築したすべてを一つの場所にまとめました:

  1. import { useDispatch } from '@wordpress/data';
  2. import { Button, Modal, TextControl } from '@wordpress/components';
  3. function PageEditButton( { pageId } ) {
  4. const [ isOpen, setOpen ] = useState( false );
  5. const openModal = () => setOpen( true );
  6. const closeModal = () => setOpen( false );
  7. return (
  8. <>
  9. <Button onClick={ openModal } variant="primary">
  10. Edit
  11. </Button>
  12. { isOpen && (
  13. <Modal onRequestClose={ closeModal } title="Edit page">
  14. <EditPageForm
  15. pageId={ pageId }
  16. onCancel={ closeModal }
  17. onSaveFinished={ closeModal }
  18. />
  19. </Modal>
  20. ) }
  21. </>
  22. );
  23. }
  24. function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
  25. const { page, lastError, isSaving, hasEdits } = useSelect(
  26. ( select ) => ( {
  27. page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
  28. lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ),
  29. isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
  30. hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
  31. } ),
  32. [ pageId ]
  33. );
  34. const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore );
  35. const handleSave = async () => {
  36. const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
  37. if ( savedRecord ) {
  38. onSaveFinished();
  39. }
  40. };
  41. const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } );
  42. return (
  43. <div className="my-gutenberg-form">
  44. <TextControl
  45. label="Page title:"
  46. value={ page.title }
  47. onChange={ handleChange }
  48. />
  49. { lastError ? (
  50. <div className="form-error">Error: { lastError.message }</div>
  51. ) : (
  52. false
  53. ) }
  54. <div className="form-buttons">
  55. <Button
  56. onClick={ handleSave }
  57. variant="primary"
  58. disabled={ ! hasEdits || isSaving }
  59. >
  60. { isSaving ? (
  61. <>
  62. <Spinner/>
  63. Saving
  64. </>
  65. ) : 'Save' }
  66. </Button>
  67. <Button
  68. onClick={ onCancel }
  69. variant="tertiary"
  70. disabled={ isSaving }
  71. >
  72. Cancel
  73. </Button>
  74. </div>
  75. </div>
  76. );
  77. }

次は何ですか?