API

CascadingInput

Prop 类型 默认值 说明
columns ColumnConfig[] 列配置,必填
value TreeNode[] [] 受控数据
onChange (value: TreeNode[]) => void 数据变更回调
line LineConfig {} 连线配置
effects Effects 副作用/联动注册函数(Formily 风格),声明各列依赖与派生状态

列较多、内容超出容器宽度时会自动出现横向滚动条,无需额外配置。

ColumnConfig

字段 类型 必填 说明
title ReactNode 列标题,支持字符串、带图标的 JSX 等任意节点
dataIndex string 对应 TreeNode 上的字段名。纯操作列(如"操作"列)可不传
width number 列宽(px)
hasAdd boolean 是否在该列显示"添加同级"按钮
render (props: CellRenderProps) => ReactNode 自定义渲染函数,必填
addRender (props: ActionRenderProps) => ReactNode 自定义"添加"按钮渲染,位置固定在单元格下方。不传则使用默认样式

CellRenderProps

render 函数接收的参数:

字段 类型 说明
value string 当前单元格的值
onChange (val: string) => void 更新当前单元格值的回调(已绑定路径)
node TreeNode 当前树节点的完整数据,可访问 node.children
path string[] 从根到当前节点的完整路径(节点 id 数组)
parent TreeNode | null 直接父节点,根层为 null,可读取父级选中值
ancestors TreeNode[] 从根到父节点的祖先链(不含当前节点)
level number 当前层级索引(0 开始),常用于区分不同层级的样式
dataIndex string | undefined 当前列的 dataIndex,同一 render 函数服务多列时可区分字段(纯操作列为 undefined
onAdd () => void 添加同级节点(已绑定路径和层级)
onDelete () => void 删除当前行(在"操作"列的 render 里调用)
isLeaf boolean 是否为最后一列(叶子层级),常用于给末列加特殊样式
width number 当前列宽(px),可用于计算内部元素尺寸
field FieldState effects 计算出的派生状态(options / disabled / loading 等),无 effects 时为空对象

ActionRenderProps

addRender 函数接收的参数:

字段 类型 说明
onClick () => void 点击回调,触发添加或删除操作

TreeNode

interface TreeNode {
  id: string;
  children?: TreeNode[];
  [dataIndex: string]: any;
}

LineConfig

字段 类型 默认值 说明
style 'curve' | 'straight' 'curve' 连线风格
color string '#d9d9d9' 连线颜色
width number 1.5 连线粗细(px)
showSource boolean | SourceAnimationOptions false 溯源动画配置

LineStyle

type LineStyle = 'straight' | 'curve';

SourceAnimationOptions

溯源动画是一颗水滴沿连线从子节点流向父节点(圆头 + 收成尖的拖尾)。line.showSource 设为 true 用默认配置,设为对象时可自定义:

字段 类型 默认值 说明
color string 跟随 line.color 水滴颜色。默认连线色偏浅,建议单独指定一个较深/高饱和度的颜色以便可见
size number 6 水滴头部大小(px)
tailLength number 28 拖尾长度(px),按像素计算,短连线上也保持一致大小
speed number 0.004 流动速度
breatheAmplitude number 1 亮度呼吸幅度 0~1
breatheCycle number 400 呼吸周期(ms)

Effects(联动 / 副作用)

<CascadingInput effects={...}> 上声明跨层依赖与派生状态,类似 Formily 的 effects。用 $.onValueChange(target, deps, handler) 声明「target 列依赖 deps 列」,target / deps 均为 dataIndexdeps 沿目标节点祖先链解析,因此可跨任意层级(依赖必须是更上层的列)。

type Effects = (register: EffectRegistrar, ctx: EffectsSetupContext) => void;

handler 可 return { options, disabled, loading } 作为派生状态简写,或在内部用 ctx.setState({...}) 异步写入;render 通过 CellRenderProps.field 读取。派生状态以节点 id 为 key 独立存储。

WARNING

field 是 patch 合并,不是替换 只要某条路径置起了 loading: true每一条出口都必须把它关掉(包括提前 return 的分支和 catch 分支),否则残留的 loading 会让该单元格永久停在「加载中」且被禁用。置过 disabled: true 后同理要在恢复分支显式写 disabled: false

handler 内未捕获的异常引擎会兜底:记 console.error 并自动复位该单元格的 loading,不会变成 unhandled rejection。但业务上的失败降级仍应自己 try/catch

import type { Effects } from 'react-cascading-input';

const effects: Effects = ($) => {
  $.onValueChange('region', ['product'], async ({ deps, setValue, setState, isActive, initial }) => {
    if (!initial) setValue(undefined); // 初始化回显时保留已有值,仅用户改动才清空
    if (!deps.product) return { options: [], disabled: true, loading: false };
    setState({ loading: true, disabled: false });
    try {
      const options = await fetchRegions(deps.product);
      if (isActive()) setState({ options, loading: false });
    } catch {
      if (isActive()) setState({ options: [], loading: false });
    }
  });
};

EffectRegistrar

字段 类型 说明
onValueChange (target: string, deps: string[], handler: (ctx: EffectContext) => FieldState | void | Promise<FieldState | undefined>) => void 声明「target 列依赖 deps 列」,任一 dep 变化时执行 handler
onValidate (target: string, rule: ValidateRule, deps?: string[]) => void 声明「target 列的校验规则」。值变化时反应式重算(首次挂载/回显不报),也可经组件 ref 的 validate() 命令式整树触发。rule 返回错误信息字符串即不通过;deps 省略只关注自身值,声明后可跨列校验。建议每列一条(多重判断写在同一 rule 内)

EffectContext

onValueChange 的 handler 接收的参数(作用于某个具体目标节点):

字段 类型 说明
value any 目标节点当前值
deps Record<string, any> 依赖字段当前值(按 dataIndex,沿祖先链解析)
node TreeNode 目标节点,其 id 即派生状态的 key
path string[] 目标节点完整路径
initial boolean 是否为该节点首次求值(详情页初始化回显时为 true),用于区分「初始化」与「用户改动上级」,避免误清空回显值
setValue (val: any) => void 设置目标节点自身值(清空传 undefined
setState (patch: FieldState) => void 合并写入派生状态,异步友好
setTreeValue (dataIndex: string, val: any) => void 设置本分支上某祖先/自身字段的值
isActive () => boolean 异步返回后判断本次回调是否仍最新(防竞态)

FieldState

字段 类型 说明
options any 供 select 等控件使用的候选项
disabled boolean 是否禁用该单元格
loading boolean 是否处于异步加载中
error string 校验错误信息,由 effects 写入、render 展示;空/undefined 视为通过。与 loading 同为 patch 合并,校验通过时需显式写 error: undefined 清除
[key] any 允许挂载任意自定义派生字段

校验(onValidate + 命令式 validate())

校验统一用 $.onValidate(target, rule, deps?) 声明规则,一份声明同时驱动两种时机:

  • 反应式:target 自身值或任一 dep 变化时自动重算,错误写进 field.error,render 展示(红边 + 提示)。首次挂载/回显不报,等用户改动后才提示,避免刚进来满屏红。
  • 命令式:拿组件 refvalidate(),强制对所有目标节点(含未改动字段)跑一遍规则,标红并返回 { valid, errors }。通常在提交时调用,不用再手动遍历 value

rule 返回错误信息字符串表示不通过,返回空串/undefined(或不 return)表示通过。必填、格式、跨列校验都用同一套机制,也不绑定任何校验库。

import { useRef, useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import type { CascadingInputHandle, Effects, TreeNode } from 'react-cascading-input';

const effects: Effects = ($) => {
  // 必填 + 格式:规则内可写多重判断
  $.onValidate('spec', ({ value }) => {
    const s = (value ?? '').trim();
    if (!s) return '框架版本不能为空';
    if (!/\d/.test(s)) return '需包含版本号,如 PyTorch 2.1';
  });

  // 跨列校验:声明 deps,沿祖先链取依赖值判定(例如两列不能相同)
  $.onValidate('spec', ({ value, deps }) => (value && value === deps.product ? '规格不能与产品同名' : undefined), ['product']);
};

function App() {
  const [value, setValue] = useState<TreeNode[]>([]);
  const ref = useRef<CascadingInputHandle>(null);

  const handleSubmit = () => {
    const { valid, errors } = ref.current!.validate(); // 整树校验,未改动字段也标红
    if (valid) submit(value);
    else console.log(errors); // [{ path, dataIndex, message }]
  };

  return <CascadingInput ref={ref} columns={columns} value={value} onChange={setValue} effects={effects} />;
}

// render 消费 field.error:
// render: ({ value, onChange, field }) => (
//   <div>
//     <input value={value} onChange={(e) => onChange(e.target.value)} style={{ borderColor: field.error ? '#ff4d4f' : undefined }} />
//     {field.error && <div style={{ color: '#ff4d4f', fontSize: 12 }}>{field.error}</div>}
//   </div>
// )

ValidateRule / ValidateContext

type ValidateRule = (ctx: ValidateContext) => string | undefined | void;

rule 接收的 ValidateContext

字段 类型 说明
value any 目标节点当前值
deps Record<string, any> 依赖字段当前值(按 dataIndex,沿祖先链解析);仅在声明了 deps 时有内容
node TreeNode 目标节点
path string[] 目标节点完整路径(节点 id 数组)

CascadingInputHandle / ValidateResult

通过 ref 拿到组件命令式句柄:

interface CascadingInputHandle {
  /** 整树校验:跑所有 onValidate 规则(含未改动字段),标红并返回聚合结果 */
  validate: () => ValidateResult;
}

interface ValidateResult {
  valid: boolean;                 // 是否全部通过
  errors: ValidateError[];        // 未通过的字段错误(深度优先顺序)
}

interface ValidateError {
  path: string[];                 // 出错节点完整路径
  dataIndex: string;              // 出错字段
  message: string;                // 错误信息
}
同一列可注册多个 reaction

校验 reaction 与取数 reaction 可并存(引擎按 reaction 索引 + node.id 分别记录),各自 setState 会 patch 合并进同一个 field,互不覆盖。所以「拉 options」(onValueChange)和「校验」(onValidate)可以拆成两条写。