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 全绿",
|
"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 栈",
|
"notes": "自补:组件补充;复用 popover + button,不重复实现定位与 Esc 栈",
|
||||||
"status": "done"
|
"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",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T05:59:36.078Z",
|
"generatedAt": "2026-10-26T06:00:53.659Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -1754,6 +1754,17 @@
|
|||||||
"n": "onReset",
|
"n": "onReset",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
"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 }))])",
|
"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(
|
section(
|
||||||
'sec-formlab',
|
'sec-formlab',
|
||||||
'表单校验',
|
'表单校验',
|
||||||
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。',
|
'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处,onError 拿到不通字段数。',
|
||||||
form(
|
form(
|
||||||
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
||||||
[
|
[
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @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)。
|
> 组件 = 普通函数,传 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)validateOn(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
||||||
- (formItem)hint:string —— 控件下方辅助说明(格式提示等);报错时让位给错误文案
|
- (formItem)hint:string —— 控件下方辅助说明(格式提示等);报错时让位给错误文案
|
||||||
- onReset:() => void —— 重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮
|
- 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 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 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 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; // 提交中标记:异步校验期间屏蔽重复提交
|
let running = false; // 提交中标记:异步校验期间屏蔽重复提交
|
||||||
|
|
||||||
/*** 整体提交:逐项 await 校验(全部跑完以显示所有错误),有错聚焦首个并中止 ***/
|
/*** 整体提交:逐项 await 校验(全部跑完以显示所有错误),有错聚焦首个并中止 ***/
|
||||||
@@ -106,19 +108,26 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
|||||||
running = true;
|
running = true;
|
||||||
submitBtn.disabled = true; // 校验期间禁用,避免连点重复提交
|
submitBtn.disabled = true; // 校验期间禁用,避免连点重复提交
|
||||||
let firstBad: HTMLElement | null = null;
|
let firstBad: HTMLElement | null = null;
|
||||||
|
let bad = 0; // 不通过的字段数
|
||||||
for (const item of items) {
|
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;
|
firstBad = item.el.querySelector('input, textarea, select') as HTMLElement | null;
|
||||||
}
|
}
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
running = 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?.();
|
props.onSubmit?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = h(
|
const el = h(
|
||||||
'form',
|
'form',
|
||||||
{ class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' },
|
{ class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' },
|
||||||
|
props.errorSummary === false ? null : summary, // 汇总条:关掉时连节点都不插
|
||||||
...items.map((i) => i.el),
|
...items.map((i) => i.el),
|
||||||
actions,
|
actions,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -393,6 +393,8 @@ export interface FormProps {
|
|||||||
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
|
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
|
||||||
onSubmit?: () => void; // 全部字段校验通过后的提交回调
|
onSubmit?: () => void; // 全部字段校验通过后的提交回调
|
||||||
onReset?: () => void; // 重置回调
|
onReset?: () => void; // 重置回调
|
||||||
|
onError?: (count: number) => void; // 校验不通过的回调,参数为不通字段数(适合埋点 / 顶部提示)
|
||||||
|
errorSummary?: boolean; // 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 时间线条目 ***/
|
/*** 时间线条目 ***/
|
||||||
|
|||||||
@@ -1625,6 +1625,17 @@
|
|||||||
"n": "onReset",
|
"n": "onReset",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
"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 }))])",
|
"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;
|
justify-content: flex-end;
|
||||||
gap: var(--ua-space-2);
|
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