Skip to content

Commit 716c6a7

Browse files
committed
Client-side validation wasn't resetted properly, when a component containing a form was destroyed and mounted again.
1 parent 26662d9 commit 716c6a7

File tree

9 files changed

+239
-23
lines changed

9 files changed

+239
-23
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ Headlines: Added, Changed, Deprecated, Removed, Fixed, Security
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [1.5.4] - 2023-08-17
99

1010
### Fixed
1111

12+
- Client-side validation wasn't resetted properly, when a component containing a form was destroyed and mounted again.
1213
- Removed debug statement left from 1.5.3
1314

1415
## [1.5.3] - 2023-08-16
@@ -23,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2324

2425
### Fixed
2526

26-
- Forms in components weren't resetted properly when destroyed and created again.
27+
- Forms in components weren't resetted properly when destroyed and mounted again.
2728

2829
## [1.5.1] - 2023-08-09
2930

src/lib/client/index.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,10 @@ export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
255255
export type EnhancedForm<T extends AnyZodObject, M = any> = SuperForm<T, M>;
256256

257257
const formIds = new WeakMap<Page, Set<string | undefined>>();
258-
259-
// Track ActionData, so it won't be applied twice for componentized forms.
260-
const postedActionData = new WeakSet<object>();
258+
const initializedForms = new WeakMap<
259+
object,
260+
SuperValidated<ZodValidation<AnyZodObject>, unknown>
261+
>();
261262

262263
function multipleFormIdError(id: string | undefined) {
263264
return (
@@ -341,8 +342,22 @@ export function superForm<
341342
}
342343
}
343344

344-
// Need to clone the validation data, in case it's used to populate multiple forms.
345-
const initialForm = clone(form);
345+
// Need to clone the form data, in case it's used to populate multiple forms and in components
346+
// that are mounted and destroyed multiple times.
347+
if (!initializedForms.has(form)) {
348+
initializedForms.set(form, clone(form));
349+
}
350+
const initialForm = initializedForms.get(form) as SuperValidated<T, M>;
351+
352+
if (typeof initialForm.valid !== 'boolean') {
353+
throw new SuperFormError(
354+
'A non-validation object was passed to superForm. ' +
355+
'It should be an object of type SuperValidated, usually returned from superValidate.'
356+
);
357+
}
358+
359+
// Restore the initial data, in case of a mounted/destroyed component.
360+
let _initiallyCloned = false;
346361

347362
// Detect if a form is posted without JavaScript.
348363
const postedData = _currentPage.form;
@@ -351,9 +366,9 @@ export function superForm<
351366
for (const postedForm of Context_findValidationForms(
352367
postedData
353368
).reverse()) {
354-
if (postedForm.id === _formId && !postedActionData.has(postedForm)) {
369+
if (postedForm.id === _formId && !initializedForms.has(postedForm)) {
355370
// Prevent multiple "posting" that can happen when components are recreated.
356-
postedActionData.add(postedData);
371+
initializedForms.set(postedData, postedData);
357372

358373
const pageDataForm = form as SuperValidated<T, M>;
359374
form = postedForm as SuperValidated<T, M>;
@@ -364,21 +379,17 @@ export function superForm<
364379
(options.resetForm === true || options.resetForm())
365380
) {
366381
form = clone(pageDataForm);
367-
form.message = postedForm.message;
382+
form.message = clone(postedForm.message);
383+
_initiallyCloned = true;
368384
}
369385
break;
370386
}
371387
}
372388
}
373389

374-
const form2 = form as SuperValidated<T, M>;
390+
if (!_initiallyCloned) form = clone(initialForm);
375391

376-
if (typeof initialForm.valid !== 'boolean') {
377-
throw new SuperFormError(
378-
'A non-validation object was passed to superForm. ' +
379-
'It should be an object of type SuperValidated, usually returned from superValidate.'
380-
);
381-
}
392+
const form2 = form as SuperValidated<T, M>;
382393

