EP189. “LinkControl 链接选择器与 useState 驱动 Popover”
🔒 登录后可标记已读给按钮 Block 加上真正的「选链接」功能:点击悬浮工具栏上的链接图标,弹出一个可以搜索站内文章/页面、也能直接输入任意 URL 的选择器(复用 WordPress 官方现成的 LinkControl 组件,不用自己写搜索逻辑)。「弹窗是否可见」这个状态用 React 的 useState(而不是 Block 的 attributes)管理——因为这纯粹是编辑器里临时的 UI 状态,用户下次打开页面时不需要记得「上次链接选择器是不是开着的」,不该跟着内容一起存进数据库。选好的链接对象(包含 URL、标题、文章 ID 等)存进新增的 linkObject 属性,最终 save 阶段只取用其中的 url 字段。
涉及文件
wp-content/themes/fictional-university-block-theme/our-blocks/genericbutton.js(修改)wp-content/themes/fictional-university-block-theme/package.json(新增依赖@wordpress/icons)
代码实现
终端命令:安装链接图标专用的包(@wordpress/scripts 无法像其他包一样自动识别别名):
npm install @wordpress/icons
our-blocks/genericbutton.js(完整文件):
import { link } from "@wordpress/icons"
import { ToolbarGroup, ToolbarButton, Popover, Button } from "@wordpress/components"
import { RichText, BlockControls, __experimentalLinkControl as LinkControl } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
import { useState } from "@wordpress/element"
registerBlockType("ourblocktheme/genericbutton", {
title: "Generic Button",
attributes: {
text: { type: "string" },
size: { type: "string", default: "large" },
linkObject: { type: "object" }
},
edit: EditComponent,
save: SaveComponent
})
function EditComponent(props) {
const [isLinkPickerVisible, setIsLinkPickerVisible] = useState(false)
function handleTextChange(x) {
props.setAttributes({ text: x })
}
function buttonHandler() {
setIsLinkPickerVisible(prev => !prev)
}
function handleLinkChange(newLink) {
props.setAttributes({ linkObject: newLink })
}
return (
<>
<BlockControls>
<ToolbarGroup>
<ToolbarButton onClick={buttonHandler} icon={link} />
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton isPressed={props.attributes.size === "large"} onClick={() => props.setAttributes({ size: "large" })}>
Large
</ToolbarButton>
<ToolbarButton isPressed={props.attributes.size === "medium"} onClick={() => props.setAttributes({ size: "medium" })}>
Medium
</ToolbarButton>
<ToolbarButton isPressed={props.attributes.size === "small"} onClick={() => props.setAttributes({ size: "small" })}>
Small
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
<RichText allowedFormats={[]} tagName="a" className={`btn btn--${props.attributes.size} btn--blue`} value={props.attributes.text} onChange={handleTextChange} />
{isLinkPickerVisible && (
<Popover position="middle center">
<LinkControl settings={[]} value={props.attributes.linkObject} onChange={handleLinkChange} />
<Button variant="primary" onClick={() => setIsLinkPickerVisible(false)} style={{ display: "block", width: "100%" }}>
Confirm Link
</Button>
</Popover>
)}
</>
)
}
function SaveComponent(props) {
return (
<a href={props.attributes.linkObject.url} className={`btn btn--${props.attributes.size} btn--blue`}>
{props.attributes.text}
</a>
)
}
关键改动点:
- 新增
linkObject属性,类型是object:WordPress 的LinkControl选中一个链接后,返回的不只是一个 URL 字符串,而是一整个对象(包含 URL、标题、文章 ID、文章类型等信息)——这里选择把整个对象原样存下来,而不是只挑url一个字段存成字符串,保留了以后想用其他信息(比如按 ID 而不是写死的 URL 做更稳健的关联)的可能性 isLinkPickerVisible用useState而不是 Blockattributes管理:这是这一讲刻意强调的设计判断——「链接选择弹窗现在是否可见」纯粹是一个临时的编辑器 UI 状态,用户离开页面重新打开时,没有必要记得上次弹窗是开着的,所以不应该存进数据库(也就是不该做成attributes的一部分),用组件本地的 React 状态就够了import { useState } from "@wordpress/element":在 WordPress 环境下,React 是通过@wordpress/element这个包「转手」提供给开发者的,而不是直接import React from 'react'——作者推测这是 WordPress 特意设计的一层抽象,万一未来官方想换掉 React、改用别的前端库,开发者只依赖@wordpress/element这个抽象层,而不是直接依赖 React 本身,替换起来影响更小ToolbarButton onClick={buttonHandler} icon={link}——只给图标不给文字的工具栏按钮:跟大/中/小那三个纯文字按钮不同,这里用iconprop 传入一个图标组件(link,从@wordpress/icons引入的官方链接图标),而不是在标签内容里写文字@wordpress/icons是一个例外,需要真正npm install:这门课其他 WordPress 包(@wordpress/blocks、@wordpress/block-editor、@wordpress/components、@wordpress/element等)都能被@wordpress/scripts的 Webpack 配置自动识别、转成读取浏览器全局已加载的对应模块,不需要真的执行安装;但@wordpress/icons这个包目前没有被那份自动识别的清单覆盖到,所以必须先老老实实npm install @wordpress/icons,装完之后要重启npm run start任务buttonHandler()用「翻转前一个值」的写法实现开关切换:setIsLinkPickerVisible(prev => !prev)——给setState系列函数传一个「基于前一个值计算新值」的函数,而不是直接给一个写死的新值,这样点击同一个按钮就能在「显示/隐藏」之间来回切换{isLinkPickerVisible && (<Popover>...</Popover>)}:这门课反复用过的 JSX 条件渲染写法——&&短路运算,只有左边条件为真时,右边的 JSX 才会被渲染;为假时&&整体是false,React 直接跳过不渲染任何东西Popover(@wordpress/components):现成的「悬浮弹层」组件,position="middle center"控制弹层相对于触发它的元素居中显示在下方——不需要自己写定位/动画的 CSS__experimentalLinkControl as LinkControl——起了个更好读的别名:这是 WordPress 内建的「链接选择器」组件,支持搜索站内文章/页面、也支持直接粘贴/输入任意 URL;因为它的原始导出名字带着__experimental前缀(说明这是当时还处于「实验性」阶段、以后可能改名或调整的 API),作者提前用as LinkControl起了个干净的别名,方便代码里书写和阅读<LinkControl settings={[]} value={props.attributes.linkObject} onChange={handleLinkChange} />:settings传空数组表示不需要额外的自定义设置项(比如「在新标签页打开」这类选项);value回显当前已选的链接对象;onChange在用户选中新链接时触发,传入的参数(这里命名为newLink)就是新的链接对象handleLinkChange(newLink):直接用props.setAttributes({linkObject: newLink})把整个新链接对象存进属性- 弹窗内的「Confirm Link」按钮:点击后调用
setIsLinkPickerVisible(false)把弹窗关掉——这里给了style={{display: "block", width: "100%"}}让这个按钮变成块级元素、撑满弹层宽度,纯粹是视觉调整 save阶段最终只用到linkObject.url这一个字段:href={props.attributes.linkObject.url}——虽然linkObject存了一整套信息(ID、标题、文章类型等),作者提到「如果想做得更严谨/抗变化,可以存 ID、用 REST API 或 PHP 在渲染时动态查询当前的最新链接(这样即使以后固定链接改了也不会失效)」,但眼下这个阶段没必要做得这么复杂,直接用当时选中的 URL 就够用- 改动
save输出后,已有的旧 Block 实例会再次触发「区块已被修改」的冲突提示:跟 EP184 遇到的情况一样,需要手动点击「尝试恢复区块」、重新走一遍选择流程再保存——这也是作者提前预告「以后学会用 PHPrender_callback就能避免这个问题」的其中一个原因
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
useState(@wordpress/element) | React Hook(WordPress 转手提供) | 管理纯编辑器 UI 层面的临时状态,不需要持久化到数据库时优先用这个而不是 attributes |
Popover(@wordpress/components) | React 组件 | 提供悬浮弹层容器,position 控制相对触发元素的定位 |
__experimentalLinkControl(起别名为 LinkControl) | React 组件(@wordpress/block-editor) | WordPress 内建的链接选择器,支持搜索站内内容或输入任意 URL |
link(@wordpress/icons) | 图标资源 | 官方链接图标,需要真正执行 npm install @wordpress/icons 才能使用 |
常见坑
- 把「弹窗是否可见」这种纯 UI 状态也存进 Block
attributes——这类数据没必要持久化,应该用useState管理在组件本地 - 以为所有
@wordpress/*包都不需要真正npm install——@wordpress/icons是例外,不装的话import { link } from "@wordpress/icons"会找不到模块 setState的开关切换直接写setIsLinkPickerVisible(true)——这样点击按钮永远只会「打开」,做不到「再点一次就关闭」的切换效果,要用prev => !prev这种基于前一个值取反的写法save阶段忘记同步把href="#"改成props.attributes.linkObject.url——链接选择器选完之后前台按钮还是死链接- 修改了
save函数的输出后,没有重新回到编辑器手动点「尝试恢复区块」并保存——数据库里存的还是旧版 HTML,前台看不到最新效果
[截图:点击按钮 Block 工具栏上的链接图标后弹出的 LinkControl 悬浮搜索框,可以搜索站内文章/页面或输入 URL]
延伸 / 后续讲座会用到
下一讲要给按钮加上「选颜色」的功能,把目前写死的 btn--blue 换成可以在编辑器里动态选择的选项。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 28, EP189