Skip to content

Commit f50650b

Browse files
committed
Snapshots can now handle files.
Added options.config to Zod adapter.
1 parent f158f94 commit f50650b

File tree

8 files changed

+159
-8
lines changed

8 files changed

+159
-8
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- The `ActionResult` for success or failure is now added to the `onUpdate` event in the `result` property.
12+
- The `ActionResult` for success or failure is now added to the `onUpdate` event in the `result` property. Can be used to more easily access the `ActionData`.
1313
- Added a `fail` function, works the same as the SvelteKit fail, but removes files and sets `form.valid` to `false`.
14+
- `options.config` added to the Zod adapter, so the JSON Schema generation can be customized.
15+
16+
### Fixed
17+
18+
- [Snapshots](https://superforms.rocks/concepts/snapshots) couldn't handle files. They are now reverted to their default value when captured and restored in a snapshot, including the tainted state for these fields.
1419

1520
## [2.10.6] - 2024-03-20
1621

src/lib/adapters/zod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ async function validate<T extends ZodValidation<ZodObjectTypes>>(
7070

7171
function _zod<T extends ZodValidation<ZodObjectTypes>>(
7272
schema: T,
73-
options?: AdapterOptions<T>
73+
options?: AdapterOptions<T> & { config?: Partial<Options> }
7474
): ValidationAdapter<Infer<T>, InferIn<T>> {
7575
return createAdapter({
7676
superFormValidationLibrary: 'zod',
7777
validate: async (data) => validate(schema, data),
78-
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema),
78+
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema, options?.config),
7979
defaults: options?.defaults
8080
});
8181
}

src/lib/client/proxies.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function fileProxy<T extends Record<string, unknown>, Path extends FormPa
156156
}
157157

158158
const dt = new DataTransfer();
159-
if (file) dt.items.add(file);
159+
if (file instanceof File) dt.items.add(file);
160160
fileProxy.set(dt.files);
161161
});
162162

@@ -219,7 +219,7 @@ export function filesProxy<
219219
return;
220220
}
221221

222-
files.filter((f) => !!f).forEach((file) => dt.items.add(file));
222+
files.filter((f) => f instanceof File).forEach((file) => dt.items.add(file));
223223
}
224224
filesProxy.set(dt.files);
225225
});
@@ -235,7 +235,7 @@ export function filesProxy<
235235
const dt = new DataTransfer();
236236
if (Array.isArray(files))
237237
files.forEach((file) => {
238-
if (file) dt.items.add(file);
238+
if (file instanceof File) dt.items.add(file);
239239
});
240240
filesProxy.set(dt.files);
241241
formFiles.set(files);

src/lib/client/superForm.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,6 +1910,30 @@ export function superForm<
19101910
});
19111911
}
19121912

1913+
function removeFiles(formData: T) {
1914+
const paths: (string | number | symbol)[][] = [];
1915+
1916+
traversePaths(formData, (data) => {
1917+
if (data.value instanceof File) {
1918+
paths.push(data.path);
1919+
return 'skip';
1920+
} else if (
1921+
Array.isArray(data.value) &&
1922+
data.value.length &&
1923+
data.value.every((d) => d instanceof File)
1924+
) {
1925+
paths.push(data.path);
1926+
return 'skip';
1927+
}
1928+
});
1929+
1930+
if (!paths.length) return { data: formData, paths };
1931+
1932+
const data = clone(formData);
1933+
setPaths(data, paths, (path) => pathExists(initialForm.data, path)?.value);
1934+
return { data, paths };
1935+
}
1936+
19131937
///// Return the SuperForm object /////////////////////////////////
19141938

