Cascader
Cascader 是 antd-mobile 分栏级联选择器的 Formily 适配,字段值为从父级到子级的选项值数组。点击字段按钮或 FormItem 整行打开弹层,确认后自动写回字段;切换选项和取消不会提交草稿。滚轮式级联选择请使用 CascadePicker。
基础使用
import { Cascader, FormButtonGroup, FormItem, Reset, Submit } from '@silver-formily/antd-mobile'
import { createForm } from '@silver-formily/core'
import { Field, FormProvider } from '@silver-formily/react'
import { useMemo } from 'react'
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function Demo() {
const form = useMemo(() => createForm({ initialValues: { region: ['zhejiang', 'hangzhou'] } }), [])
return (
<FormProvider form={form}>
<Field
name="region"
title="所在地区"
decorator={[FormItem]}
dataSource={[{ label: '浙江省', value: 'zhejiang', children: [{ label: '杭州市', value: 'hangzhou' }, { label: '宁波市', value: 'ningbo' }] }, { label: '江苏省', value: 'jiangsu', children: [{ label: '南京市', value: 'nanjing' }] }]}
component={[Cascader, { title: '选择省市', placeholder: '请选择地区' }]}
/>
<FormButtonGroup>
<Submit onSubmit={onSubmit}>查看结果</Submit>
<Reset>重置</Reset>
</FormButtonGroup>
</FormProvider>
)
}字段 dataSource 中的选项使用 { label, value, disabled?, children? } 结构。叶子节点应省略 children,不要设置 children: [],以便原生组件识别最后一级。
异步层级加载
参考 antd-mobile 官方异步加载示例,在 onSelect 中按已选路径请求子级,并更新字段 dataSource。示例用 800ms 延迟模拟接口,提供三级选项;已加载的节点会缓存,重复选择不会再次请求。
缓存区分三种状态:未加载时使用原生 Cascader.optionSkeleton 显示下一层骨架;加载后提供子选项数组;确认是叶子节点时记录为 null,生成选项树时省略其 children。骨架数组应直接使用原生导出的实例,不要复制,否则原生组件无法识别加载状态。示例中原生组件以 AntCascader 命名,避免与 Formily 封装混淆。
import type { CascaderOption } from 'antd-mobile'
import { Cascader, FormButtonGroup, FormItem, Submit } from '@silver-formily/antd-mobile'
import { createForm } from '@silver-formily/core'
import { Field, FormProvider } from '@silver-formily/react'
import { Cascader as AntCascader } from 'antd-mobile'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
// undefined: not loaded; null: a leaf node; array: cached children.
type OptionsCache = Record<string, CascaderOption[] | null>
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function Demo() {
const form = useMemo(() => createForm({ initialValues: { region: [] } }), [])
const [cache, setCache] = useState<OptionsCache>({})
const requested = useRef(new Set<string>())
const timers = useRef(new Set<ReturnType<typeof setTimeout>>())
const fetchOptions = useCallback((value: string, level: number) => {
// The request boundary also enforces the maximum depth. Field callbacks
// must not depend on a React render's snapshot of the cache.
if (level >= 3)
return
if (requested.current.has(value))
return
requested.current.add(value)
// Simulate an API request. Each node's value is its unique full path.
const timer = setTimeout(() => {
timers.current.delete(timer)
const children = Array.from({ length: 3 }, (_, index) => ({
value: value ? `${value}-${index + 1}` : `${index + 1}`,
label: value ? `${value}-${index + 1}` : `选项 ${index + 1}`,
}))
setCache(previous => ({
...previous,
[value]: children,
// This example has three levels. Mark the last level as leaves.
...(level === 2
? Object.fromEntries(children.map(child => [child.value, null]))
: {}),
}))
}, 800)
timers.current.add(timer)
}, [])
useEffect(() => {
fetchOptions('', 0)
const activeTimers = timers.current
const activeRequests = requested.current
return () => {
activeTimers.forEach(clearTimeout)
activeTimers.clear()
activeRequests.clear()
}
}, [fetchOptions])
const options = useMemo(() => {
function generate(value: string): CascaderOption[] | undefined {
const children = cache[value]
if (children === null)
return undefined
if (children === undefined)
return AntCascader.optionSkeleton
return children.map(child => ({ ...child, children: generate(child.value) }))
}
return generate('') ?? []
}, [cache])
useEffect(() => {
form.setFieldState('region', { dataSource: options })
}, [form, options])
return (
<FormProvider form={form}>
<Field
name="region"
title="异步级联"
decorator={[FormItem]}
dataSource={options}
component={[Cascader, {
title: '逐级加载选项',
onSelect: (value) => {
value.forEach((item, index) => {
if (typeof item === 'string')
fetchOptions(item, index + 1)
})
},
}]}
/>
<FormButtonGroup>
<Submit onSubmit={onSubmit}>查看结果</Submit>
</FormButtonGroup>
</FormProvider>
)
}选择选项只更新弹层草稿并触发加载,确认后才提交字段值。取消不会写回字段,已获取的选项仍保留在缓存中。实际接口返回空子级时也应将该节点标记为叶子;请求失败时应提供错误提示及重试处理。
禁用、只读与阅读态
通过 Field 模型设置字段状态:
disabled:入口禁用,无法打开弹层或写回值。readOnly:保留入口,可以打开查看完整选项并切换层级 Tab;所有选项显示为禁用,禁止更改选择和确认写回。readPretty:仅展示路径标签,不渲染触发按钮或选择弹层。
编辑态与阅读态默认以 / 连接路径标签;displayRender(labels, selectedOptions) 可统一自定义两种状态的展示。编辑态空值使用 placeholder,默认为“请选择”;阅读态空值使用 PreviewText.Placeholder。
import { Cascader, FormButtonGroup, FormItem, Submit } from '@silver-formily/antd-mobile'
import { createForm } from '@silver-formily/core'
import { Field, FormProvider } from '@silver-formily/react'
import { useMemo } from 'react'
const dataSource = [{ label: '浙江省', value: 'zhejiang', children: [{ label: '杭州市', value: 'hangzhou' }, { label: '宁波市', value: 'ningbo' }] }, { label: '江苏省', value: 'jiangsu', children: [{ label: '南京市', value: 'nanjing' }] }]
function onSubmit(values: unknown) {
return Prompts.alert(JSON.stringify(values, null, 2))
}
export default function Demo() {
const form = useMemo(() => createForm({ initialValues: {
disabled: ['zhejiang', 'hangzhou'],
readOnly: ['zhejiang', 'ningbo'],
preview: ['jiangsu', 'nanjing'],
} }), [])
return (
<FormProvider form={form}>
<Field name="disabled" title="禁用" disabled dataSource={dataSource} decorator={[FormItem]} component={[Cascader]} />
<Field name="readOnly" title="只读" readOnly dataSource={dataSource} decorator={[FormItem]} component={[Cascader]} />
<Field name="preview" title="阅读态" readPretty dataSource={dataSource} decorator={[FormItem]} component={[Cascader]} />
<Field name="empty" title="未选择" dataSource={dataSource} decorator={[FormItem, { clearable: true }]} component={[Cascader, { placeholder: '请选择地区' }]} />
<FormButtonGroup>
<Submit onSubmit={onSubmit}>查看结果</Submit>
</FormButtonGroup>
</FormProvider>
)
}API
导出 Cascader 和 ICascaderProps。当前值由 Formily 管理,弹层可见性由组件内部管理;不提供非受控 defaultValue、外部 visible 或原生 children 渲染函数。
| 属性 | 说明 |
|---|---|
placeholder | string,入口空值占位及弹层待选择提示,默认入口显示“请选择” |
displayRender | (labels: ReactNode[], selectedOptions: (CascaderOption | null)[]) => ReactNode,自定义路径展示 |
aria-label / aria-labelledby | 触发按钮的无障碍名称;aria-label 默认使用 Field 的字符串标题 |
onConfirm | (value, extend) => void,字段写回后通知;保留原生 items、isLeaf 附加信息 |
其余配置参考 antd-mobile Cascader 官方文档。
行为说明
- 默认逐级匹配路径标签,未找到的选项回退显示原值。空数组显示占位,数值
0不视为空。 - 字段值及数据源变化会更新展示,打开的弹层也会接收最新选项。确认提交原生选择草稿,取消保留字段值;再次打开使用最新字段值。
- 关闭动画结束前不会重复打开,关闭动画结束后焦点回到入口按钮。
- FormItem 通过唯一的
data-form-item-trigger按钮转发整行点击。FormItem 的onClick可以调用event.preventDefault()阻止整行打开;直接点击 Cascader 按钮独立打开,不触发 FormItem 的点击回调。 - 没有 FormItem 时,Cascader 自身的按钮仍可打开选择器。