Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-clocks-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: warn on unread validation issues
53 changes: 52 additions & 1 deletion packages/kit/src/runtime/client/remote-functions/form.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -239,6 +276,10 @@ export function form(id) {
} else {
void invalidateAll();
}
} else {
if (DEV) {
warn_on_missing_issue_reads();
}
}

return succeeded;
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/form-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ export function deep_get(object, path) {
* @param {any} target - Function or empty POJO
* @param {() => Record<string, any>} get_input - Function to get current input data
* @param {(path: (string | number)[], value: any) => void} set_input - Function to set input data
* @param {() => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
* @param {(path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
* @param {(string | number)[]} path - Current access path
* @returns {any} Proxy object with name(), value(), and issues() methods
*/
Expand Down Expand Up @@ -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) => ({
Expand Down
Loading