## 接下来做什么? * **上一篇:** [构建页面列表](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) * **下一篇:** [构建创建页面表单](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md) * (可选)在 block-development-examples 代码库中查看[完整示例应用](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) # 构建编辑表单 本节内容将为我们的应用添加*编辑*功能。以下是即将构建功能的预览图: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-finished.png) ### 步骤一:添加“编辑”按钮 要实现*编辑*功能首先需要添加编辑按钮,让我们从在`PagesList`组件中添加按钮开始: ```js import { Button } from '@wordpress/components'; import { decodeEntities } from '@wordpress/html-entities'; const PageEditButton = () => ( ) function PagesList( { hasResolved, pages } ) { if ( ! hasResolved ) { return ; } if ( ! pages?.length ) { return
暂无结果
; } return ( { pages?.map( ( page ) => ( ) ) }
标题 操作
{ decodeEntities( page.title.rendered ) }
); } ``` `PagesList`组件中唯一的变化是新增了标为“操作”的列: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/edit-button.png) ### 步骤二:显示编辑表单 我们的按钮外观不错但尚未实现功能。要显示编辑表单,首先需要创建它: ```js import { Button, TextControl } from '@wordpress/components'; function EditPageForm( { pageId, onCancel, onSaveFinished } ) { return (
); } ``` 现在让按钮触发显示刚创建的编辑表单。由于本教程不侧重网页设计,我们将使用需要最少代码量的[`Modal`](https://developer.wordpress.org/block-editor/reference-guides/components/modal/)组件将两者连接。更新`PageEditButton`如下: ```js import { Button, Modal, TextControl } from '@wordpress/components'; function PageEditButton({ pageId }) { const [ isOpen, setOpen ] = useState( false ); const openModal = () => setOpen( true ); const closeModal = () => setOpen( false ); return ( <> { isOpen && ( ) } ) } ``` 现在点击*编辑*按钮,您将看到如下模态框: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-scaffold.png) 很好!我们现在有了可操作的基础用户界面。 ### 步骤三:在表单中填充页面详情 我们需要让`EditPageForm`显示当前编辑页面的标题。您可能注意到它并未接收`page`属性,仅接收`pageId`。这没有问题,Gutenberg Data让我们能够轻松在任何组件中访问实体记录。 这里我们需要使用[`getEntityRecord`](/docs/reference-guides/data/data-core.md#getentityrecord)选择器。得益于`MyFirstApp`中的`getEntityRecords`调用,记录列表已准备就绪,甚至无需发起额外的HTTP请求——我们可以直接获取缓存记录。 您可以在浏览器开发工具中这样尝试: ```js wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ); // 将9替换为实际页面ID ``` 接下来更新`EditPageForm`: ```js function EditPageForm( { pageId, onCancel, onSaveFinished } ) { const page = useSelect( select => select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId ), [pageId] ); return (
{ /* ... */ }
); } ``` 现在效果应如图所示: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-populated.png) ### 步骤五:保存表单数据 既然我们已经能够编辑页面标题,接下来要确保能够保存它。在 Gutenberg 数据系统中,我们使用 `saveEditedEntityRecord` 操作将变更保存到 WordPress REST API。该操作会发送请求、处理结果,并更新 Redux 状态中的缓存数据。 以下示例可在浏览器开发者工具中尝试: ```js // 将数字9替换为实际页面ID wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', 9, { title: '更新后的标题' } ); wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'page', 9 ); ``` 以上代码片段保存了新标题。与之前不同,现在 `getEntityRecord` 会反映更新后的标题: ```js // 将数字9替换为实际页面ID wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ).title.rendered // 返回:"更新后的标题" ``` 在 REST API 请求完成后,实体记录会立即更新以反映所有已保存的变更。 这是带有生效*保存*按钮的 `EditPageForm` 组件示例: ```js function EditPageForm( { pageId, onCancel, onSaveFinished } ) { // ... const { saveEditedEntityRecord } = useDispatch( coreDataStore ); const handleSave = () => saveEditedEntityRecord( 'postType', 'page', pageId ); return (
{/* ... */}
{/* ... */}
); } ``` 虽然功能已实现,但还需修复一个问题:表单模态框不会自动关闭,因为我们从未调用 `onSaveFinished`。幸运的是,`saveEditedEntityRecord` 返回的 Promise 会在保存操作完成后解析。让我们在 `EditPageForm` 中利用这个特性: ```js function EditPageForm( { pageId, onCancel, onSaveFinished } ) { // ... const handleSave = async () => { await saveEditedEntityRecord( 'postType', 'page', pageId ); onSaveFinished(); }; // ... } ``` ### 步骤六:错误处理 此前我们乐观地假设*保存*操作总能成功。但实际操作可能因以下原因失败: * 网站可能宕机 * 更新内容可能无效 * 页面可能已被他人删除 为了在出现这些问题时通知用户,我们需要进行两处调整。当更新失败时,我们不希望关闭表单模态框。仅当更新确实成功时,`saveEditedEntityRecord` 返回的 Promise 才会解析为更新后的记录。若出现异常,则会解析为空值。我们可以利用这一点来保持模态框开启状态: ```js function EditPageForm( { pageId, onSaveFinished } ) { // ... const handleSave = async () => { const updatedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId ); if ( updatedRecord ) { onSaveFinished(); } }; // ... } ``` 很好!现在让我们来显示错误信息。可以通过 `getLastEntitySaveError` 选择器获取失败详情: ```js // 将数字9替换为实际页面ID wp.data.select( 'core' ).getLastEntitySaveError( 'postType', 'page', 9 ) ``` 以下是在 `EditPageForm` 中的具体应用: ```js function EditPageForm( { pageId, onSaveFinished } ) { // ... const { lastError, page } = useSelect( select => ({ page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ), lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ) }), [ pageId ] ) // ... return (
{/* ... */} { lastError ? (
错误:{ lastError.message }
) : false } {/* ... */}
); } ``` 太棒了!现在 `EditPageForm` 已能完全感知错误状态。 让我们通过触发无效更新来查看错误提示效果。由于文章标题很难引发错误,我们可以将 `date` 属性设置为 `-1` —— 这必定会触发验证错误: ```js function EditPageForm( { pageId, onCancel, onSaveFinished } ) { // ... const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title, date: -1 } ); // ... } ``` 刷新页面后,打开表单修改标题并点击保存,您将看到如下错误提示: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-error.png) 非常好!现在我们可以**恢复 `handleChange` 函数的先前版本**,继续下一步操作。 ### 步骤七:状态指示器 我们的表单还存在最后一个问题:缺乏视觉反馈。在表单消失或显示错误信息之前,我们无法完全确定*保存*按钮是否生效。 现在我们将解决这个问题,并向用户传达两种状态:_保存中_和_未检测到更改_。相关的选择器是`isSavingEntityRecord`和`hasEditsForEntityRecord`。与`getEntityRecord`不同,这些选择器从不发起HTTP请求,仅返回当前实体记录状态。 让我们在`EditPageForm`中使用它们: ```js function EditPageForm( { pageId, onSaveFinished } ) { // ... const { isSaving, hasEdits, /* ... */ } = useSelect( select => ({ isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ), hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ), // ... }), [ pageId ] ) } ``` 现在我们可以使用`isSaving`和`hasEdits`在保存过程中显示加载动画,并在无编辑内容时禁用保存按钮: ```js function EditPageForm( { pageId, onSaveFinished } ) { // ... return ( // ...
// ... ); } ``` 请注意,当没有编辑内容或页面正在保存时,我们会禁用*保存*按钮。这是为了防止用户意外重复点击按钮。 此外,由于`@wordpress/data`不支持中断正在进行的*保存*操作,我们也相应禁用了*取消*按钮。 实际运行效果如下: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-inactive.png) ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-spinner.png) ### 整体联调 所有组件都已就位,太棒了!以下是我们本章构建的完整代码: ```js import { useDispatch } from '@wordpress/data'; import { Button, Modal, TextControl } from '@wordpress/components'; function PageEditButton( { pageId } ) { const [ isOpen, setOpen ] = useState( false ); const openModal = () => setOpen( true ); const closeModal = () => setOpen( false ); return ( <> { isOpen && ( ) } ); } function EditPageForm( { pageId, onCancel, onSaveFinished } ) { const { page, lastError, isSaving, hasEdits } = useSelect( ( select ) => ( { page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ), lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ), isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ), hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ), } ), [ pageId ] ); const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore ); const handleSave = async () => { const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId ); if ( savedRecord ) { onSaveFinished(); } }; const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } ); return (
{ lastError ? (
错误:{ lastError.message }
) : ( false ) }
); } ``` ### 步骤四:实现页面标题字段的可编辑功能 我们的*页面标题*字段存在一个问题:无法编辑。它接收固定值但在输入时不会更新。我们需要一个 `onChange` 处理函数。 您可能在其他 React 应用中也见过类似的模式,这被称为["受控组件"](https://reactjs.org/docs/forms.html#controlled-components): ```js function VanillaReactForm({ initialTitle }) { const [title, setTitle] = useState( initialTitle ); return ( ); } ``` 在 Gutenberg 数据中更新实体记录与此类似,但不同之处在于,我们不使用 `setTitle` 将数据存储在本地(组件级别)状态,而是使用 `editEntityRecord` 操作将更新存储在 *Redux* 状态中。以下是在浏览器的开发工具中尝试的方法: ```js // 我们需要一个有效的页面 ID 来调用 editEntityRecord,因此使用 getEntityRecords 获取第一个可用的 ID。 const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id; // 更新标题 wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', pageId, { title: '更新后的标题' } ); ``` 此时,您可能会问,`editEntityRecord` 比 `useState` 好在哪里?答案是它提供了一些额外功能。 首先,我们可以像检索数据一样轻松地保存更改,并确保所有缓存都能正确更新。 其次,通过 `editEntityRecord` 应用的更改可以通过 `undo` 和 `redo` 操作轻松撤销或重做。 最后,由于更改存储在 *Redux* 状态中,它们是“全局的”,可以被其他组件访问。例如,我们可以让 `PagesList` 显示当前编辑的标题。 关于最后一点,让我们看看使用 `getEntityRecord` 访问刚刚更新的实体记录时会发生什么: ```js wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title ``` 它并未反映编辑后的内容。这是怎么回事? 实际上,`` 渲染的是 `getEntityRecord()` 返回的数据。如果 `getEntityRecord()` 反映了更新后的标题,那么用户在 `TextControl` 中输入的任何内容也会立即显示在 `` 中。这并不是我们想要的效果。在用户决定保存之前,编辑内容不应泄漏到表单之外。 Gutenberg 数据通过区分*实体记录*和*已编辑的实体记录*来解决这个问题。*实体记录*反映来自 API 的数据,忽略任何本地编辑,而*已编辑的实体记录*则在数据基础上应用了所有本地编辑。两者同时存在于 Redux 状态中。 让我们看看调用 `getEditedEntityRecord` 会发生什么: ```js wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title // "更新后的标题" wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title // { "rendered": "<原始未更改的标题>", "raw": "..." } ``` 如您所见,实体记录的 `title` 是一个对象,而已编辑实体记录的 `title` 是一个字符串。 这并非偶然。像 `title`、`excerpt` 和 `content` 这样的字段可能包含[短代码](https://developer.wordpress.org/apis/handbook/shortcode/)或[动态区块](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md),这意味着它们只能在服务器上渲染。对于这些字段,REST API 同时暴露了 `raw` 标记和 `rendered` 字符串。例如,在区块编辑器中,`content.rendered` 可用于视觉预览,而 `content.raw` 可用于填充代码编辑器。 那么,为什么已编辑实体记录的 `content` 是一个字符串?由于 JavaScript 无法正确渲染任意的区块标记,它仅存储 `raw` 标记,而不包含 `rendered` 部分。由于这是一个字符串,整个字段就变成了字符串。 现在我们可以相应地更新 `EditPageForm`。我们可以使用 [`useDispatch`](/packages/data/README.md#usedispatch) 钩子访问操作,类似于使用 `useSelect` 访问选择器: ```js import { useDispatch } from '@wordpress/data'; function EditPageForm( { pageId, onCancel, onSaveFinished } ) { const page = useSelect( select => select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ), [ pageId ] ); const { editEntityRecord } = useDispatch( coreDataStore ); const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title } ); return (
); } ``` 我们添加了一个 `onChange` 处理函数,通过 `editEntityRecord` 操作跟踪编辑,然后将选择器更改为 `getEditedEntityRecord`,以便 `page.title` 始终反映更改。 现在的效果如下: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-editable.png)