Skip to content

Commit 98dd381

Browse files
committed
WIP: Support for sets.
FormPath needs to be updated.
1 parent eba864a commit 98dd381

File tree

8 files changed

+150
-26
lines changed

8 files changed

+150
-26
lines changed

src/lib/client/clientValidation.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
type ZodValidation,
88
type FormPathLeaves,
99
SuperFormError,
10-
type TaintedFields
10+
type TaintedFields,
11+
type UnwrapEffects
1112
} from '../index.js';
1213
import type { z, AnyZodObject, ZodError, ZodTypeAny } from 'zod';
1314
import {
@@ -23,10 +24,13 @@ import type { FormPathType } from '../stringPath.js';
2324
import { clone } from '../utils.js';
2425
import { get } from 'svelte/store';
2526

26-
export type ValidateOptions<V> = Partial<{
27+
export type ValidateOptions<
28+
V,
29+
T extends AnyZodObject = AnyZodObject
30+
> = Partial<{
2731
value: V;
2832
update: boolean | 'errors' | 'value';
29-
taint: TaintOption;
33+
taint: TaintOption<T>;
3034
errors: string | string[];
3135
}>;
3236

@@ -43,14 +47,16 @@ export function validateForm<T extends AnyZodObject>(): Promise<
4347
export function validateForm<T extends AnyZodObject>(
4448
path: FormPathLeaves<z.infer<T>>,
4549
opts?: ValidateOptions<
46-
FormPathType<z.infer<T>, FormPathLeaves<z.infer<T>>>
50+
FormPathType<z.infer<T>, FormPathLeaves<z.infer<T>>>,
51+
T
4752
>
4853
): Promise<string[] | undefined>;
4954

5055
export function validateForm<T extends AnyZodObject>(
5156
path?: FormPathLeaves<z.infer<T>>,
5257
opts?: ValidateOptions<
53-
FormPathType<z.infer<T>, FormPathLeaves<z.infer<T>>>
58+
FormPathType<z.infer<T>, FormPathLeaves<z.infer<T>>>,
59+
T
5460
>
5561
) {
5662
// See the validate function inside superForm for implementation.
@@ -254,13 +260,16 @@ export async function validateObjectErrors<T extends AnyZodObject, M>(
254260
* Validate a specific form field.
255261
* @DCI-context
256262
*/
257-
export async function validateField<T extends AnyZodObject, M>(
263+
export async function validateField<
264+
T extends ZodValidation<AnyZodObject>,
265+
M
266+
>(
258267
path: string[],
259268
formOptions: FormOptions<T, M>,
260269
data: SuperForm<T, M>['form'],
261270
Errors: SuperForm<T, M>['errors'],
262271
Tainted: SuperForm<T, M>['tainted'],
263-
options: ValidateOptions<unknown> = {}
272+
options: ValidateOptions<unknown, UnwrapEffects<T>> = {}
264273
): Promise<string[] | undefined> {
265274
function Errors_clear() {
266275
clearErrors(Errors, { undefinePath: path, clearFormLevelErrors: true });
@@ -330,13 +339,13 @@ export async function validateField<T extends AnyZodObject, M>(
330339
}
331340

332341
// @DCI-context
333-
async function _validateField<T extends AnyZodObject, M>(
342+
async function _validateField<T extends ZodValidation<AnyZodObject>, M>(
334343
path: string[],
335344
validators: FormOptions<T, M>['validators'],
336345
data: SuperForm<T, M>['form'],
337346
Errors: SuperForm<T, M>['errors'],
338347
Tainted: SuperForm<T, M>['tainted'],
339-
options: ValidateOptions<unknown> = {}
348+
options: ValidateOptions<unknown, UnwrapEffects<T>> = {}
340349
): Promise<{ validated: boolean | 'all'; errors: string[] | undefined }> {
341350
if (options.update === undefined) options.update = true;
342351
if (options.taint === undefined) options.taint = false;

src/lib/client/index.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ type SuperFormSnapshot<T extends AnyZodObject, M = any> = SuperValidated<
188188
M
189189
> & { tainted: TaintedFields<T> | undefined };
190190

191-
export type TaintOption = boolean | 'untaint' | 'untaint-all';
191+
export type TaintOption<T extends AnyZodObject = AnyZodObject> =
192+
| boolean
193+
| 'untaint'
194+
| 'untaint-all'
195+
| { fields: FormPathLeaves<z.infer<T>> | FormPathLeaves<z.infer<T>>[] };
192196

193197
// eslint-disable-next-line @typescript-eslint/no-explicit-any
194198
export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
@@ -197,12 +201,12 @@ export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
197201
set(
198202
this: void,
199203
value: z.infer<UnwrapEffects<T>>,
200-
options?: { taint?: TaintOption }
204+
options?: { taint?: TaintOption<UnwrapEffects<T>> }
201205
): void;
202206
update(
203207
this: void,
204208
updater: Updater<z.infer<UnwrapEffects<T>>>,
205-
options?: { taint?: TaintOption }
209+
options?: { taint?: TaintOption<UnwrapEffects<T>> }
206210
): void;
207211
};
208212
formId: Writable<string | undefined>;
@@ -417,7 +421,7 @@ export function superForm<
417421
subscribe: _formData.subscribe,
418422
set: (
419423
value: Parameters<typeof _formData.set>[0],
420-
options: { taint?: TaintOption } = {}
424+
options: { taint?: TaintOption<UnwrappedT> } = {}
421425
) => {
422426
Tainted_update(
423427
value,
@@ -429,7 +433,7 @@ export function superForm<
429433
},
430434
update: (
431435
updater: Parameters<typeof _formData.update>[0],
432-
options: { taint?: TaintOption } = {}
436+
options: { taint?: TaintOption<UnwrappedT> } = {}
433437
) => {
434438
return _formData.update((value) => {
435439
const output = updater(value);
@@ -593,7 +597,10 @@ export function superForm<
593597
return obj === true;
594598
}
595599

596-
async function Tainted__validate(path: string[], taint: TaintOption) {
600+
async function Tainted__validate(
601+
path: string[],
602+
taint: TaintOption<UnwrappedT>
603+
) {
597604
if (
598605
options.validationMethod == 'onblur' ||
599606
options.validationMethod == 'submit-only'
@@ -641,7 +648,7 @@ export function superForm<
641648
async function Tainted_update(
642649
newObj: unknown,
643650
compareAgainst: unknown,
644-
taintOptions: TaintOption
651+
taintOptions: TaintOption<UnwrappedT>
645652
) {
646653
if (taintOptions === false) {
647654
return;
@@ -650,7 +657,18 @@ export function superForm<
650657
return;
651658
}
652659

653-
const paths = comparePaths(newObj, compareAgainst);
660+
let paths = comparePaths(newObj, compareAgainst);
661+
662+
if (typeof taintOptions === 'object') {
663+
if (typeof taintOptions.fields === 'string')
664+
taintOptions.fields = [taintOptions.fields];
665+
666+
paths = taintOptions.fields.map((path) =>
667+
splitPath(path)
668+
) as string[][];
669+
670+
taintOptions = true;
671+
}
654672

655673
if (taintOptions === true) {
656674
LastChanges.set(paths);
@@ -827,7 +845,10 @@ export function superForm<
827845

828846
function validate<Path extends FormPathLeaves<z.infer<UnwrapEffects<T>>>>(
829847
path?: Path,
830-
opts?: ValidateOptions<FormPathType<z.infer<UnwrapEffects<T>>, Path>>
848+
opts?: ValidateOptions<
849+
FormPathType<z.infer<UnwrapEffects<T>>, Path>,
850+
UnwrapEffects<T>
851+
>
831852
) {
832853
if (path === undefined) {
833854
return clientValidation<UnwrapEffects<T>, M>(
@@ -881,7 +902,7 @@ export function superForm<
881902
return rebind(snapshot, snapshot.tainted ?? true);
882903
},
883904

884-
validate: validate as typeof validateForm<UnwrapEffects<T>>,
905+
validate: validate as typeof validateForm<UnwrappedT>,
885906

886907
enhance: (
887908
el: HTMLFormElement,

src/lib/schemaEntity.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,10 @@ export function valueOrDefault(
242242
// Cannot add default for ZodDate due to https://github.com/Rich-Harris/devalue/issues/51
243243
//if (zodType._def.typeName == "ZodDate") return new Date(NaN);
244244
if (zodType._def.typeName == 'ZodArray') return [];
245-
if (zodType._def.typeName == 'ZodObject')
245+
if (zodType._def.typeName == 'ZodObject') {
246246
return defaultValues(zodType as AnyZodObject);
247+
}
248+
if (zodType._def.typeName == 'ZodSet') return new Set();
247249
if (zodType._def.typeName == 'ZodRecord') return {};
248250
if (zodType._def.typeName == 'ZodBigInt') return BigInt(0);
249251
if (zodType._def.typeName == 'ZodSymbol') return Symbol();

src/lib/traversal.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,20 +197,31 @@ export function comparePaths(newObj: unknown, oldObj: unknown) {
197197
const diffPaths = new Map<string, string[]>();
198198

199199
function checkPath(data: PathData, compareTo: object) {
200-
if (data.isLeaf) {
201-
const exists = traversePath(compareTo, data.path as FieldPath<object>);
200+
const exists = traversePath(compareTo, data.path as FieldPath<object>);
201+
202+
function addDiff() {
203+
diffPaths.set(data.path.join(' '), data.path);
204+
}
202205

206+
if (data.isLeaf) {
203207
if (!exists) {
204-
diffPaths.set(data.path.join(' '), data.path);
208+
addDiff();
205209
} else if (
206210
data.value instanceof Date &&
207211
exists.value instanceof Date &&
208212
data.value.getTime() != exists.value.getTime()
209213
) {
210-
diffPaths.set(data.path.join(' '), data.path);
214+
addDiff();
211215
} else if (data.value !== exists.value) {
212-
diffPaths.set(data.path.join(' '), data.path);
216+
addDiff();
213217
}
218+
} else if (
219+
exists &&
220+
data.value instanceof Set &&
221+
exists.value instanceof Set &&
222+
data.value !== exists.value
223+
) {
224+
addDiff();
214225
}
215226
}
216227

src/routes/tests/_empty/+page.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export const load = (async () => {
1111
export const actions = {
1212
default: async ({ request }) => {
1313
const formData = await request.formData();
14-
const form = await superValidate(formData, schema);
14+
console.log(formData);
1515

16+
const form = await superValidate(formData, schema);
1617
console.log('POST', form);
1718

1819
if (!form.valid) return fail(400, { form });
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Actions, PageServerLoad } from './$types';
2+
import { message, superValidate } from '$lib/server';
3+
import { schema } from './schema';
4+
import { fail } from '@sveltejs/kit';
5+
6+
export const load = (async () => {
7+
const form = await superValidate(schema);
8+
console.log('🚀 ~ file: +page.server.ts:8 ~ load ~ form:', form);
9+
10+
return { form };
11+
}) satisfies PageServerLoad;
12+
13+
export const actions = {
14+
default: async ({ request }) => {
15+
const formData = await request.formData();
16+
console.log(formData);
17+
18+
const form = await superValidate(formData, schema);
19+
console.log('POST', form);
20+
21+
if (!form.valid) return fail(400, { form });
22+
23+
return message(form, 'Posted OK!');
24+
}
25+
} satisfies Actions;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script lang="ts">
2+
import { superForm } from '$lib/client';
3+
import type { PageData } from './$types';
4+
import SuperDebug from '$lib/client/SuperDebug.svelte';
5+
//import { schema } from './schema';
6+
7+
export let data: PageData;
8+
9+
const { form, errors, tainted, message, enhance } = superForm(data.form, {
10+
dataType: 'json'
11+
//validators: schema
12+
});
13+
</script>
14+
15+
<SuperDebug data={{ things: Array.from($form.things), $tainted }} />
16+
17+
{#if $message}<h4>{$message}</h4>{/if}
18+
19+
<form method="POST" use:enhance>
20+
<div>
21+
<button
22+
type="button"
23+
on:click={() =>
24+
form.update(
25+
($form) => {
26+
$form.things.add(1);
27+
return $form;
28+
},
29+
{ taint: { fields: 'things' } }
30+
)}>Add 1</button
31+
>
32+
<button
33+
type="button"
34+
on:click={() => {
35+
$form.things = new Set([2, 3, 4]);
36+
}}>Set 2,3,4</button
37+
>
38+
<button>Submit</button>
39+
</div>
40+
</form>
41+
42+
<style lang="scss">
43+
form {
44+
margin: 2rem 0;
45+
46+
input {
47+
background-color: #dedede;
48+
}
49+
}
50+
</style>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { z } from 'zod';
2+
3+
export const schema = z.object({
4+
things: z.set(z.number()).default(new Set([2, 3]))
5+
});

0 commit comments

Comments
 (0)