gutenbergdocs/docs/how-to-guides/data-basics/3-building-an-edit-form.md
2025-10-22 01:40:18 +08:00

552 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 接下来做什么?
* **上一篇:** [构建页面列表](/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 = () => (
<Button variant="primary">
编辑
</Button>
)
function PagesList( { hasResolved, pages } ) {
if ( ! hasResolved ) {
return <Spinner />;
}
if ( ! pages?.length ) {
return <div>暂无结果</div>;
}
return (
<table className="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<td>标题</td>
<td style={{width: 120}}>操作</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={page.id}>
<td>{ decodeEntities( page.title.rendered ) }</td>
<td>
<PageEditButton pageId={ page.id } />
</td>
</tr>
) ) }
</tbody>
</table>
);
}
```
`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 (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
value=''
label='页面标题:'
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
保存
</Button>
<Button onClick={ onCancel } variant="tertiary">
取消
</Button>
</div>
</div>
);
}
```
现在让按钮触发显示刚创建的编辑表单。由于本教程不侧重网页设计,我们将使用需要最少代码量的[`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 (
<>
<Button
onClick={ openModal }
variant="primary"
>
编辑
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="编辑页面">
<EditPageForm
pageId={pageId}
onCancel={closeModal}
onSaveFinished={closeModal}
/>
</Modal>
) }
</>
)
}
```
现在点击*编辑*按钮,您将看到如下模态框:
![](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 (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label='页面标题:'
value={ page.title.rendered }
/>
{ /* ... */ }
</div>
);
}
```
现在效果应如图所示:
![](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 (
<div className="my-gutenberg-form">
{/* ... */}
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary">
保存
</Button>
{/* ... */}
</div>
</div>
);
}
```
虽然功能已实现,但还需修复一个问题:表单模态框不会自动关闭,因为我们从未调用 `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 (
<div className="my-gutenberg-form">
{/* ... */}
{ lastError ? (
<div className="form-error">
错误{ lastError.message }
</div>
) : false }
{/* ... */}
</div>
);
}
```
太棒了!现在 `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 (
// ...
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary" disabled={ ! hasEdits || isSaving }>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
// ...
);
}
```
请注意,当没有编辑内容或页面正在保存时,我们会禁用*保存*按钮。这是为了防止用户意外重复点击按钮。
此外,由于`@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 (
<>
<Button onClick={ openModal } variant="primary">
编辑
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="编辑页面">
<EditPageForm
pageId={ pageId }
onCancel={ closeModal }
onSaveFinished={ closeModal }
/>
</Modal>
) }
</>
);
}
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 (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label="页面标题:"
value={ page.title }
onChange={ handleChange }
/>
{ lastError ? (
<div className="form-error">错误{ lastError.message }</div>
) : (
false
) }
<div className="form-buttons">
<Button
onClick={ handleSave }
variant="primary"
disabled={ ! hasEdits || isSaving }
>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
</div>
);
}
```
### 步骤四:实现页面标题字段的可编辑功能
我们的*页面标题*字段存在一个问题:无法编辑。它接收固定值但在输入时不会更新。我们需要一个 `onChange` 处理函数。
您可能在其他 React 应用中也见过类似的模式,这被称为["受控组件"](https://reactjs.org/docs/forms.html#controlled-components)
```js
function VanillaReactForm({ initialTitle }) {
const [title, setTitle] = useState( initialTitle );
return (
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
value={ title }
onChange={ setTitle }
/>
);
}
```
在 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
```
它并未反映编辑后的内容。这是怎么回事?
实际上,`<PagesList />` 渲染的是 `getEntityRecord()` 返回的数据。如果 `getEntityRecord()` 反映了更新后的标题,那么用户在 `TextControl` 中输入的任何内容也会立即显示在 `<PagesList />` 中。这并不是我们想要的效果。在用户决定保存之前,编辑内容不应泄漏到表单之外。
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 (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label="页面标题:"
value={ page.title }
onChange={ handleChange }
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
保存
</Button>
<Button onClick={ onCancel } variant="tertiary">
取消
</Button>
</div>
</div>
);
}
```
我们添加了一个 `onChange` 处理函数,通过 `editEntityRecord` 操作跟踪编辑,然后将选择器更改为 `getEditedEntityRecord`,以便 `page.title` 始终反映更改。
现在的效果如下:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-editable.png)