From 37b7b93ff91c87a249ff0adb2cbc3cb07b7fbf31 Mon Sep 17 00:00:00 2001 From: unloco Date: Sat, 6 Apr 2024 22:11:57 +0100 Subject: [PATCH 1/3] Add support for form arrays --- src/lib/client/superForm.ts | 30 +++++++++- src/routes/(v2)/v2/Navigation.svelte | 1 + .../(v2)/v2/multiple-forms/+page.server.ts | 58 +++++++++++++++++++ .../(v2)/v2/multiple-forms/+page.svelte | 26 +++++++++ src/routes/(v2)/v2/multiple-forms/Form.svelte | 28 +++++++++ src/routes/(v2)/v2/multiple-forms/schema.ts | 6 ++ 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/routes/(v2)/v2/multiple-forms/+page.server.ts create mode 100644 src/routes/(v2)/v2/multiple-forms/+page.svelte create mode 100644 src/routes/(v2)/v2/multiple-forms/Form.svelte create mode 100644 src/routes/(v2)/v2/multiple-forms/schema.ts diff --git a/src/lib/client/superForm.ts b/src/lib/client/superForm.ts index cc61b04d..bbc4da37 100644 --- a/src/lib/client/superForm.ts +++ b/src/lib/client/superForm.ts @@ -389,6 +389,30 @@ try { // No Storybook } +const lifeCycleCallbacks = { + onDestroy: new Set<() => void>(), + beforeNavigate: new Set<(nav: BeforeNavigate) => Promise>() +} +let componentInitialized = false; +function initLifeCycleCallbacks() { + if (componentInitialized) return; + componentInitialized = true; + + onDestroy(() => { + for (const callback of lifeCycleCallbacks.onDestroy) { + callback(); + } + lifeCycleCallbacks.onDestroy.clear(); + }); + + beforeNavigate((nav: BeforeNavigate) => { + for (const callback of lifeCycleCallbacks.beforeNavigate) { + callback(nav); + } + lifeCycleCallbacks.beforeNavigate.clear(); + }); +} + ///////////////////////////////////////////////////////////////////// /** @@ -410,6 +434,8 @@ export function superForm< // To check if a full validator is used when switching options.validators dynamically let initialValidator: FormOptions['validators'] | undefined = undefined; + initLifeCycleCallbacks(); + { if (options.legacy ?? LEGACY_MODE) { if (options.resetForm === undefined) options.resetForm = false; @@ -525,7 +551,7 @@ export function superForm< ///// From here, form is properly initialized ///// - onDestroy(() => { + lifeCycleCallbacks.onDestroy.add(() => { Unsubscriptions_unsubscribe(); NextChange_clear(); EnhancedForm_destroy(); @@ -1397,7 +1423,7 @@ export function superForm< ///// Store subscriptions /////////////////////////////////////////////////// if (browser) { - beforeNavigate(Tainted_check); + lifeCycleCallbacks.beforeNavigate.add(Tainted_check); // Need to subscribe to catch page invalidation. Unsubscriptions_add( diff --git a/src/routes/(v2)/v2/Navigation.svelte b/src/routes/(v2)/v2/Navigation.svelte index 1aeebb3f..184754e8 100644 --- a/src/routes/(v2)/v2/Navigation.svelte +++ b/src/routes/(v2)/v2/Navigation.svelte @@ -14,6 +14,7 @@ 'issue-337-checkboxes', 'issue-345', 'letters', + 'multiple-forms', 'multiple-files', 'multistep-client', 'multistep-server', diff --git a/src/routes/(v2)/v2/multiple-forms/+page.server.ts b/src/routes/(v2)/v2/multiple-forms/+page.server.ts new file mode 100644 index 00000000..6321d1ac --- /dev/null +++ b/src/routes/(v2)/v2/multiple-forms/+page.server.ts @@ -0,0 +1,58 @@ +import { zod } from '$lib/adapters/zod.js'; +import { message, superValidate } from '$lib/server/index.js'; +import { type Actions, fail } from '@sveltejs/kit'; +import { schema } from './schema.js'; + +const items = [ + { + id: 1, + label: 'One' + }, + { + id: 2, + label: 'Two' + }, + { + id: 3, + label: 'Three' + } +]; + +export const load = async () => { + const item_forms = await Promise.all( + items.map((item) => + superValidate(item, zod(schema), { + id: item.id.toString() + }) + ) + ); + + return { item_forms }; +}; + +export const actions: Actions = { + async create() { + items.push({ + id: items.length + 1, + label: (items.length + 1).toString() + }); + + return { success: true }; + }, + + async save({ request }) { + const form = await superValidate(request, zod(schema)); + + if (!form.valid) { + // Again, return { form } and things will just work. + return fail(400, { form }); + } + + const index = items.findIndex((item) => item.id === form.data.id); + if(index !== -1) { + items[index].label = form.data.label; + } + + return message(form, `Item ${form.data.id} updated`); + } +}; diff --git a/src/routes/(v2)/v2/multiple-forms/+page.svelte b/src/routes/(v2)/v2/multiple-forms/+page.svelte new file mode 100644 index 00000000..4d527efa --- /dev/null +++ b/src/routes/(v2)/v2/multiple-forms/+page.svelte @@ -0,0 +1,26 @@ + + +{#each superforms as superform (superform.formId)} +
+
+{/each} + + + +
\ No newline at end of file diff --git a/src/routes/(v2)/v2/multiple-forms/Form.svelte b/src/routes/(v2)/v2/multiple-forms/Form.svelte new file mode 100644 index 00000000..57d927ba --- /dev/null +++ b/src/routes/(v2)/v2/multiple-forms/Form.svelte @@ -0,0 +1,28 @@ + + +
+ + + + + + + + + {#if $message} +

{$message}

+ {/if} + \ No newline at end of file diff --git a/src/routes/(v2)/v2/multiple-forms/schema.ts b/src/routes/(v2)/v2/multiple-forms/schema.ts new file mode 100644 index 00000000..97ada301 --- /dev/null +++ b/src/routes/(v2)/v2/multiple-forms/schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const schema = z.object({ + id: z.number(), + label: z.string() +}); From 2a70cbbdc2c59c95951557491e55c04529684c30 Mon Sep 17 00:00:00 2001 From: unloco Date: Thu, 11 Apr 2024 22:07:03 +0100 Subject: [PATCH 2/3] Fix each key --- src/routes/(v2)/v2/multiple-forms/+page.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/(v2)/v2/multiple-forms/+page.svelte b/src/routes/(v2)/v2/multiple-forms/+page.svelte index 4d527efa..932b88c3 100644 --- a/src/routes/(v2)/v2/multiple-forms/+page.svelte +++ b/src/routes/(v2)/v2/multiple-forms/+page.svelte @@ -3,6 +3,7 @@ import { superForm } from '$lib/client/index.js'; import Form from './Form.svelte'; import { enhance } from '$app/forms'; + import { get } from 'svelte/store'; let { data @@ -16,7 +17,7 @@ }))); -{#each superforms as superform (superform.formId)} +{#each superforms as superform (get(superform.formId))}

{/each} From a3e6212165cf76e213ad401da4975523732202a9 Mon Sep 17 00:00:00 2001 From: unloco Date: Thu, 11 Apr 2024 22:10:36 +0100 Subject: [PATCH 3/3] Avoid reassigning form.id if not different than options.id When form.id is reassigned during Svelte's derived phase, ERR_SVELTE_UNSAFE_MUTATION is thrown. It's because it can lead to unexpected errors and possibly cause infinite loops --- src/lib/client/superForm.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/client/superForm.ts b/src/lib/client/superForm.ts index bbc4da37..8712b00c 100644 --- a/src/lib/client/superForm.ts +++ b/src/lib/client/superForm.ts @@ -491,7 +491,12 @@ export function superForm< form = form as SuperValidated; // Assign options.id to form, if it exists - const _initialFormId = (form.id = options.id ?? form.id); + // Avoid reassigning id in runes mode to avoid ERR_SVELTE_UNSAFE_MUTATION + // TODO: detect runes mode if possible & warn if id is different + let _initialFormId = form.id + if (options.id && form.id !== options.id) { + _initialFormId = options.id; + } const _currentPage = get(page) ?? (STORYBOOK_MODE ? {} : undefined); // Check multiple id's