ant-design Tag 数据驱动动态标签:用数组生成与管理可关闭标签的完整实践
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/gh_mirrors/antde/ant-design
导读
本文以 ant-design 仓库中的 Tag 组件演示文档 components/tag/demo/control.md 为核心,讲解如何用数组(State 数据)驱动生成一组标签,并实现"点击关闭即从数据源移除、点击按钮即追加新标签"的完整增删交互。文章将完整继承原文档的示例代码,并深入到 Tag 组件源码 与 标签样式 中,剖析afterClose与onClose的差异、关闭动画的底层触发链路,帮助你掌握在真实业务场景(如关键词管理、筛选条件集合)中安全地动态管理标签。
一、场景与需求:为什么需要"数据生成标签"
在 ant-design 的 Tag 组件文档 中,Tag 被定义为"进行标记和分类的小标签"。但静态写死的标签(见 basic.md)只能展示,无法满足以下真实业务诉求:
- 关键词管理:用户为内容打上多个标签,并可随时删除不再需要的标签;
- 筛选条件集合:动态添加多个筛选标签,删掉某个条件后条件列表随之收缩;
- 增量追加:通过输入或按钮持续追加新标签,且每个新标签都可独立关闭。
解决这类问题的标准做法,正是原文档control.md所演示的:把标签集合放进组件的 State(数组),渲染时map成一组<Tag>,删除时从数组中filter掉对应项,追加时向数组push新项。数据是唯一事实来源(Single Source of Truth),界面只是数据的投影。
二、核心代码:用数组驱动标签的增删(原文档完整示例)
原文档 components/tag/demo/control.md 提供了完整的可运行示例,这里完整继承并补充注释说明:
import { Tag, Button } from 'antd'; let index = 2; // 全局计数器,保证新增标签的 key 不重复 const App = React.createClass({ // 初始数据:两个标签,每个标签是一个包含 key 与 name 的对象 getInitialState() { return { tags: [ { key: 1, name: '标签一' }, { key: 2, name: '标签二' }, ], }; }, // 关闭:根据 key 过滤掉被删除的标签,生成新数组并 setState handleClose(key) { const tags = [...this.state.tags].filter(tag => (tag.key !== key) && tag); console.log(tags); this.setState({ tags }); }, // 追加:复制数组,push 一个新标签对象 addTag() { const tags = [...this.state.tags]; index += 1; tags.push({ key: index, name: `新标签${index}` }); this.setState({ tags }); }, render() { return ( <div> {this.state.tags.map(tag => <Tag key={tag.key} closable afterClose={this.handleClose.bind(this, tag.key)}>{tag.name}</Tag> )} <div> <Button onClick={this.addTag}>添加标签</Button> </div> </div> ); } }); ReactDOM.render(<App />, mountNode);原文档的关键提示:afterClose而不是onClose
原文档特别强调了一句话,这也是本示例最核心的技术要点:
使用
afterClose而不是onClose,删除时有动画效果。
原因在于:onClose在用户点击关闭图标的那一刻立即触发,此时组件尚未播放退场动画;如果立刻从数组中移除该标签,React 会直接卸载节点,动画根本没有机会执行。而afterClose会等到缩带动画完整播放结束之后才触发,此时再更新数组、移除标签,界面便呈现出平滑的"收起消失"效果。
数组操作的两个易错点
- 追加时先复制再
push:addTag中先执行const tags = [...this.state.tags]创建新数组,再push新对象,最后setState({ tags })。直接push原数组会修改 State 引用,违反 React 不可变更新原则,可能导致渲染优化失效。 - 删除时用
filter生成新数组:handleClose中filter(tag => tag.key !== key)同样返回全新数组,配合key精确定位要移除的标签,避免误删。
三、源码级原理:afterClose背后的动画触发链路
要真正理解"为什么用afterClose",需要走进 Tag 组件的实现。查看 components/tag/index.jsx 源码,可以还原完整调用链:
close(e) { const dom = ReactDOM.findDOMNode(this); dom.style.width = `${dom.offsetWidth}px`; // 锁定当前宽度(执行两次,保证布局稳定) dom.style.width = `${dom.offsetWidth}px`; this.setState({ closing: true }); this.props.onClose(e); // ① 此刻立即触发 onClose } animationEnd(key, existed) { if (!existed && !this.state.closed) { this.setState({ closed: true, closing: false }); this.props.afterClose(); // ② 动画结束后才触发 afterClose } }从源码结构看,关闭流程分两阶段:
- 阶段一(点击):
close()先把 DOM 节点的width锁定为当前宽度,再将closing置为true,并立即调用this.props.onClose(e)。样式表中 tag.less 对.ant-tag-close定义了width: 0 !important; padding: 0; margin: 0;,配合rc-animate的ant-tag-zoom过渡类(antZoomOut动画,时长0.2s)完成横向收起动画。 - 阶段二(动画结束):
rc-animate的onEnd回调触发animationEnd(key, existed),当existed为false(节点即将卸载)且尚未标记关闭时,置closed: true并调用this.props.afterClose()。此时 DOM 已不可见,从数据数组移除标签才不会出现"闪烁/瞬间消失"。
因此:onClose适合做"确认/记录"等即时逻辑;afterClose才适合做"从数据源移除"这类会触发重渲染的操作——这正是原文档提示的本质。
四、组件 API 与参数语义(对照官方文档)
配合 components/tag/index.md 的 API 表,本示例涉及的关键参数如下:
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
closable | 标签是否可以关闭 | boolean | — | false |
onClose | 关闭时的回调(点击立即触发) | function | — | 无 |
afterClose | 动画关闭后的回调(动画结束触发) | function | — | 无 |
color | 标签的色彩 | string | bluegreenyellowred | 无 |
closable默认关闭,动态标签必须显式加上closable才会渲染右上角叉号。源码 tag/index.jsx 中AntTag.defaultProps确认了默认值:closable: false、onClose() {}、afterClose() {}。- 四个颜色值在样式表 style/components/tag.less 中各有对应背景(
@link-color、@success-color、@warning-color、@error-color),可在 colorful.md 看到四种颜色与closable组合的效果。若要为动态标签增加视觉区分,只需给<Tag color="blue">等传参即可。
五、实战扩展:将示例落地为生产可用的组件
原文档示例使用React.createClass与getInitialState,这是 React 早期(ES5)写法。在需要实际复用时,可以按当前 React 生态改造成函数组件 + Hooks 形式,业务逻辑与原示例完全一致:
import React, { useState, useRef } from 'react'; import { Tag, Button } from 'antd'; const TagGroup = () => { const [tags, setTags] = useState([ { key: 1, name: '标签一' }, { key: 2, name: '标签二' }, ]); const indexRef = useRef(2); // 替代全局变量 index,避免命名冲突 const handleClose = (key) => { setTags(tags => tags.filter(tag => tag.key !== key)); }; const addTag = () => { indexRef.current += 1; setTags(tags => [...tags, { key: indexRef.current, name: `新标签${indexRef.current}` }]); }; return ( <div> {tags.map(tag => <Tag key={tag.key} closable afterClose={handleClose.bind(null, tag.key)}> {tag.name} </Tag> )} <div> <Button onClick={addTag}>添加标签</Button> </div> </div> ); }; export default TagGroup;改造要点:
key必须稳定且唯一:示例用自增的index作key。若用name作 key,会出现两个同名标签冲突、以及重名时filter误删的问题;自增计数器(原示例的全局let index或 Hooks 中的useRef)能保证每次追加的key唯一。- 回调需绑定具体参数:
afterClose={this.handleClose.bind(this, tag.key)}将每个标签的key预先绑定进回调,map渲染时无需再为每个标签创建内联匿名函数包裹层,语义清晰且避免闭包捕获过期状态。 console.log(tags)可移除:原示例仅用于演示数组内容,生产代码中删除即可。
在此基础上,还可以继续扩展:将handleClose内改为调用服务端接口删除持久化标签;把"添加标签"的按钮替换为 Input 输入框 + 回车提交;或在addTag中加入去重与数量上限校验。
六、小结
围绕 components/tag/demo/control.md,本文完整继承并扩展了"数据生成标签"的实践方案:
- 用State 数组 +
map渲染实现标签集合的数据驱动; - 用
filter生成新数组删除标签、push新对象追加标签,遵循不可变更新原则; - 牢记
afterClose而非onClose,这是删除动画能否生效的关键(底层由 tag/index.jsx 的两阶段关闭链路与 tag.less 的ant-tag-zoom动画共同支撑); - 结合 Tag API 表 正确使用
closable、onClose、afterClose、color四个参数,并可将示例迁移为函数组件形态落地到真实业务。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/gh_mirrors/antde/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考