Skip to content

Commit 0462f82

Browse files
committed
feat: expose field touched state via proxy API
1 parent 0704908 commit 0462f82

File tree

5 files changed

+112
-39
lines changed

5 files changed

+112
-39
lines changed

packages/kit/src/exports/public.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,6 +1878,8 @@ type RemoteFormFieldMethods<T> = {
18781878
value(): T;
18791879
/** Set the values that will be submitted */
18801880
set(input: T): T;
1881+
/** Whether the field or any nested field has been interacted with since the form was mounted */
1882+
touched(): boolean;
18811883
/** Validation issues, if any */
18821884
issues(): RemoteFormIssue[] | undefined;
18831885
};

packages/kit/src/runtime/app/server/remote/form.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
create_field_proxy,
1010
set_nested_value,
1111
throw_on_old_property_access,
12-
deep_set
12+
deep_set,
13+
build_path_string
1314
} from '../../../form-utils.svelte.js';
1415
import { get_cache, run_remote_function } from './shared.js';
1516

@@ -222,9 +223,17 @@ export function form(validate_or_fn, maybe_fn) {
222223
const input =
223224
path.length === 0 ? value : deep_set(data?.input ?? {}, path.map(String), value);
224225

225-
(get_cache(__)[''] ??= {}).input = input;
226+
const cache = get_cache(__);
227+
const entry = (cache[''] ??= {});
228+
entry.input = input;
229+
230+
if (path.length > 0) {
231+
const key = build_path_string(path);
232+
(entry.touched ??= {})[key] = true;
233+
}
226234
},
227-
() => data?.issues ?? {}
235+
() => data?.issues ?? {},
236+
() => data?.touched ?? {}
228237
);
229238
}
230239
});

packages/kit/src/runtime/client/remote-functions/form.svelte.js

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,16 @@ export function form(id) {
345345
// but that makes the types unnecessarily awkward
346346
const element = /** @type {HTMLInputElement} */ (e.target);
347347

348-
let name = element.name;
349-
if (!name) return;
348+
const original_name = element.name;
349+
if (!original_name) return;
350350

351-
const is_array = name.endsWith('[]');
352-
if (is_array) name = name.slice(0, -2);
351+
const is_array = original_name.endsWith('[]');
352+
const base_name = is_array ? original_name.slice(0, -2) : original_name;
353353

354354
const is_file = element.type === 'file';
355355

356-
touched[name] = true;
356+
const sanitized_name = base_name.replace(/^[nb]:/, '');
357+
touched[sanitized_name] = true;
357358

358359
if (is_array) {
359360
let value;
@@ -365,7 +366,7 @@ export function form(id) {
365366
);
366367
} else {
367368
const elements = /** @type {HTMLInputElement[]} */ (
368-
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
369+
Array.from(form.querySelectorAll(`[name="${base_name}[]"]`))
369370
);
370371

371372
if (DEV) {
@@ -386,48 +387,47 @@ export function form(id) {
386387
}
387388
}
388389

389-
input = set_nested_value(input, name, value);
390+
input = set_nested_value(input, base_name, value);
390391
} else if (is_file) {
391392
if (DEV && element.multiple) {
392393
throw new Error(
393-
`Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${name}" to "${name}[]"`
394+
`Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${sanitized_name}" to "${sanitized_name}[]"`
394395
);
395396
}
396397

397398
const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
398399

399400
if (file) {
400-
input = set_nested_value(input, name, file);
401+
input = set_nested_value(input, base_name, file);
401402
} else {
402403
// Remove the property by setting to undefined and clean up
403-
const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
404+
const path_parts = sanitized_name.split(/\.|\[|\]/).filter(Boolean);
404405
let current = /** @type {any} */ (input);
405406
for (let i = 0; i < path_parts.length - 1; i++) {
406-
if (current[path_parts[i]] == null) return;
407-
current = current[path_parts[i]];
407+
const part = path_parts[i];
408+
if (current[part] == null) return;
409+
current = current[part];
408410
}
409411
delete current[path_parts[path_parts.length - 1]];
410412
}
411413
} else {
412414
input = set_nested_value(
413415
input,
414-
name,
416+
base_name,
415417
element.type === 'checkbox' && !element.checked ? null : element.value
416418
);
417419
}
418420

419-
name = name.replace(/^[nb]:/, '');
421+
versions[sanitized_name] ??= 0;
422+
versions[sanitized_name] += 1;
420423

421-
versions[name] ??= 0;
422-
versions[name] += 1;
423-
424-
const path = split_path(name);
424+
const path = split_path(sanitized_name);
425425

426426
while (path.pop() !== undefined) {
427-
const name = build_path_string(path);
427+
const parent_name = build_path_string(path);
428428

429-
versions[name] ??= 0;
430-
versions[name] += 1;
429+
versions[parent_name] ??= 0;
430+
versions[parent_name] += 1;
431431
}
432432
});
433433

@@ -437,6 +437,7 @@ export function form(id) {
437437
await tick();
438438

439439
input = convert_formdata(new FormData(form));
440+
touched = {};
440441
update_all_versions();
441442
});
442443

