iter(g342): 重置按钮逐项复位字段与错误态
This commit is contained in:
@@ -50,6 +50,9 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
||||
// 条件显示:隐藏时顺带清掉错误态,否则「看不见的红框」会拦住提交
|
||||
if (props.visible) effect(() => { el.hidden = !props.visible?.(); if (el.hidden) paint(null); });
|
||||
let seq = 0; // 校验轮次:异步结论回来时若已有更新的一轮,丢弃旧结果(防乱序覆盖)
|
||||
// 初始值快照:重置要回到「刚渲染时」而不是空串(带默认值的字段不能被清成空)
|
||||
const initial = Array.from(control.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>('input, textarea, select'))
|
||||
.map((node) => ({ node, value: node.value, checked: (node as HTMLInputElement).checked }));
|
||||
|
||||
/*** 错误态落点:控件根 + 内部原生控件,保证 select / checkbox 这类包裹式控件也能标红 ***/
|
||||
const marked = (): HTMLElement[] => [
|
||||
@@ -98,6 +101,19 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
||||
return msg;
|
||||
};
|
||||
|
||||
/*** 复位:先跑调用方的自绘控件钩子,再把原生控件还原到初始值并同步信号,最后清错误态 ***/
|
||||
const reset = (): void => {
|
||||
props.reset?.(); // 自绘控件(无原生 input)拿不到值,只能交给调用方回写信号
|
||||
for (const { node, value, checked } of initial) {
|
||||
const boxed = node instanceof HTMLInputElement && (node.type === 'checkbox' || node.type === 'radio'); // 勾选类走 checked
|
||||
if (boxed) (node as HTMLInputElement).checked = checked;
|
||||
else node.value = value;
|
||||
node.dispatchEvent(new Event('input', { bubbles: true })); // 冒泡给组件监听器:信号跟着改,否则下次渲染又把旧值写回 DOM
|
||||
if (boxed) node.dispatchEvent(new Event('change', { bubbles: true })); // 勾选类只认 change,input 未必触发
|
||||
}
|
||||
paint(null); // 红框与错误文案一起退场
|
||||
};
|
||||
|
||||
control.addEventListener('input', () => {
|
||||
if (mode === 'input' || error.textContent) void validateAsync(); // input 模式即时校验;其余模式仅清已报错误
|
||||
});
|
||||
@@ -107,17 +123,24 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
||||
control.addEventListener('focusout', () => {
|
||||
if (mode !== 'submit') void validateAsync(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效
|
||||
});
|
||||
return { el, validate, validateAsync };
|
||||
return { el, validate, validateAsync, reset };
|
||||
}
|
||||
|
||||
/*** 表单容器:布局 + 原生 submit 拦截 + 聚合校验(全部通过才触发 onSubmit,异步规则会等待) ***/
|
||||
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;
|
||||
/*** 重置:逐项复位 + 收起汇总条,最后交给调用方回调(只跑回调等于点了没反应) ***/
|
||||
const doReset = (): void => {
|
||||
for (const item of items) item.reset();
|
||||
summary.hidden = true;
|
||||
summary.textContent = '';
|
||||
props.onReset?.();
|
||||
};
|
||||
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: doReset }) : null; // 重置不得提交表单
|
||||
const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn);
|
||||
let running = false; // 提交中标记:异步校验期间屏蔽重复提交
|
||||
|
||||
/*** 整体提交:逐项 await 校验(全部跑完以显示所有错误),有错聚焦首个并中止 ***/
|
||||
|
||||
@@ -473,6 +473,7 @@ export interface FormItemProps {
|
||||
hint?: string; // 控件下方辅助说明(格式提示等);报错时让位给错误文案
|
||||
span?: number; // 多列表单里本字段占几列(如整宽的多行文本);单列布局下无效
|
||||
visible?: Signal<boolean>; // 条件显示信号:为假时整项隐藏且不参与校验(未显示的字段不该拦住提交)
|
||||
reset?: () => void; // 自绘控件的复位钩子:无原生 input 时值由调用方回写信号(原生控件由 formItem 自己还原)
|
||||
}
|
||||
|
||||
/*** 表单字段句柄:form 聚合校验用 ***/
|
||||
@@ -480,6 +481,7 @@ export interface FormItemHandle {
|
||||
el: HTMLElement; // 字段根元素(label + 控件 + 错误区)
|
||||
validate(): string | null; // 同步校验:null 通过 / 字符串为错误消息(异步规则不在此时下结论)
|
||||
validateAsync(): Promise<string | null>; // 异步校验:逐条 await,异步规则生效(form 提交走这里)
|
||||
reset(): void; // 复位:原生控件回初始值并同步信号,错误态与红框一起退场
|
||||
}
|
||||
|
||||
/*** 表单容器入参 ***/
|
||||
@@ -489,7 +491,7 @@ export interface FormProps {
|
||||
submitText?: string; // 提交按钮文案;缺省「提交」
|
||||
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
|
||||
onSubmit?: () => void; // 全部字段校验通过后的提交回调
|
||||
onReset?: () => void; // 重置回调
|
||||
onReset?: () => void; // 重置按钮点击回调;组件已先逐项复位(值回初始值、错误态清空),此处只做额外清理
|
||||
onError?: (count: number) => void; // 校验不通过的回调,参数为不通字段数(适合埋点 / 顶部提示)
|
||||
errorSummary?: boolean; // 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true
|
||||
}
|
||||
|
||||
@@ -1909,10 +1909,15 @@
|
||||
"t": "Signal<boolean>",
|
||||
"d": "条件显示信号:为假时整项隐藏且不参与校验(未显示的字段不该拦住提交)"
|
||||
},
|
||||
{
|
||||
"n": "(formItem)reset",
|
||||
"t": "() => void",
|
||||
"d": "自绘控件的复位钩子:select / dataTable 这类没有原生 input 的控件拿不到值,由调用方在这里回写信号;原生输入框由 formItem 自己还原,不用管"
|
||||
},
|
||||
{
|
||||
"n": "onReset",
|
||||
"t": "() => void",
|
||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
||||
"d": "重置按钮点击回调;组件已先逐项复位(原生控件回初始值、错误态与汇总条清空),此处只做额外清理;不传 resetText 不渲染按钮"
|
||||
},
|
||||
{
|
||||
"n": "onError",
|
||||
|
||||
@@ -1685,6 +1685,21 @@ function checkFormErrorLink(pkgs) {
|
||||
return issues;
|
||||
}
|
||||
|
||||
/*** 校验表单重置:只跑 onReset 回调等于「点了没反应」,按钮名叫重置就得真复位 ***/
|
||||
function checkFormReset(pkgs) {
|
||||
const issues = [];
|
||||
for (const pkg of pkgs) {
|
||||
if (pkg.type !== 'skin-dom') continue;
|
||||
const formPath = path.join(ROOT, pkg.dir, 'src', 'form.ts');
|
||||
if (!fs.existsSync(formPath)) continue;
|
||||
const src = fs.readFileSync(formPath, 'utf8');
|
||||
if (!/return \{ el, validate, validateAsync, reset \}/.test(src)) issues.push(`${pkg.name}/src/form.ts 的字段句柄没暴露 reset:form 拿不到复位入口`);
|
||||
if (!/for \(const item of items\) item\.reset\(\);/.test(src)) issues.push(`${pkg.name}/src/form.ts 的重置没逐项复位:只调 onReset 时点「重置」等于什么都不干,红框还留在屏幕上`);
|
||||
if (!/type: 'button', onClick: doReset/.test(src)) issues.push(`${pkg.name}/src/form.ts 的重置按钮没接到 doReset:会退回只跑回调的空壳`);
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/*** 校验同义词表:目标 id 必须真实存在(改名后残留会让回退指到空气),同一个词不得归两组(歧义会让结果随表序漂移) ***/
|
||||
function checkSynonyms(ids) {
|
||||
const issues = [];
|
||||
@@ -1768,6 +1783,7 @@ function cmdVerify() {
|
||||
issues.push(...checkAriaNames(pkgs)); // 输入控件必须有可访问名
|
||||
issues.push(...checkToastLive(pkgs)); // toast 必须有 live 区域与 alert 抢占
|
||||
issues.push(...checkFormErrorLink(pkgs)); // 表单错误文案必须被控件关联
|
||||
issues.push(...checkFormReset(pkgs)); // 重置按钮必须真复位
|
||||
issues.push(...checkSynonyms(new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id))))); // 同义词表指向的组件必须真实存在
|
||||
checkDarkTokens(issues);
|
||||
checkTokenParity(issues);
|
||||
|
||||
Reference in New Issue
Block a user