iter(g108): 表单补错误汇总条与 onError
This commit is contained in:
@@ -1220,6 +1220,14 @@
|
||||
"acceptance": "PopoverProps 新增受控 open 信号(不传行为不变);新增 popconfirm(props, trigger):title/description/confirmText/cancelText/variant/placement/onConfirm/onCancel/open,确定先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」;组件 58→59,manifest/docs/README/AGENTS/ARCHITECTURE/demo 同步;npm run check 全绿",
|
||||
"notes": "自补:组件补充;复用 popover + button,不重复实现定位与 Esc 栈",
|
||||
"status": "done"
|
||||
},
|
||||
{
|
||||
"id": "g108",
|
||||
"title": "表单提交失败给汇总条与回调",
|
||||
"motivation": "提交失败时只把焦点扔到第一处错误:视觉上用户得自己数还有几处红框,读屏用户更是只听到某个输入框的 aria-invalid,完全不知道总共有几项要改;调用方也拿不到「这次失败了几项」做埋点或顶部提示",
|
||||
"acceptance": "form 提交时统计不通字段数,顶部渲染 role=alert 的汇总条「有 N 项需要修改,已定位到第一处」(通过则隐藏);FormProps 新增 onError(count) 与 errorSummary(缺省 true,传 false 不插节点);文档参数表与演示说明同步;npm run check 全绿",
|
||||
"notes": "自补:无障碍与可用性;汇总条与逐项红框同源,均由一次提交的校验结果推导",
|
||||
"status": "open"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "ui-arch-registry-index/0.1",
|
||||
"generatedAt": "2026-10-26T05:59:36.078Z",
|
||||
"generatedAt": "2026-10-26T06:00:53.659Z",
|
||||
"repoRoot": ".",
|
||||
"packages": [
|
||||
{
|
||||
@@ -1754,6 +1754,17 @@
|
||||
"n": "onReset",
|
||||
"t": "() => void",
|
||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
||||
},
|
||||
{
|
||||
"n": "onError",
|
||||
"t": "(count: number) => void",
|
||||
"k": 1,
|
||||
"d": "校验不通过时的回调,参数为不通字段数(适合埋点或顶部提示)"
|
||||
},
|
||||
{
|
||||
"n": "errorSummary",
|
||||
"t": "boolean",
|
||||
"d": "是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true,自带错误 UI 时传 false"
|
||||
}
|
||||
],
|
||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||
|
||||
+1
-1
@@ -437,7 +437,7 @@ const content = h(
|
||||
section(
|
||||
'sec-formlab',
|
||||
'表单校验',
|
||||
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。',
|
||||
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处,onError 拿到不通字段数。',
|
||||
form(
|
||||
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
||||
[
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||
|
||||
> 自动生成于 2026-10-26T05:59:36.078Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 自动生成于 2026-10-26T06:00:53.659Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||
|
||||
## 快速上手
|
||||
@@ -913,6 +913,8 @@ const open = signal(false); drawer({ title: '详情', open, width: 360, onClose:
|
||||
- (formItem)validateOn(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
||||
- (formItem)hint:string —— 控件下方辅助说明(格式提示等);报错时让位给错误文案
|
||||
- onReset:() => void —— 重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮
|
||||
- onError(回调):(count: number) => void —— 校验不通过时的回调,参数为不通字段数(适合埋点或顶部提示)
|
||||
- errorSummary:boolean —— 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true,自带错误 UI 时传 false
|
||||
|
||||
示例:
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
||||
const submitBtn = button({ label: props.submitText ?? '提交', type: 'submit' }) as HTMLButtonElement; // 走原生 submit,避免 click + submit 双触发
|
||||
const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', type: 'button', onClick: props.onReset }) : null; // 重置不得提交表单
|
||||
const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn);
|
||||
const summary = h('div', { class: 'ua-form-summary', role: 'alert' }) as HTMLElement; // 错误汇总:读屏能一次听到共几处要改
|
||||
summary.hidden = true;
|
||||
let running = false; // 提交中标记:异步校验期间屏蔽重复提交
|
||||
|
||||
/*** 整体提交:逐项 await 校验(全部跑完以显示所有错误),有错聚焦首个并中止 ***/
|
||||
@@ -106,19 +108,26 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
||||
running = true;
|
||||
submitBtn.disabled = true; // 校验期间禁用,避免连点重复提交
|
||||
let firstBad: HTMLElement | null = null;
|
||||
let bad = 0; // 不通过的字段数
|
||||
for (const item of items) {
|
||||
if (!(await item.validateAsync()) || firstBad) continue; // 首错之前的项才记焦点目标
|
||||
const msg = await item.validateAsync();
|
||||
if (!msg) continue; // 卫语句:本项通过
|
||||
bad += 1;
|
||||
if (firstBad) continue; // 只把首个错误控件留作焦点目标
|
||||
firstBad = item.el.querySelector('input, textarea, select') as HTMLElement | null;
|
||||
}
|
||||
submitBtn.disabled = false;
|
||||
running = false;
|
||||
if (firstBad) { firstBad.focus(); return; } // 卫语句:有错误不提交
|
||||
summary.hidden = bad === 0;
|
||||
summary.textContent = bad ? `有 ${bad} 项需要修改,已定位到第一处` : '';
|
||||
if (firstBad) { firstBad.focus(); props.onError?.(bad); return; } // 卫语句:有错误不提交
|
||||
props.onSubmit?.();
|
||||
};
|
||||
|
||||
const el = h(
|
||||
'form',
|
||||
{ class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' },
|
||||
props.errorSummary === false ? null : summary, // 汇总条:关掉时连节点都不插
|
||||
...items.map((i) => i.el),
|
||||
actions,
|
||||
);
|
||||
|
||||
@@ -393,6 +393,8 @@ export interface FormProps {
|
||||
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
|
||||
onSubmit?: () => void; // 全部字段校验通过后的提交回调
|
||||
onReset?: () => void; // 重置回调
|
||||
onError?: (count: number) => void; // 校验不通过的回调,参数为不通字段数(适合埋点 / 顶部提示)
|
||||
errorSummary?: boolean; // 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true
|
||||
}
|
||||
|
||||
/*** 时间线条目 ***/
|
||||
|
||||
@@ -1625,6 +1625,17 @@
|
||||
"n": "onReset",
|
||||
"t": "() => void",
|
||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
||||
},
|
||||
{
|
||||
"n": "onError",
|
||||
"t": "(count: number) => void",
|
||||
"k": 1,
|
||||
"d": "校验不通过时的回调,参数为不通字段数(适合埋点或顶部提示)"
|
||||
},
|
||||
{
|
||||
"n": "errorSummary",
|
||||
"t": "boolean",
|
||||
"d": "是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true,自带错误 UI 时传 false"
|
||||
}
|
||||
],
|
||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||
|
||||
@@ -3738,3 +3738,16 @@ button.ua-step-dot:hover {
|
||||
justify-content: flex-end;
|
||||
gap: var(--ua-space-2);
|
||||
}
|
||||
|
||||
/* —— 表单错误汇总条 —— */
|
||||
.ua-form-summary {
|
||||
padding: var(--ua-space-2) var(--ua-space-3);
|
||||
border: 1px solid color-mix(in srgb, var(--ua-color-danger) 45%, transparent);
|
||||
border-radius: var(--ua-radius-sm);
|
||||
background: color-mix(in srgb, var(--ua-color-danger) 10%, transparent);
|
||||
color: var(--ua-color-danger);
|
||||
font-size: var(--ua-font-size-md);
|
||||
}
|
||||
.ua-form-summary[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user