@@ -554,7 +555,8 @@ export function form(id) {
554555
}
555556
}
556557
},
557-
() => issues
558+
() => issues,
559+
() => touched
558560
)
559561
},
560562
result: {

packages/kit/src/runtime/form-utils.svelte.js

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,19 @@ export function deep_get(object, path) {
180180
* @param {(path: string) => void} depend - Function to make an effect depend on a specific field
181181
* @param {(path: (string | number)[], value: any) => void} set_input - Function to set input data
182182
* @param {() => Record<string, InternalRemoteFormIssue[]>} get_issues - Function to get current issues
183+
* @param {() => Record<string, boolean>} [get_touched] - Function to get touched fields
183184
* @param {(string | number)[]} path - Current access path
184185
* @returns {any} Proxy object with name(), value(), and issues() methods
185186
*/
186-
export function create_field_proxy(target, get_input, depend, set_input, get_issues, path = []) {
187+
export function create_field_proxy(
188+
target,
189+
get_input,
190+
depend,
191+
set_input,
192+
get_issues,
193+
get_touched = () => ({}),
194+
path = []
195+
) {
187196
const path_string = build_path_string(path);
188197

189198
const get_value = () => {
@@ -197,7 +206,7 @@ export function create_field_proxy(target, get_input, depend, set_input, get_iss
197206

198207
// Handle array access like jobs[0]
199208
if (/^\d+$/.test(prop)) {
200-
return create_field_proxy({}, get_input, depend, set_input, get_issues, [
209+
return create_field_proxy({}, get_input, depend, set_input, get_issues, get_touched, [
201210
...path,
202211
parseInt(prop, 10)
203212
]);
@@ -210,17 +219,22 @@ export function create_field_proxy(target, get_input, depend, set_input, get_iss
210219
set_input(path, newValue);
211220
return newValue;
212221
};
213-
return create_field_proxy(set_func, get_input, depend, set_input, get_issues, [
222+
return create_field_proxy(set_func, get_input, depend, set_input, get_issues, get_touched, [
214223
...path,
215224
prop
216225
]);
217226
}
218227

219228
if (prop === 'value') {
220-
return create_field_proxy(get_value, get_input, depend, set_input, get_issues, [
221-
...path,
222-
prop
223-
]);
229+
return create_field_proxy(
230+
get_value,
231+
get_input,
232+
depend,
233+
set_input,
234+
get_issues,
235+
get_touched,
236+
[...path, prop]
237+
);
224238
}
225239

226240
if (prop === 'issues' || prop === 'allIssues') {
@@ -240,10 +254,51 @@ export function create_field_proxy(target, get_input, depend, set_input, get_iss
240254
}));
241255
};
242256

243-
return create_field_proxy(issues_func, get_input, depend, set_input, get_issues, [
244-
...path,
245-
prop
246-
]);
257+
return create_field_proxy(
258+
issues_func,
259+
get_input,
260+
depend,
261+
set_input,
262+
get_issues,
263+
get_touched,
264+
[...path, prop]
265+
);
266+
}
267+
268+
if (prop === 'touched') {
269+
const touched_func = () => {
270+
depend(path_string);
271+
272+
const touched = get_touched();
273+
274+
if (path.length === 0 || path_string === '') {
275+
return Object.keys(touched).length > 0;
276+
}
277+
278+
if (touched[path_string]) return true;
279+
280+
for (const candidate in touched) {
281+
if (candidate === path_string) continue;
282+
if (!candidate.startsWith(path_string)) continue;
283+
284+
const next = candidate.slice(path_string.length, path_string.length + 1);
285+
if (next === '.' || next === '[') {
286+
return true;
287+
}
288+
}
289+
290+
return false;
291+
};
292+
293+
return create_field_proxy(
294+
touched_func,
295+
get_input,
296+
depend,
297+
set_input,
298+
get_issues,
299+
get_touched,
300+
[...path, prop]
301+
);
247302
}
248303

249304
if (prop === 'as') {
@@ -392,14 +447,17 @@ export function create_field_proxy(target, get_input, depend, set_input, get_iss
392447
});
393448
};
394449

395-
return create_field_proxy(as_func, get_input, depend, set_input, get_issues, [
450+
return create_field_proxy(as_func, get_input, depend, set_input, get_issues, get_touched, [
396451
...path,
397452
'as'
398453
]);
399454
}
400455

401456
// Handle property access (nested fields)
402-
return create_field_proxy({}, get_input, depend, set_input, get_issues, [...path, prop]);
457+
return create_field_proxy({}, get_input, depend, set_input, get_issues, get_touched, [
458+
...path,
459+
prop
460+
]);
403461
}
404462
});
405463
}

packages/kit/types/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1854,6 +1854,8 @@ declare module '@sveltejs/kit' {
18541854
value(): T;
18551855
/** Set the values that will be submitted */
18561856
set(input: T): T;
1857+
/** Whether the field or any nested field has been interacted with since the form was mounted */
1858+
touched(): boolean;
18571859
/** Validation issues, if any */
18581860
issues(): RemoteFormIssue[] | undefined;
18591861
};
@@ -3476,4 +3478,4 @@ declare module '$app/types' {
34763478
export type Asset = ReturnType<AppTypes['Asset']>;
34773479
}
34783480

3479-
//# sourceMappingURL=index.d.ts.map
3481+
//# sourceMappingURL=index.d.ts.map

0 commit comments

Comments
 (0)