diff --git a/.changeset/slow-clocks-brush.md b/.changeset/slow-clocks-brush.md new file mode 100644 index 000000000000..a5e2dd6d59ea --- /dev/null +++ b/.changeset/slow-clocks-brush.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: warn on unread validation issues diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 1af3bffdcc6a..e34c1f70a5bb 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -87,6 +87,38 @@ export function form(id) { let submitted = false; + /** @type {InternalRemoteFormIssue[] | null} */ + let unread_issues = null; + + /** + * In dev, warn if there are validation issues going unread + */ + function warn_on_missing_issue_reads() { + unread_issues = raw_issues; + + setTimeout(() => { + if (unread_issues === null) { + return; + } + + if (unread_issues.length > 0) { + const message = `Form submission had invalid data, but the validation issues were ignored:`; + const summary = unread_issues + .map((issue) => + issue.path.length === 0 + ? ` - ${issue.message}` + : ` - ${issue.path.join('.')} (${issue.message})` + ) + .join('\n'); + const suggestion = `Make sure you provide actionable feedback to users, using e.g. \`myForm.fields.myField.issues()\` or \`myForm.fields.allIssues()\``; + + console.warn(`${message}\n\n${summary}\n\n${suggestion}`); + } + + unread_issues = null; + }); + } + /** * @param {FormData} form_data * @returns {Record} @@ -121,6 +153,11 @@ export function form(id) { raw_issues, validated.issues.map((issue) => normalize_issue(issue, false)) ); + + if (DEV) { + warn_on_missing_issue_reads(); + } + pending_count--; return; } @@ -239,6 +276,10 @@ export function form(id) { } else { void invalidateAll(); } + } else { + if (DEV) { + warn_on_missing_issue_reads(); + } } return succeeded; @@ -505,7 +546,17 @@ export function form(id) { touched[key] = true; } }, - () => issues + (path, all) => { + if (DEV && unread_issues !== null && path !== undefined) { + unread_issues = unread_issues.filter((issue) => { + return all + ? issue.path.slice(0, path.length).join('.') !== path.join('.') + : issue.path.join('.') !== path.join('.'); + }); + } + + return issues; + } ) }, result: { diff --git a/packages/kit/src/runtime/form-utils.js b/packages/kit/src/runtime/form-utils.js index 88bd9b58a9fd..9c757f9093ad 100644 --- a/packages/kit/src/runtime/form-utils.js +++ b/packages/kit/src/runtime/form-utils.js @@ -604,7 +604,7 @@ export function deep_get(object, path) { * @param {any} target - Function or empty POJO * @param {() => Record} get_input - Function to get current input data * @param {(path: (string | number)[], value: any) => void} set_input - Function to set input data - * @param {() => Record} get_issues - Function to get current issues + * @param {(path?: (string | number)[], all?: boolean) => Record} get_issues - Function to get current issues * @param {(string | number)[]} path - Current access path * @returns {any} Proxy object with name(), value(), and issues() methods */ @@ -641,7 +641,7 @@ export function create_field_proxy(target, get_input, set_input, get_issues, pat if (prop === 'issues' || prop === 'allIssues') { const issues_func = () => { - const all_issues = get_issues()[key === '' ? '$' : key]; + const all_issues = get_issues(path, prop === 'allIssues')[key === '' ? '$' : key]; if (prop === 'allIssues') { return all_issues?.map((issue) => ({