ToolJet List View 组件完全指南:数据列表、分页与子组件控制
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
List View 是 ToolJet 应用构建器中用于批量渲染重复行数据的核心容器组件:你只需设计一行(包含任意嵌套组件),组件就会根据List data自动复制出多行实例。本篇指南基于当前仓库 listview.md 展开,结合 listview.js 组件配置与 Listview.jsx 运行时代码,完整讲解 List data 绑定、全部属性/事件/暴露变量、分页、样式定制,以及通过children变量用 JS 查询控制子组件,读完即可独立构建"接口数据 → 列表展示 → 行交互"的完整场景。
一、组件定位与核心能力
从组件配置源码 listview.js 可以看出,List View(内部组件名为Listview)本质上是一个带模板子组件的容器:
- 拖入画布时自带 3 个默认子组件:
Image、Text、Button,分别通过accessorKey(imageURL/text/buttonText)与数据对象的字段自动关联; - 默认画布尺寸为宽 15 格、高 450px;
- 你只编辑第一行模板,组件会按
List data的数组长度自动生成后续所有行实例。
与 Container 类似,List View 内部可嵌套任意组件,也支持 List View 嵌套 List View(多级列表)。唯一的限制是:Calendar 和 Kanban 组件被禁止通过拖拽放入 List View 内部。
:::caution 受限组件Calendar与Kanban组件不允许拖入 List View 中使用,请改用其他容器承载它们。 :::
二、设置 List Data:listItem数据绑定
List View 的List data属性接受对象数组或返回对象数组的查询结果。在 List View 内部,每一条数据会通过约定变量listItem暴露给行模板中的子组件。
官方文档示例数据:
{{[ { imageURL: 'https://www.svgrepo.com/show/34217/image.svg', text: 'Sample text 1', buttonText: 'Button 1' }, { imageURL: 'https://www.svgrepo.com/show/34217/image.svg', text: 'Sample text 1', buttonText: 'Button 2' }, { imageURL: 'https://www.svgrepo.com/show/34217/image.svg', text: 'Sample text 1', buttonText: 'Button 3' }, ]}}行内组件的绑定方式:
// Text 组件的 Data 属性 {{listItem.text}} // Image 组件的 source 属性 {{listItem.imageURL}}从源码看,listItem的注入发生在 Listview.jsx:组件会对过滤后的数据逐条执行filteredData.map((listItem) => ({ listItem })),并通过updateCustomResolvables(id, listItems, 'listItem', ...)注册为自定义可解析变量。这意味着listItem是行作用域的——第 0 行的{{listItem.text}}取数组第 0 个元素,第 1 行取第 1 个元素,以此类推。
数据来源的三种方式
| 方式 | 写法 | 说明 |
|---|---|---|
| 字面量数组 | {{[{...}, {...}]}} | 静态数据,适合原型设计 |
| 查询结果 | {{queries.restapi1.data.data}} | 最常见的动态数据源 |
| JS 表达式 | {{queries.users.data.map(u => ({...u, fullName: u.first + ' ' + u.last}))}} | 对查询结果做二次加工 |
组件配置 listview.js 对data的校验 schema 是「对象数组」或「字符串数组」的联合类型,默认值为[{text: 'Sample text 1'}]——即使留空,组件也有一个可渲染的模板数据。
三、属性详解(Properties)
List View 的全部属性在属性面板中可配置,下表整理了官方属性与源码中的默认值(见 listview.js):
| 属性 | 说明 | 期望值 | 源码默认值 |
|---|---|---|---|
| List data | 要展示的数据,对象数组或返回对象数组的查询 | 对象数组 / 查询 | [{text: 'Sample text 1'}] |
| Mode | 布局模式,List(单列列表)或Grid(多列网格) | list/grid | list |
| Show bottom border | 是否显示每行底部分隔线,仅List模式可用 | true/false | true |
| Columns | Grid 模式下的列数,仅Grid模式可用 | 任意数值 | 3 |
| Row height | 每行高度(像素) | 1–100 之间的数值 | 100 |
| Enable pagination | 是否启用分页 | true/false | false |
| Rows per page | 每页行数,仅启用分页后可用 | 任意数值 | 10 |
分页的底层实现
从 Listview.jsx 源码可以看到分页是前端切片实现的:
const startIndexOfRowInThePage = currentPage === 1 ? 0 : currentPage * rowPerPageValue - rowPerPageValue; const endIndexOfRowInThePage = startIndexOfRowInThePage + rowPerPageValue; const filteredData = _.isArray(data) ? enablePagination ? data.slice(startIndexOfRowInThePage, endIndexOfRowInThePage) : data : [];即:enablePagination为true时,组件按rowsPerPage对数据做slice分页;rowsPerPage会被强制转成数值(Number(rowsPerPage) ? +rowsPerPage || 10 : 10),非法输入回退到 10。启用分页后,组件底部会渲染一个 Pagination 控件,且组件自身高度会预留约 54px 给分页栏。
Grid 模式与行高
- Grid 模式下,每行子容器宽度为
100 / columns百分比(见 ListviewSubcontainer.jsx),列数小于 1 时会被强制修正为 1(setPositiveColumns); - Row height直接作用于每行子容器的高度(
height: rowHeight px),超出部分overflow: hidden; - List 模式下每行宽度为 100%,
showBorder为true时在行底绘制1px solid var(--cc-default-border)分隔线。
附加行为属性
除上述属性外,源码中还定义了以下行为开关(位于 Additional Actions 区):
- Loading state:
true时组件显示 Spinner 加载动画,等待数据就绪(Listview.jsx); - Dynamic height:
true时行高随内容自适应(仅在运行态生效),并会清理行模板的临时布局缓存; - Visibility:控制组件可见性;
- Collapse when hidden:隐藏时是否折叠占位;
- Disable:锁定组件,禁用交互(通过
data-disabled+inert同时阻断鼠标事件与键盘 Tab 焦点); - Tooltip:悬停提示(支持 Plain text / Markdown / HTML 三种格式,见 listview.js)。
四、事件(Events)
为 List View 添加事件处理器:点击组件句柄打开右侧属性面板 → 进入Events区 → 点击+Add handler。List View 提供两个事件:
Row clicked(已弃用)
任意一行被点击时触发,可定义多个动作。触发后通过selectedRowId、selectedRow两个变量暴露被点击行的信息(详见下方暴露变量)。
:::warning 弃用提醒Row clicked事件已标记为 deprecated,官方推荐改用Record clicked事件。 :::
Record clicked(推荐)
与 Row clicked 行为一致,点击行内任意记录时触发,并通过selectedRecordId、selectedRecord暴露数据。
从源码 Listview.jsx 看,点击处理统一由onRecordOrRowClicked(index)完成:它同时设置selectedRecordId/selectedRecord/selectedRowId/selectedRow四组暴露变量,并依次触发fireEvent('onRecordClicked')与fireEvent('onRowClicked')。事件定义见 listview.js。
值得注意的是,点击发生在捕获阶段(onClickCapture,见 ListviewSubcontainer.jsx),因此即便行内子组件自身有点击处理器,List View 的行点击事件也会先行触发。当选中行内的子组件数据后续更新时,listViewComponentSlice.js 还会自动同步刷新selectedRecord/selectedRow的快照,避免出现"点击后数据滞后一行"的问题。
关于可用动作(Actions)的完整说明,请查阅文档站点的 Action Reference 分类文档。
Component Specific Actions(CSA)
目前 List View尚未实现组件级专属动作(CSA),无法通过 JS 查询直接调用如components.listview1.reset()之类的控制方法——对子组件的控制需通过下文第七节的children变量实现。
五、暴露变量(Exposed Variables)
List View 暴露给全局 JS 作用域的变量如下(组件实例名假设为listview1):
| 变量 | 说明 | 访问方式 |
|---|---|---|
| data | 存储加载到组件中的数据(按行索引组织) | {{components.listview1.data["0"].text1.text}} |
| selectedRowId(已弃用) | 被点击行的 ID,从0开始 | {{components.listview1.selectedRowId}} |
| selectedRow(已弃用) | 被点击行内各组件的数据 | {{components.listview1.selectedRow.text1}} |
| selectedRecordId | 被点击记录的 ID,从0开始 | {{components.listview1.selectedRecordId}} |
| selectedRecord | 被点击记录内各组件的数据 | {{components.listview1.selectedRecord.text1}} |
| children | 所有记录内子组件的数据数组 | 用于通过 JS 控制子组件(见第七节) |
data 与 children 的生成原理
这两个变量的内容并非手动维护,而是由 store 层 listViewComponentSlice.js 的deriveListviewExposedData自动派生:
- 遍历 List View 的直接子组件,把每个子组件在当前行的暴露值收集为
rowData(对象键为子组件名称,值为{...暴露值, id: 子组件uuid}); - 写入结构:
components.listview1.children[rowIndex]与components.listview1.data[rowIndex],其中data是经过deepClone并剥离函数后的纯数据副本——这也是data变量适合被其他组件/查询读取的原因; - 每次派生后同步触发依赖更新(
updateDependencyValues),保证引用了components.listview1.data的表达式实时重算。
该实现同样支持嵌套 List View:子列表的暴露值通过outerIndices沿外层行索引定位(如components.listview1[0].children表示第 0 行内的嵌套列表),并能正确处理多层嵌套链(_deriveListviewChain)。
行级作用域(Row Scope)机制
listItem绑定与{{components.xxx.value}}行内引用之所以能各取所需,得益于 listViewComponentSlice.js 中的prepareRowScope/updateRowScope:
- 行内子组件(如复选框)的暴露值以按行数组存储(
components['checkbox-uuid'] = [{value:false},{value:true},...]); - 解析表达式时,引擎用
Object.create(components)创建一个以全局组件表为原型的作用域覆盖对象,仅对 List View 的后代组件覆盖为"当前行那一项"(scoped[childId] = val[rowIndex]); - 于是
{{components.checkbox1.value}}在行 2 内解析到的是{value: true}而不是整个数组。这也解释了为什么同一 List View 内每行的表达式互不干扰。
六、通用属性与样式
Tooltip
在General折叠区设置 Tooltip 的字符串内容,鼠标悬停时即显示提示。源码中该配置支持Plain text/Markdown/HTML三种渲染格式(tooltipFormat),默认Plain text。
Devices(响应式)
| 属性 | 说明 | 期望值 |
|---|---|---|
| Show on desktop | 桌面端是否显示组件 | 开关按钮,或用fx配置逻辑表达式 |
| Show on mobile | 移动端是否显示组件 | 开关按钮,或用fx配置逻辑表达式 |
Styles(样式)
| 样式 | 说明 |
|---|---|
| Background color | 背景色,支持 Hex 色值或取色器;源码默认var(--cc-surface1-surface)(跟随主题的表面色) |
| Border color | 边框颜色,默认var(--cc-weak-border) |
| Visibility | 可见性,仅接受布尔值{{true}}/{{false}},默认{{true}} |
| Disable | 禁用组件,仅接受布尔值,默认{{false}} |
| Border radius | 圆角,仅接受 1–100 的数值,源码默认6 |
此外源码还定义了Box shadow(默认0px 0px 0px 0px #00000040)样式项。
:::info 任何带fx按钮的属性都支持编程式配置——点击 fx 后输入{{表达式}},即可在运行态动态取值。 :::
七、控制子组件(Controlling Child Components)
所有行内子组件都通过children变量暴露,它是一个按记录索引的数组,每个元素对应一条记录内各子组件的数据。
你可以用 JS 查询(JavaScript Query)控制行内子组件,例如禁用第一条记录里的button1:
components.listview1.children[0].button1.disable(true) // 禁用第 1 条记录中的 button1:::caution 适用前提 只有实现了组件专属动作(CSA)的子组件才能通过 JS 查询控制。判断某组件是否支持 CSA,请查阅对应组件的文档(组件总览)。 :::
对于支持 CSA 的组件(如按钮的disable/enable/setVisibility等),这一机制让你可以按记录粒度批量操作行内组件——这是 List View 相比普通容器最强大的动态能力之一。
八、完整实操:从 REST API 渲染用户列表
下面复现官方示例,演示一个完整的 List View 数据流。
第 1 步:新建应用并拖入 List View
新建应用,从左侧组件面板把 List View 拖到画布上,此时它处于空列表状态,自带 Image/Text/Button 模板行:
第 2 步:创建 REST API 查询
新建查询,数据源选择REST API,方法选GET,端点填https://reqres.in/api/users?page=1。保存并运行查询,在左侧查看结果,可以看到返回的data对象里是一个对象数组(每项含avatar、first_name、email等字段):
第 3 步:绑定 List data
编辑 List View 的List data属性,用 JS 从查询取数:
{{queries.restapi1.data.data}}这里第一个data是restapi1查询的返回结果,第二个data才是结果中真正承载对象数组的字段。绑定后组件会按数组长度自动生成对应行数的实例:
第 4 步:设计行模板
把组件(文本、图片、按钮等)嵌套进第一行,后续行会自动按第一行的样式复制。行内组件通过listItem取当前行数据:
{{listItem.avatar}} // 示例:展示头像图片 {{listItem.first_name}} // 示例:展示姓名后续每行会自动套用第一行的布局与数据绑定:
:::tip 在嵌套组件上使用{{listItem.key}}展示数据,其中key是查询结果对象中的字段名。例如示例中用{{listItem.avatar}}显示头像,因为avatar是接口返回对象中的键。 :::
九、源码级注意事项与最佳实践
- 数据形态校验:
List data的 schema 要求数组元素为对象或字符串(listview.js)。如果查询返回的不是数组(例如{ data: [...] }结构),请像示例一样补一层.data取到数组本身; - 行数变化自动同步:当过滤后的数据行数变化时,Listview.jsx 会通过
initExposedValueArrayForChildren为所有子组件初始化/裁剪对应行数的暴露值槽位,并清理children/data中的过期行,因此不需要手动管理行数; - 大列表性能:行内组件解析采用行级作用域 + 惰性行索引机制(
isLazyResolvableParent/getLazyRowIndices,见 componentsSlice.js),Table 的可展开行等场景只解析模板行与当前需要的行,其余按需解析;对普通大列表建议配合分页使用; - 嵌套场景:List View 支持在行内再嵌套 List View,
listItem与暴露变量会按外层索引正确隔离,多级表达式如{{listItem.orders}}配合内层listItem即可实现主子表结构; - 事件选择:新应用请直接使用Record clicked,
Row clicked仅为向后兼容保留(事件定义中明确标注(Deprecated)); - 组件约束:Calendar / Kanban 不可拖入 List View;行内组件的 CSA 控制能力取决于该组件自身的实现。
十、相关文档与源码索引
- 组件官方文档:listview.md(另有 version-3.0.0-LTS 版本 可对照差异)
- 组件配置与默认值:listview.js
- 组件运行时实现:Listview.jsx、ListviewSubcontainer.jsx、listview.scss
- 暴露变量派生与行级作用域:listViewComponentSlice.js
listItem解析与惰性行解析:componentsSlice.js- 相关测试可参考仓库
frontend与cypress-tests目录下针对 List View 的用例,验证分页、事件与数据绑定行为。
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考