Skip to content

Commit 16ca711

Browse files
authored
Merge pull request #170 from x0k/fix-169-2
Preserve array keys by injecting default values instead of state replacing
2 parents 59d5b91 + c32feb6 commit 16ca711

File tree

6 files changed

+91
-14
lines changed

6 files changed

+91
-14
lines changed

.changeset/social-walls-post.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/form": patch
3+
---
4+
5+
Preserve array keys by injecting default values instead of state replacing

packages/form/src/core/schema.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,54 @@ export function isSubSchemasArrayKey(key: string): key is SubSchemasArrayKey {
171171
export function isSubSchemasRecordKey(key: string): key is SubSchemasRecordKey {
172172
return SET_OF_RECORDS_OF_SUB_SCHEMAS.has(key as SubSchemasRecordKey);
173173
}
174+
175+
export const UNCHANGED = Symbol("unchanged");
176+
177+
export function reconcileSchemaValues(
178+
target: SchemaValue | undefined,
179+
source: SchemaValue | undefined
180+
): SchemaValue | undefined | typeof UNCHANGED {
181+
if (target === source) {
182+
return UNCHANGED;
183+
}
184+
if (typeof target === "object" && typeof source === "object") {
185+
const isTArr = Array.isArray(target);
186+
const isSArr = Array.isArray(source);
187+
if (isTArr && isSArr) {
188+
const l = Math.min(target.length, source.length);
189+
let i = 0;
190+
for (; i < l; i++) {
191+
const v = reconcileSchemaValues(target[i], source[i]);
192+
if (v !== UNCHANGED) {
193+
target[i] = v;
194+
}
195+
}
196+
for (; i < source.length; i++) {
197+
target.push(source[i]);
198+
}
199+
target.splice(source.length);
200+
return UNCHANGED;
201+
}
202+
if (!isTArr && !isSArr && target !== null && source !== null) {
203+
const tKeys = Object.keys(target);
204+
let l = tKeys.length;
205+
for (let i = 0; i < l; i++) {
206+
const key = tKeys[i]!;
207+
if (!(key in source)) {
208+
delete target[key];
209+
}
210+
}
211+
const sKeys = Object.keys(source);
212+
l = sKeys.length;
213+
for (let i = 0; i < l; i++) {
214+
const key = sKeys[i]!;
215+
const v = reconcileSchemaValues(target[key], source[key]);
216+
if (v !== UNCHANGED) {
217+
target[key] = v;
218+
}
219+
}
220+
return UNCHANGED;
221+
}
222+
}
223+
return source;
224+
}

packages/form/src/fields/object/context.svelte.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getContext, setContext, untrack } from "svelte";
1+
import { getContext, setContext } from "svelte";
22

33
import {
44
getDefaultValueForType,
@@ -31,6 +31,7 @@ import {
3131
retrieveUiOption,
3232
uiTitleOption,
3333
type Translate,
34+
markSchemaChange,
3435
} from "@/form/index.js";
3536

3637
import {
@@ -86,20 +87,10 @@ export function createObjectContext<V extends Validator>(
8687
return lastSchemaProperties;
8788
});
8889

89-
// NOTE: This code should populate `defaults` for properties from
90-
// `dependencies` before new `fields` will populate their `defaults`.
90+
// NOTE: `defaults` population
9191
$effect(() => {
9292
schemaProperties;
93-
setValue(
94-
untrack(
95-
() =>
96-
getDefaultFieldState(
97-
ctx,
98-
retrievedSchema,
99-
value()
100-
) as SchemaObjectValue
101-
)
102-
);
93+
markSchemaChange(ctx);
10394
});
10495

10596
const uiOption: UiOption = (opt) => retrieveUiOption(ctx, config(), opt);

packages/form/src/form/context/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface FormInternalContext<V extends Validator>
3232
Readonly<Required<IdOptions>> {
3333
value: FormValue;
3434
isChanged: boolean;
35+
readonly markSchemaChange: () => void;
3536
readonly rootId: Id;
3637
readonly fieldsValidationMode: number;
3738
readonly isSubmitted: boolean;

packages/form/src/form/context/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,9 @@ export function getDefaultFieldState<V extends Validator>(
8888
) {
8989
return ctx.merger.mergeFormDataAndSchemaDefaults(formData, schema);
9090
}
91+
92+
export function markSchemaChange<V extends Validator>(
93+
ctx: FormInternalContext<V>
94+
) {
95+
ctx.markSchemaChange();
96+
}

packages/form/src/form/create-form.svelte.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
type TasksCombinator,
1212
type FailedTask,
1313
} from "@/lib/task.svelte.js";
14-
import type { Schema, Validator } from "@/core/index.js";
14+
import {
15+
UNCHANGED,
16+
reconcileSchemaValues,
17+
type Schema,
18+
type Validator,
19+
} from "@/core/index.js";
1520

1621
import {
1722
type ValidationError,
@@ -541,9 +546,27 @@ export function createForm<T, V extends Validator>(
541546
},
542547
submitHandler,
543548
resetHandler,
549+
markSchemaChange() {
550+
if (isDefaultsInjectionQueued) return;
551+
isDefaultsInjectionQueued = true;
552+
queueMicrotask(injectSchemaDefaults);
553+
},
544554
};
545555
const fieldTypeResolver = $derived(options.resolver(context));
546556

557+
let isDefaultsInjectionQueued = false;
558+
function injectSchemaDefaults() {
559+
isDefaultsInjectionQueued = false;
560+
const nextValue = merger.mergeFormDataAndSchemaDefaults(
561+
valueRef.current,
562+
options.schema
563+
);
564+
const change = reconcileSchemaValues(valueRef.current, nextValue);
565+
if (change !== UNCHANGED) {
566+
valueRef.current = change;
567+
}
568+
}
569+
547570
return {
548571
context,
549572
[FORM_CONTEXT]: context,

0 commit comments

Comments
 (0)