Skip to content

Commit 0dddb36

Browse files
committed
Client-side validation was ignored when a data property was missing or didn't match the schema type.
Fixes #243.
1 parent 8c22a48 commit 0dddb36

File tree

8 files changed

+143
-41
lines changed

8 files changed

+143
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- In rare cases, timers weren't resetted when redirecting to the same route.
13+
- Client-side validation was ignored when a data property was missing or didn't match the schema type. It now fails with a console error. ([#243](https://github.com/ciscoheat/sveltekit-superforms/issues/243))
1314

1415
## [1.5.0] - 2023-07-23
1516

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
"slugify": "^1.6.6",
108108
"svelte": "^4.1.2",
109109
"svelte-check": "^3.4.6",
110-
"sveltekit-flash-message": "^2.1.1",
110+
"sveltekit-flash-message": "^2.1.3",
111111
"sveltekit-rate-limiter": "^0.3.2",
112112
"throttle-debounce": "^5.0.0",
113113
"tslib": "^2.6.1",

pnpm-lock.yaml

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/client/clientValidation.ts

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
9393
constraints: SuperValidated<ZodValidation<T>>['constraints'],
9494
posted: boolean
9595
): Promise<SuperValidated<ZodValidation<T>>> {
96-
if (validators) {
97-
let valid: boolean;
98-
let clientErrors: ValidationErrors<T> = {};
96+
let valid = true;
97+
let clientErrors: ValidationErrors<T> = {};
9998

99+
if (validators) {
100100
if ('safeParseAsync' in validators) {
101101
// Zod validator
102102
const validator = validators as AnyZodObject;
@@ -113,8 +113,15 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
113113
}
114114
} else {
115115
// SuperForms validator
116-
117-
valid = true;
116+
checkData = { ...checkData };
117+
// Add top-level validator fields to non-existing checkData fields
118+
// so they will be validated even if the field doesn't exist
119+
for (const [key, value] of Object.entries(validators)) {
120+
if (typeof value === 'function' && !(key in checkData)) {
121+
// @ts-expect-error Setting undefined fields so they will be validated based on field existance.
122+
checkData[key] = undefined;
123+
}
124+
}
118125

119126
const validator = validators as Validators<T>;
120127
const newErrors: {
@@ -133,29 +140,49 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
133140
if (typeof maybeValidator?.value === 'function') {
134141
const check = maybeValidator.value as Validator<unknown>;
135142

143+
let errors: string | string[] | null | undefined;
144+
136145
if (Array.isArray(value)) {
137146
for (const key in value) {
138-
const errors = await check(value[key]);
147+
try {
148+
errors = await check(value[key]);
149+
if (errors) {
150+
valid = false;
151+
newErrors.push({
152+
path: path.concat([key]),
153+
errors:
154+
typeof errors === 'string'
155+
? [errors]
156+
: errors ?? undefined
157+
});
158+
}
159+
} catch (e) {
160+
valid = false;
161+
console.error(
162+
`Error in form validators for field "${path}":`,
163+
e
164+
);
165+
}
166+
}
167+
} else {
168+
try {
169+
errors = await check(value);
139170
if (errors) {
140171
valid = false;
141172
newErrors.push({
142-
path: path.concat([key]),
173+
path,
143174
errors:
144175
typeof errors === 'string'
145176
? [errors]
146177
: errors ?? undefined
147178
});
148179
}
149-
}
150-
} else {
151-
const errors = await check(value);
152-
if (errors) {
180+
} catch (e) {
153181
valid = false;
154-
newErrors.push({
155-
path,
156-
errors:
157-
typeof errors === 'string' ? [errors] : errors ?? undefined
158-
});
182+
console.error(
183+
`Error in form validators for field "${path}":`,
184+
e
185+
);
159186
}
160187
}
161188
}
@@ -177,24 +204,12 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
177204
}
178205
}
179206
}
180-
181-
if (!valid) {
182-
return {
183-
valid: false,
184-
posted,
185-
errors: clientErrors,
186-
data: checkData,
187-
constraints,
188-
message: undefined,
189-
id: formId
190-
};
191-
}
192207
}
193208

194209
return {
195-
valid: true,
210+
valid,
196211
posted,
197-
errors: {},
212+
errors: clientErrors,
198213
data: checkData,
199214
constraints,
200215
message: undefined,

src/routes/spa/without-zod/+page.svelte

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@
2929
}
3030
},
3131
onUpdated({ form }) {
32-
console.log('onUpdated, valid:', form.valid);
32+
console.log('onUpdated, valid:', form.valid, form);
3333
},
3434
validators: {
3535
tags: {
36-
id: (id) => (id < 3 ? 'Id must be larger than 2' : null),
36+
id: (id) =>
37+
isNaN(id) || id < 3 ? 'Id must be larger than 2' : null,
3738
name: (name) =>
38-
name.length < 2 ? 'Tags must be at least two characters' : null
39+
!name || name.length < 2
40+
? 'Tags must be at least two characters'
41+
: null
3942
}
4043
}
4144
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { message, superValidate } from '$lib/server';
2+
import { schema } from './schema';
3+
import { fail } from '@sveltejs/kit';
4+
5+
export const load = async () => {
6+
const form = await superValidate(schema);
7+
return { form };
8+
};
9+
10+
export const actions = {
11+
default: async ({ request }) => {
12+
const formData = await request.formData();
13+
console.log(formData);
14+
15+
const form = await superValidate(formData, schema);
16+
console.log('POST', form);
17+
18+
if (!form.valid) {
19+
form.message = 'Failed on server.';
20+
return fail(400, { form });
21+
}
22+
23+
return message(form, 'Posted OK!');
24+
}
25+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
6+
export let data: PageData;
7+
8+
// Missing a field, which will result in a console error.
9+
// @ts-ignore
10+
data.form.data = {};
11+
12+
const { form, errors, tainted, message, enhance } = superForm(data.form, {
13+
//dataType: 'json',
14+
validators: {
15+
age: (age) =>
16+
isNaN(age) || age < 30 ? 'At least 30 years, please!' : null,
17+
name: (name) =>
18+
name.length < 2 ? 'At least two characters, please!' : null
19+
},
20+
taintedMessage: null
21+
});
22+
</script>
23+
24+
{#if $message}<h4>{$message}</h4>{/if}
25+
26+
<form method="POST" use:enhance>
27+
<label>
28+
Age: <input name="age" type="number" bind:value={$form.age} />
29+
{#if $errors.age}<span class="invalid">{$errors.age}</span>{/if}
30+
</label>
31+
<label>
32+
Name: <input name="name" bind:value={$form.name} />
33+
{#if $errors.name}<span class="invalid">{$errors.name}</span>{/if}
34+
</label>
35+
<div>
36+
<button>Submit</button>
37+
</div>
38+
</form>
39+
40+
<style lang="scss">
41+
form {
42+
margin: 2rem 0;
43+
44+
input {
45+
background-color: #dedede;
46+
}
47+
48+
.invalid {
49+
color: crimson;
50+
}
51+
}
52+
</style>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { z } from 'zod';
2+
3+
export const schema = z.object({
4+
age: z.number().min(30),
5+
name: z.string().min(1)
6+
});

0 commit comments

Comments
 (0)