Skip to content

Commit 17a7aeb

Browse files
committed
fieldErrors added to arrayProxy.
1 parent a26e565 commit 17a7aeb

File tree

6 files changed

+96
-14
lines changed

6 files changed

+96
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- A `fieldErrors` store is added to [arrayProxy](https://superforms.rocks/api#arrayproxysuperform-fieldname-options), so field errors (for items in the array, not the array itself) can be accessed.
13+
1014
### Fixed
1115

1216
- When cancelling a request, timers were cancelled too early in [SPA mode](https://superforms.rocks/concepts/spa) and when [client-side validation](https://superforms.rocks/concepts/client-validation) failed.
17+
- [Proxies](https://superforms.rocks/concepts/proxy-objects) didn't set or update a nested path unless it previously existed.
1318

1419
## [1.10.2] - 2023-11-14
1520

src/lib/client/proxies.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ function _stringProxy<
240240
};
241241
}
242242

243+
type ArrayFieldErrors = any[];
244+
243245
export function arrayProxy<
244246
T extends ZodValidation<AnyZodObject>,
245247
Path extends FormPathArrays<z.infer<UnwrapEffects<T>>>
@@ -252,15 +254,66 @@ export function arrayProxy<
252254
path: Path;
253255
values: Writable<FormPathType<z.infer<UnwrapEffects<T>>, Path>>;
254256
errors: Writable<string[] | undefined>;
257+
fieldErrors: Writable<ArrayFieldErrors>;
255258
} {
259+
const allErrors = fieldProxy(
260+
superForm.errors,
261+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
262+
`${path}` as any
263+
);
264+
265+
const onlyFieldErrors = derived<typeof allErrors, ArrayFieldErrors>(
266+
allErrors,
267+
($errors) => {
268+
const output: ArrayFieldErrors = [];
269+
for (const key in $errors) {
270+
if (key == '_errors') continue;
271+
output[key] = $errors[key];
272+
}
273+
return output as ArrayFieldErrors;
274+
}
275+
);
276+
277+
function updateArrayErrors(
278+
errors: Record<number, any> | undefined,
279+
value: ArrayFieldErrors
280+
) {
281+
if (errors !== undefined) {
282+
for (const key in errors) {
283+
if (key == '_errors') continue;
284+
errors[key] = undefined;
285+
}
286+
}
287+
if (value !== undefined) {
288+
for (const key in value) {
289+
if (errors === undefined) errors = {};
290+
errors[key] = value[key];
291+
}
292+
}
293+
return errors;
294+
}
295+
296+
const fieldErrors: Writable<ArrayFieldErrors> = {
297+
subscribe: onlyFieldErrors.subscribe,
298+
update(upd: Updater<ArrayFieldErrors>) {
299+
allErrors.update(($errors) =>
300+
updateArrayErrors($errors, upd($errors))
301+
);
302+
},
303+
set(value: ArrayFieldErrors) {
304+
allErrors.update(($errors) => updateArrayErrors($errors, value));
305+
}
306+
};
307+
256308
return {
257309
path,
258310
values: superFieldProxy(superForm, path, options),
259311
errors: fieldProxy(
260312
superForm.errors,
261313
// eslint-disable-next-line @typescript-eslint/no-explicit-any
262314
`${path}._errors` as any
263-
) as Writable<string[] | undefined>
315+
) as Writable<string[] | undefined>,
316+
fieldErrors
264317
};
265318
}
266319

