Skip to content

Commit c204d6e

Browse files
committed
[sveltekit] Consider idPrefix as form id
1 parent 058d06e commit c204d6e

File tree

9 files changed

+97
-24
lines changed

9 files changed

+97
-24
lines changed

.changeset/light-apples-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/sveltekit": major
3+
---
4+
5+
Consider `idPrefix` as form id

packages/sveltekit/src/lib/client/form.svelte.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { isRecord } from '@sjsf/form/lib/object';
3-
import { createForm, type Schema, type FormOptions, updateErrors } from '@sjsf/form';
3+
import {
4+
createForm,
5+
type Schema,
6+
type FormOptions,
7+
updateErrors,
8+
DEFAULT_ID_PREFIX
9+
} from '@sjsf/form';
410

511
import { page } from '$app/state';
612

@@ -24,31 +30,34 @@ export type SvelteKitFormOptions<
2430
> = Omit<FormOptions<T>, 'schema' | ToOmit> & SchemaOption<SendSchema>;
2531

2632
function initialFormData<Meta extends SvelteKitFormMeta<any, any, string, any>>(
27-
meta: Meta
33+
meta: Meta,
34+
idPrefix: string
2835
): InitialFormData<Meta['__formValue'], Meta['__sendSchema']> | undefined {
2936
if (isRecord(page.form)) {
3037
const validationData = page.form[meta.name] as
3138
| ValidatedFormData<Meta['__sendData']>
3239
| undefined;
33-
if (validationData !== undefined) {
34-
return validationData.isValid
35-
? page.data[meta.name]
36-
: {
37-
...page.data[meta.name],
38-
initialValue: validationData.data,
39-
initialErrors: validationData.errors
40-
};
40+
if (
41+
validationData !== undefined &&
42+
validationData.idPrefix === idPrefix &&
43+
!validationData.isValid
44+
) {
45+
return {
46+
...page.data[meta.name],
47+
initialValue: validationData.data,
48+
initialErrors: validationData.errors
49+
};
4150
}
42-
} else {
43-
return page.data[meta.name];
4451
}
52+
return page.data[meta.name];
4553
}
4654

4755
export function createSvelteKitForm<
4856
Meta extends SvelteKitFormMeta<any, any, string, any>,
4957
Options extends SvelteKitFormOptions<Meta['__formValue'], Meta['__sendSchema']>
5058
>(meta: Meta, options: Options) {
51-
const defaults = initialFormData(meta) ?? {};
59+
const formIdPrefix = $derived(options.idPrefix ?? DEFAULT_ID_PREFIX);
60+
const defaults = initialFormData(meta, formIdPrefix) ?? {};
5261
const form = createForm(
5362
new Proxy(options, {
5463
has(target, p) {
@@ -69,7 +78,7 @@ export function createSvelteKitForm<
6978
const validationData = page.form[meta.name] as
7079
| ValidatedFormData<Meta['__sendData']>
7180
| undefined;
72-
if (validationData === undefined) {
81+
if (validationData === undefined || formIdPrefix !== validationData.idPrefix) {
7382
return;
7483
}
7584
if (validationData.sendData) {

packages/sveltekit/src/lib/client/request.svelte.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import { createTask, type TaskOptions } from '@sjsf/form/lib/task.svelte';
66
import { applyAction, deserialize } from '$app/forms';
77
import { invalidateAll } from '$app/navigation';
88

9-
import { FORM_DATA_FILE_PREFIX, JSON_CHUNKS_KEY } from '../model.js';
9+
import { FORM_DATA_FILE_PREFIX, ID_PREFIX_KEY, JSON_CHUNKS_KEY } from '../model.js';
1010

1111
import type { SvelteKitFormMeta } from './meta.js';
12+
import { DEFAULT_ID_PREFIX } from '@sjsf/form';
1213

1314
export type SveltekitRequestOptions<ActionData, V> = Omit<
1415
TaskOptions<[V, SubmitEvent], ActionResult<NonNullable<ActionData>>, unknown>,
1516
'execute'
1617
> & {
18+
/** @default DEFAULT_ID_PREFIX */
19+
idPrefix?: string;
1720
/** By default, handles conversion of `File` */
1821
createReplacer?: (formData: FormData) => (key: string, value: any) => any;
1922
/** @default 500000 */
@@ -74,6 +77,7 @@ export function createSvelteKitRequest<Meta extends SvelteKitFormMeta<any, any,
7477
}
7578

7679
const formData = new FormData();
80+
formData.append(ID_PREFIX_KEY, options.idPrefix ?? DEFAULT_ID_PREFIX);
7781
for (const chunk of chunks(JSON.stringify(data, createReplacer(formData)), jsonChunkSize)) {
7882
formData.append(JSON_CHUNKS_KEY, chunk);
7983
}

packages/sveltekit/src/lib/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ValidationError
88
} from '@sjsf/form';
99

10+
export const ID_PREFIX_KEY = '__sjsf_sveltekit_id_prefix';
1011
export const JSON_CHUNKS_KEY = '__sjsf_sveltekit_json_chunks';
1112
export const FORM_DATA_FILE_PREFIX = '__sjsf_sveltekit_file__';
1213

@@ -45,6 +46,7 @@ export type InitialFormData<T, SendSchema extends boolean> = SerializableOptiona
4546
};
4647

4748
export interface ValidatedFormData<SendData extends boolean> {
49+
idPrefix: string;
4850
isValid: boolean;
4951
sendData?: SendData;
5052
data: SendData extends true ? SchemaValue | undefined : undefined;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { DEFAULT_ID_PREFIX } from '@sjsf/form';
2+
import { escapeRegex } from '@sjsf/form/lib/reg-exp';
3+
4+
export interface IdPrefixParserOptions {
5+
formData: FormData;
6+
idSeparator: string;
7+
idIndexSeparator: string;
8+
idPseudoSeparator: string;
9+
}
10+
11+
export function parseIdPrefix({
12+
formData,
13+
idIndexSeparator,
14+
idPseudoSeparator,
15+
idSeparator
16+
}: IdPrefixParserOptions) {
17+
const first = formData.keys().next().value;
18+
if (first === undefined) {
19+
return DEFAULT_ID_PREFIX;
20+
}
21+
const escapedIdSeparator = escapeRegex(idSeparator);
22+
const escapedIndexSeparator = escapeRegex(idIndexSeparator);
23+
const escapedPseudoSeparator = escapeRegex(idPseudoSeparator);
24+
const regExp = new RegExp(
25+
`^(.+?)($|${escapedIdSeparator}|${escapedPseudoSeparator}|${escapedIndexSeparator})`
26+
);
27+
const match = first.match(regExp);
28+
if (match === null) {
29+
throw new Error(`Unable to extract ID prefix from "${first}" form data key`);
30+
}
31+
return match[1];
32+
}

packages/sveltekit/src/lib/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './entry.js';
22
export * from './server.js';
33
export * from './convert-form-data-entries.js';
44
export * from './schema-value-parser.js';
5+
export * from './id-prefix-parser.js';

packages/sveltekit/src/lib/server/server.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest';
22
import { createFormValidator } from '@sjsf/ajv8-validator';
33
import { createFormMerger } from '@sjsf/form/mergers/modern';
4+
import { createFormIdBuilder } from '@sjsf/form/id-builders/modern';
45

56
import { createFormHandler } from './server.js';
67

@@ -9,6 +10,7 @@ describe('makeFormDataParser', () => {
910
const formData = new FormData();
1011
formData.append('root', new File(['hello'], 'test.txt', { type: 'text/plain' }));
1112
const parse = createFormHandler({
13+
idBuilder: createFormIdBuilder,
1214
validator: createFormValidator,
1315
merger: createFormMerger,
1416
schema: {
@@ -24,6 +26,7 @@ describe('makeFormDataParser', () => {
2426
const formData = new FormData();
2527
formData.append('root', new File([], '', { type: '' }));
2628
const parse = createFormHandler({
29+
idBuilder: createFormIdBuilder,
2730
validator: createFormValidator,
2831
merger: createFormMerger,
2932
schema: {

packages/sveltekit/src/lib/server/server.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ import {
1515
type FormMerger,
1616
type UiOptionsRegistry,
1717
type Creatable,
18-
create
18+
create,
19+
type FormIdBuilder,
20+
type IdBuilderFactoryOptions
1921
} from '@sjsf/form';
2022
import {
2123
type IdOptions,
2224
DEFAULT_ID_SEPARATOR,
23-
DEFAULT_ID_PSEUDO_SEPARATOR,
24-
createFormIdBuilder
25+
DEFAULT_ID_PSEUDO_SEPARATOR
2526
} from '@sjsf/form/id-builders/legacy';
2627
import { DEFAULT_INDEX_SEPARATOR } from '@sjsf/form/id-builders/modern';
2728

2829
import {
2930
FORM_DATA_FILE_PREFIX,
31+
ID_PREFIX_KEY,
3032
JSON_CHUNKS_KEY,
3133
type InitialFormData,
3234
type SerializableOptionalFormOptions,
@@ -40,6 +42,7 @@ import {
4042
type UnknownEntryConverter
4143
} from './convert-form-data-entries.js';
4244
import type { EntriesConverter } from './entry.js';
45+
import { parseIdPrefix } from './id-prefix-parser.js';
4346

4447
export type InitFormOptions<T, SendSchema extends boolean> = SerializableOptionalFormOptions<T> & {
4548
sendSchema?: SendSchema;
@@ -70,6 +73,7 @@ export interface FormHandlerOptions<SendData extends boolean> extends IdOptions
7073
uiSchema?: UiSchemaRoot;
7174
uiOptionsRegistry?: UiOptionsRegistry;
7275
idIndexSeparator?: string;
76+
idBuilder: Creatable<FormIdBuilder, IdBuilderFactoryOptions>;
7377
validator: Creatable<Validator, ValidatorFactoryOptions>;
7478
merger: Creatable<FormMerger, MergerFactoryOptions>;
7579
createEntriesConverter?: Creatable<
@@ -96,6 +100,7 @@ export function createFormHandler<SendData extends boolean>({
96100
schema,
97101
uiSchema = {},
98102
uiOptionsRegistry = {},
103+
idBuilder: createIdBuilder,
99104
merger: createMerger,
100105
validator: createValidator,
101106
createEntriesConverter = createFormDataEntriesConverter,
@@ -107,10 +112,11 @@ export function createFormHandler<SendData extends boolean>({
107112
sendData,
108113
createReviver = createDefaultReviver
109114
}: FormHandlerOptions<SendData>) {
110-
const idBuilder = createFormIdBuilder({
115+
const idBuilder = create(createIdBuilder, {
111116
idPrefix,
112-
idSeparator,
113-
idPseudoSeparator
117+
schema,
118+
uiOptionsRegistry,
119+
uiSchema
114120
});
115121
const validator: Validator = create(createValidator, {
116122
schema,
@@ -142,6 +148,14 @@ export function createFormHandler<SendData extends boolean>({
142148
(errors: ValidationError[]) => ValidatedFormData<SendData>
143149
]
144150
> => {
151+
const idPrefix = formData.has(ID_PREFIX_KEY)
152+
? (formData.get(ID_PREFIX_KEY) as string)
153+
: parseIdPrefix({
154+
formData,
155+
idIndexSeparator,
156+
idPseudoSeparator,
157+
idSeparator
158+
});
145159
const data = formData.has(JSON_CHUNKS_KEY)
146160
? JSON.parse(formData.getAll(JSON_CHUNKS_KEY).join(''), createReviver(formData))
147161
: await parseSchemaValue(signal, {
@@ -163,11 +177,12 @@ export function createFormHandler<SendData extends boolean>({
163177
: [];
164178
function validated(errors: ValidationError[]) {
165179
return {
180+
idPrefix,
166181
isValid: errors.length === 0,
167-
sendData: sendData ? data : undefined,
168-
data: data as FormValue,
182+
sendData,
183+
data: (sendData ? data : undefined) as SendData extends true ? FormValue : undefined,
169184
errors
170-
} as ValidatedFormData<SendData>;
185+
} satisfies ValidatedFormData<SendData>;
171186
}
172187
return [validated(errors), data, validated];
173188
};

packages/sveltekit/src/routes/native-form.ssr.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe.skip('native form SSR', () => {
5959
},
6060
form: {
6161
form: {
62+
idPrefix: 'root',
6263
isValid: false,
6364
sendData: true,
6465
data: 'validated value',
@@ -92,6 +93,7 @@ describe.skip('native form SSR', () => {
9293
},
9394
form: {
9495
form: {
96+
idPrefix: 'root',
9597
isValid: true,
9698
sendData: true,
9799
data: 'validated value',

0 commit comments

Comments
 (0)