19151939
return {
@@ -1927,15 +1951,22 @@ export function superForm<
19271951
options: options as T extends T ? FormOptions<T, M> : never,
19281952

19291953
capture() {
1954+
const { data, paths } = removeFiles(Data.form);
1955+
let tainted = Data.tainted;
1956+
if (paths.length) {
1957+
tainted = clone(tainted) ?? {};
1958+
setPaths(tainted, paths, false);
1959+
}
1960+
19301961
return {
19311962
valid: Data.valid,
19321963
posted: Data.posted,
19331964
errors: Data.errors,
1934-
data: Data.form,
1965+
data,
19351966
constraints: Data.constraints,
19361967
message: Data.message,
19371968
id: Data.formId,
1938-
tainted: Data.tainted,
1969+
tainted,
19391970
shape: Data.shape
19401971
};
19411972
},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { zod } from '$lib/adapters/zod.js';
2+
import { message, superValidate } from '$lib/server/index.js';
3+
import { schema } from './schema.js';
4+
import { fail } from '@sveltejs/kit';
5+
6+
export const load = async () => {
7+
const form = await superValidate(zod(schema));
8+
return { form };
9+
};
10+
11+
export const actions = {
12+
default: async ({ request }) => {
13+
const formData = await request.formData();
14+
console.log(formData);
15+
16+
const form = await superValidate(formData, zod(schema));
17+
console.log(form);
18+
19+
if (!form.valid) return fail(400, { form });
20+
21+
return message(form, 'Posted OK!');
22+
}
23+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script lang="ts">
2+
import { fileProxy, superForm } from '$lib/client/index.js';
3+
import SuperDebug from '$lib/client/SuperDebug.svelte';
4+
5+
export let data;
6+
7+
const { form, errors, tainted, message, enhance, capture, restore } = superForm(data.form, {
8+
taintedMessage: false
9+
});
10+
11+
//export const snapshot = { capture, restore };
12+
13+
let file = fileProxy(form, 'image');
14+
15+
const addFiles = async () => {
16+
form.update(
17+
($form) => {
18+
$form.images = [new File(['123123'], 'test2.png'), new File(['123123'], 'test3.png')];
19+
$form.undefImage = new File(['123123'], 'test4.png');
20+
return $form;
21+
},
22+
{ taint: false }
23+
);
24+
};
25+
26+
export const snapshot = { capture, restore };
27+
</script>
28+
29+
<SuperDebug data={{ $form, $tainted }} />
30+
31+
{#if $message}<h4>{$message}</h4>{/if}
32+
33+
<button on:click={() => addFiles()}>Add files</button>
34+
35+
<form method="POST" enctype="multipart/form-data" use:enhance>
36+
<label>
37+
File: <input
38+
type="file"
39+
name="name"
40+
bind:files={$file}
41+
aria-invalid={$errors.image ? 'true' : undefined}
42+
/>
43+
{#if $errors.image}<span class="invalid">{$errors.image}</span>{/if}
44+
</label>
45+
<div>
46+
<button>Submit</button>
47+
</div>
48+
</form>
49+
50+
<style lang="scss">
51+
form {
52+
margin: 2rem 0;
53+
54+
input {
55+
background-color: #dedede;
56+
}
57+
58+
.invalid {
59+
color: crimson;
60+
}
61+
}
62+
</style>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from 'zod';
2+
3+
export const schema = z.object({
4+
image: z.instanceof(File, { message: 'Please upload a file.' }).nullable(),
5+
undefImage: z.instanceof(File, { message: 'Please upload a file.' }),
6+
images: z.instanceof(File, { message: 'Please upload files.' }).array()
7+
});

src/tests/superForm.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,29 @@ describe('FormOptions', () => {
329329
});
330330
});
331331

332+
describe('Capture', () => {
333+
it('should replace files with their default value, and empty file arrays', async () => {
334+
const schema = z.object({
335+
image: z.instanceof(File, { message: 'Please upload a file.' }).nullable(),
336+
undefImage: z.instanceof(File, { message: 'Please upload a file.' }),
337+
images: z.instanceof(File, { message: 'Please upload files.' }).array()
338+
});
339+
340+
const validated = await superValidate(zod(schema));
341+
const form = superForm(validated);
342+
343+
form.form.update(($form) => {
344+
$form.image = new File(['123123'], 'test.png');
345+
$form.images = [new File(['123123'], 'test2.png'), new File(['123123'], 'test3.png')];
346+
$form.undefImage = new File(['123123'], 'test4.png');
347+
return $form;
348+
});
349+
350+
const captured = form.capture();
351+
expect(captured.data).toStrictEqual({ image: null, images: [], undefImage: undefined });
352+
});
353+
});
354+
332355
///// mockSvelte.ts (must be copy/pasted here) ////////////////////////////////
333356

334357
import { vi } from 'vitest';

0 commit comments

Comments
 (0)