@@ -407,14 +460,20 @@ export function fieldProxy<T extends object, Path extends FormPath<T>>(
407460
},
408461
update(upd: Updater<FormPathType<T, Path>>) {
409462
form.update((f) => {
410-
const output = traversePath(f, path2);
463+
const output = traversePath(f, path2, ({ parent, key, value }) => {
464+
if (value === undefined) parent[key] = {};
465+
return parent[key];
466+
});
411467
if (output) output.parent[output.key] = upd(output.value);
412468
return f;
413469
});
414470
},
415471
set(value: FormPathType<T, Path>) {
416472
form.update((f) => {
417-
const output = traversePath(f, path2);
473+
const output = traversePath(f, path2, ({ parent, key, value }) => {
474+
if (value === undefined) parent[key] = {};
475+
return parent[key];
476+
});
418477
if (output) output.parent[output.key] = value;
419478
return f;
420479
});

src/lib/stringPath.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export type FormPathType<T, P extends string> = P extends keyof T
141141
: P extends `${number}]${infer Rest}`
142142
? NonNullable<T> extends (infer U)[]
143143
? FormPathType<U, Rest>
144-
: { invalid_path: P; Type: T }
144+
: { invalid_path1: P; Type: T }
145145
: P extends `${infer K}[${infer Rest}`
146146
? K extends keyof NonNullable<T>
147147
? FormPathType<NonNullable<T>[K], Rest>
@@ -151,23 +151,23 @@ export type FormPathType<T, P extends string> = P extends keyof T
151151
? FormPathType<NonNullable<T>[K], Rest>
152152
: NonNullable<T> extends (infer U)[]
153153
? FormPathType<U, Rest>
154-
: { invalid_path: P; Type: T }
154+
: { invalid_path2: P; Type: T }
155155
: P extends `[${infer K}].${infer Rest}`
156156
? K extends number
157157
? T extends (infer U)[]
158158
? FormPathType<U, Rest>
159-
: { invalid_path: P; Type: T }
159+
: { invalid_path3: P; Type: T }
160160
: P extends `${number}`
161161
? NonNullable<T> extends (infer U)[]
162162
? U
163-
: { invalid_path: P; Type: T }
163+
: { invalid_path4: P; Type: T }
164164
: P extends keyof NonNullable<T>
165165
? NonNullable<T>[P]
166166
: P extends `${number}`
167167
? NonNullable<T> extends (infer U)[]
168168
? U
169-
: { invalid_path: P; Type: T }
170-
: { invalid_path: P; Type: T }
169+
: { invalid_path5: P; Type: T }
170+
: { invalid_path6: P; Type: T }
171171
: P extends ''
172172
? T
173-
: { invalid_path: P; Type: T };
173+
: { invalid_path7: P; Type: T };

src/routes/tests/array-component/+page.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { superForm } from '$lib/client';
2+
import { arrayProxy, superForm } from '$lib/client';
33
import type { PageData } from './$types';
44
import SuperDebug from '$lib/client/SuperDebug.svelte';
55
import AutoComplete from './AutoComplete.svelte';
@@ -16,9 +16,11 @@
1616
{ value: 'A', label: 'Aldebaran' },
1717
{ value: 'B', label: 'Betelgeuse' }
1818
];
19+
20+
const { fieldErrors } = arrayProxy(pageForm, 'sub.tags');
1921
</script>
2022

21-
<SuperDebug data={{ $form, $errors }} />
23+
<SuperDebug data={{ $form, $errors, $fieldErrors }} />
2224

2325
{#if $tainted}<h3>FORM IS TAINTED</h3>{/if}
2426

src/routes/tests/array-component/AutoComplete.svelte

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
import type { ZodValidation, FormPathArrays } from '$lib';
99
import type { SuperForm } from '$lib/client';
1010
import { arrayProxy, type TaintOptions } from '$lib/client';
11+
import SuperDebug from '$lib/client/SuperDebug.svelte';
1112
1213
export let form: SuperForm<ZodValidation<T>, unknown>;
1314
export let field: FormPathArrays<z.infer<T>>;
1415
export let options: { value: string; label: string }[];
1516
export let label = '';
1617
export let taint: TaintOptions = true;
1718
18-
const { values, errors } = arrayProxy(form, field, { taint });
19+
const { values, errors, fieldErrors } = arrayProxy(form, field, { taint });
1920
</script>
2021

22+
<SuperDebug label="Component fieldErrors" data={$fieldErrors} />
23+
2124
{#if label}<label for={field}>{label}</label>{/if}
2225

2326
<!-- Note that the selected attribute is required for this to work without JS -->
@@ -29,8 +32,21 @@
2932
{/each}
3033
</select>
3134

35+
<button
36+
type="button"
37+
on:click={() => {
38+
$fieldErrors = [];
39+
}}>Change errors</button
40+
>
41+
3242
{#if $errors}<p class="invalid">{$errors}</p>{/if}
3343

44+
{#each $fieldErrors as $err, i}
45+
{#if $err}
46+
<p class="invalid">Item {$values[i]}: {$err}</p>
47+
{/if}
48+
{/each}
49+
3450
<style>
3551
.invalid {
3652
color: crimson;

src/routes/tests/array-component/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { z } from 'zod';
22

33
export const schema = z.object({
44
sub: z.object({
5-
tags: z.string().min(1).array().min(1)
5+
tags: z.string().min(2).array().min(2)
66
})
77
});

0 commit comments

Comments
 (0)