383394
// Underlying store for Errors
384395
const _errors = writable(form2.errors);
@@ -833,12 +844,12 @@ export function superForm<
833844
const forms = Context_findValidationForms(actionData);
834845
for (const newForm of forms) {
835846
//console.log('🚀~ ActionData ~ newForm:', newForm.id);
836-
if (newForm.id !== _formId || postedActionData.has(newForm)) {
847+
if (newForm.id !== _formId || initializedForms.has(newForm)) {
837848
continue;
838849
}
839850

840851
// Prevent multiple "posting" that can happen when components are recreated.
841-
postedActionData.add(newForm);
852+
initializedForms.set(newForm, newForm);
842853

843854
await Form_updateFromValidation(
844855
newForm as SuperValidated<T, M>,
@@ -851,7 +862,9 @@ export function superForm<
851862
const forms = Context_findValidationForms(pageUpdate.data);
852863
for (const newForm of forms) {
853864
//console.log('🚀 ~ PageData ~ newForm:', newForm.id);
854-
if (newForm.id !== _formId) continue;
865+
if (newForm.id !== _formId || initializedForms.has(newForm)) {
866+
continue;
867+
}
855868

856869
rebind(newForm as SuperValidated<T, M>, untaint);
857870
}

src/routes/Navigation.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
<a href="/tests/spa-values-disappearing">SPA onUpdate</a>
2626
<a href="/posted">Posted store</a>
2727
<a href="/tests/flash-onerror">Flash onError</a>
28+
<a href="/tests/reset-component">Reset component 1</a>
29+
<a href="/tests/reset-component-2">Reset component 2</a>
2830
</nav>
2931

3032
<style lang="scss">
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { superValidate, message } from '$lib/server';
2+
import { registerSchema, profileSchema } from './schema';
3+
import { fail } from '@sveltejs/kit';
4+
import type { Actions } from './$types';
5+
6+
export const load = async () => {
7+
// Server API:
8+
const regForm = await superValidate(registerSchema);
9+
const profileForm = await superValidate(profileSchema);
10+
11+
return { regForm, profileForm };
12+
};
13+
14+
export const actions = {
15+
register: async ({ request }) => {
16+
const regForm = await superValidate(request, registerSchema);
17+
18+
console.log('register', regForm);
19+
20+
if (!regForm.valid) return fail(400, { regForm });
21+
22+
return message(regForm, {
23+
text: 'Form "register" posted successfully!'
24+
});
25+
},
26+
27+
edit: async ({ request }) => {
28+
const profileForm = await superValidate(request, profileSchema);
29+
30+
console.log('edit', profileForm);
31+
32+
if (!profileForm.valid) return fail(400, { profileForm });
33+
34+
return message(profileForm, {
35+
text: 'Form "profile" posted successfully!'
36+
});
37+
}
38+
} satisfies Actions;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script lang="ts">
2+
import { registerSchema } from './schema';
3+
4+
import SuperForm from './Form.svelte';
5+
import TextField from './TextField.svelte';
6+
import type { PageData } from './$types';
7+
8+
export let data: PageData;
9+
10+
let visible = true;
11+
</script>
12+
13+
<h3>Multiple componentized Forms</h3>
14+
<hr />
15+
16+
<h4>Register Form</h4>
17+
18+
<button on:click={() => (visible = !visible)}>Toggle</button>
19+
20+
{#if visible}
21+
<!-- SuperForm with dataType 'form' -->
22+
<SuperForm
23+
action="?/register"
24+
schema={registerSchema}
25+
data={data.regForm}
26+
invalidateAll={false}
27+
let:form
28+
let:message
29+
>
30+
{#if message}
31+
<div
32+
class="status"
33+
class:error={message.status >= 400}
34+
class:success={!message.status || message.status < 300}
35+
>
36+
{message.text}
37+
</div>
38+
{/if}
39+
40+
<TextField type="text" {form} field="name" label="Name" />
41+
<TextField type="text" {form} field="email" label="E-Mail" />
42+
<p>
43+
<button type="submit">submit</button>
44+
</p>
45+
</SuperForm>
46+
{/if}
47+
48+
<style>
49+
.status {
50+
color: white;
51+
padding: 6px;
52+
padding-left: 8px;
53+
border-radius: 2px;
54+
font-weight: 500;
55+
margin-block: 0.75em;
56+
}
57+
58+
.status.success {
59+
background-color: seagreen;
60+
}
61+
62+
.status.error {
63+
background-color: #ff2a02;
64+
}
65+
66+
hr {
67+
margin: 2rem 0;
68+
}
69+
</style>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script lang="ts">
2+
import SuperDebug from '$lib/client/SuperDebug.svelte';
3+
4+
import { superForm } from '$lib/client';
5+
export let data;
6+
export let schema: any | undefined = undefined;
7+
export let dataType: 'form' | 'json' = 'form';
8+
export let invalidateAll; // set to false to keep form data using muliple forms on a page
9+
10+
export const _form = superForm(data, {
11+
dataType: dataType,
12+
validators: schema ? schema : undefined,
13+
invalidateAll: invalidateAll,
14+
onError({ result }) {
15+
$message = {
16+
text: result?.error?.message,
17+
status: 500
18+
};
19+
},
20+
onUpdated({ form }) {
21+
if (form.valid) {
22+
// Successful post! Do some more client-side stuff.
23+
}
24+
}
25+
});
26+
27+
const { form, message, tainted, delayed, errors, allErrors, enhance } =
28+
_form;
29+
</script>
30+
31+
<form method="POST" use:enhance {...$$restProps}>
32+
<slot
33+
form={_form}
34+
message={$message}
35+
errors={$errors}
36+
allErrors={$allErrors}
37+
delayed={$delayed}
38+
/>
39+
</form>
40+
41+
<SuperDebug data={$form} />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<script lang="ts">
2+
import { formFieldProxy } from '$lib/client';
3+
4+
let _class = '';
5+
6+
export { _class as class };
7+
export let label: string | undefined = undefined;
8+
export let field: string;
9+
export let form;
10+
11+
const { value, errors, constraints } = formFieldProxy(form, field);
12+
</script>
13+
14+
{#if label !== undefined}
15+
<label class="label" for={field}>{label}</label>
16+
{/if}
17+
<div class="control">
18+
<input
19+
class={'input ' + _class}
20+
name={field}
21+
aria-invalid={$errors ? 'true' : undefined}
22+
placeholder=""
23+
bind:value={$value}
24+
{...$$restProps}
25+
/>
26+
</div>
27+
{#if $errors}
28+
<p class="help is-danger">{$errors}</p>
29+
{/if}
30+
31+
<style>
32+
.is-danger {
33+
color: red;
34+
}
35+
36+
input {
37+
background-color: #e7e7e7;
38+
}
39+
40+
input:not(:placeholder-shown):invalid {
41+
box-shadow: inset 0px 0px 3px 1px #f00;
42+
}
43+
</style>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod';
2+
3+
export const registerSchema = z.object({
4+
name: z.string().min(2),
5+
email: z.string().email()
6+
});
7+
8+
export const profileSchema = z.object({
9+
name: z.string().min(2),
10+
age: z.coerce.number().gte(16).default(18)
11+
});

src/routes/tests/reset-component/Form.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
<script>
22
import { superForm, superValidateSync } from '$lib/client';
33
import { z } from 'zod';
4-
import { onDestroy } from 'svelte';
54
import SuperDebug from '$lib/client/SuperDebug.svelte';
65
import { page } from '$app/stores';
7-
import { get } from 'svelte/store';
86
97
const schema = z.object({
108
id: z.number().min(1).max(255).default(1),
119
name: z
1210
.string()
13-
.min(1, 'Name must contain at least 1 character')
11+
.min(2, 'Name must contain at least 2 characters')
1412
.max(255, 'Name must not exceed 255 characters')
1513
.default('')
1614
});

0 commit comments

Comments
 (0)