Skip to content

Commit 43f6eeb

Browse files
authored
Merge pull request #180 from x0k/keyed-array-access
Keyed array access
2 parents 0c6c584 + 13b2663 commit 43f6eeb

File tree

11 files changed

+242
-11
lines changed

11 files changed

+242
-11
lines changed

.changeset/purple-facts-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sjsf/form": minor
3+
---
4+
5+
Add `keyedArraysMap` form option

apps/docs2/src/content/docs/form/options.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export interface FormOptions<T, V extends Validator>
5050
idPseudoSeparator?: string;
5151
//
5252
value?: [() => T, (v: T) => void];
53-
initialValue?: Partial<T>;
53+
initialValue?: InitialValue<T>;
5454
initialErrors?: InitialErrors<V>;
5555
/**
5656
* @default waitPrevious
@@ -143,5 +143,6 @@ export interface FormOptions<T, V extends Validator>
143143
*/
144144
onReset?: (e: Event) => void;
145145
schedulerYield?: SchedulerYield;
146+
keyedArraysMap?: KeyedArraysMap;
146147
}
147148
```

apps/docs2/src/content/docs/form/state.mdx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,48 @@ export interface FormState<T, V extends Validator> {
3838
submit(e: SubmitEvent): void;
3939
reset(e: Event): void;
4040
}
41-
```
41+
```
42+
43+
## Direct modification of form state
44+
45+
If you are using a [controlled form](../../guides/quickstart/#controlled-form),
46+
you should consider the following aspects:
47+
48+
### Initialization
49+
50+
It is recommended to initialize the state as follows:
51+
52+
```ts
53+
let value = $state(
54+
merger.mergeFormDataAndSchemaDefaults(initialValue, schema)
55+
);
56+
```
57+
58+
### Arrays
59+
60+
To modify arrays, use one of the following methods:
61+
62+
1. Reassign
63+
64+
```ts
65+
value.array = value.array.concat(123)
66+
```
67+
68+
2. Use `KeyedArray` API
69+
70+
```ts
71+
import { createForm, type KeyedArraysMap } from "@sjsf/form";
72+
73+
const keyedArraysMap: KeyedArraysMap = new WeakMap()
74+
let value = $state({ array: [] })
75+
const form = createForm({
76+
keyedArraysMap,
77+
value: [() => value, (v) => (value = v)],
78+
// ...otherOptions
79+
})
80+
81+
const api = keyedArraysMap.get(value.array)
82+
if (api) {
83+
api.push(123)
84+
}
85+
```

apps/docs2/src/content/docs/guides/quickstart.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ so here is a list of situations in which this approach can be used
105105

106106
:::
107107

108+
:::caution
109+
110+
Before using, please read the section - [Direct modification of form state](../../form/state/#direct-modification-of-form-state)
111+
112+
:::
113+
108114
<Code code={controlledFormCode} lang="svelte" />
109115

110116
<FormCard>

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getContext, setContext } from "svelte";
22

3-
import { createKeyedArray } from "@/lib/keyed-array.svelte.js";
43
import {
54
getDefaultValueForType,
65
getSimpleSchemaType,
@@ -14,6 +13,7 @@ import {
1413
import {
1514
AFTER_SUBMITTED,
1615
createChildId,
16+
createKeyedArrayDeriver,
1717
getDefaultFieldState,
1818
getErrors,
1919
ON_ARRAY_CHANGE,
@@ -73,6 +73,7 @@ function createItemsAPI<V extends Validator>(
7373
ctx: FormInternalContext<V>,
7474
config: () => Config,
7575
value: () => SchemaArrayValue | undefined,
76+
setValue: (v: SchemaArrayValue) => void,
7677
itemSchema: () => Schema | undefined
7778
) {
7879
function validate() {
@@ -84,7 +85,7 @@ function createItemsAPI<V extends Validator>(
8485
}
8586
const uiOption: UiOption = (opt) => retrieveUiOption(ctx, config(), opt);
8687

87-
const keyedArray = createKeyedArray(() => value() ?? []);
88+
const keyedArray = $derived.by(createKeyedArrayDeriver(ctx, value, setValue));
8889

8990
const errors = $derived(getErrors(ctx, config().id));
9091

@@ -176,9 +177,7 @@ export function createArrayContext<V extends Validator>(
176177
ctx: FormInternalContext<V>,
177178
config: () => Config,
178179
value: () => SchemaArrayValue | undefined,
179-
// NOTE: It looks like the `undefined` value is always replaced by an array
180-
// when calculating default values, so this is unnecessary
181-
_: (v: SchemaArrayValue) => void
180+
setValue: (v: SchemaArrayValue) => void
182181
): ArrayContext<V> {
183182
const itemSchema: Schema = $derived.by(() => {
184183
const {
@@ -187,7 +186,7 @@ export function createArrayContext<V extends Validator>(
187186
return isSchemaObjectValue(items) ? items : {};
188187
});
189188

190-
const api = createItemsAPI(ctx, config, value, () => itemSchema);
189+
const api = createItemsAPI(ctx, config, value, setValue, () => itemSchema);
191190

192191
const itemUiSchema = $derived.by(() => {
193192
const {
@@ -267,7 +266,13 @@ export function createTupleContext<V extends Validator>(
267266
return isSchemaObjectValue(additionalItems) ? additionalItems : undefined;
268267
});
269268

270-
const api = createItemsAPI(ctx, config, value, () => schemaAdditionalItems);
269+
const api = createItemsAPI(
270+
ctx,
271+
config,
272+
value,
273+
setValue,
274+
() => schemaAdditionalItems
275+
);
271276

272277
const canAdd = $derived.by(
273278
createCanAdd(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
SimpleKeyedArray,
3+
type KeyedArray2,
4+
} from "@/lib/keyed-array.svelte.js";
5+
import type { SchemaArrayValue, Validator } from "@/core/index.js";
6+
7+
import type { FieldValue } from "../model.js";
8+
9+
import type { FormInternalContext } from "./context.js";
10+
11+
class VirtualKeyedArray implements KeyedArray2<number, FieldValue> {
12+
constructor(protected readonly setValue: (v: SchemaArrayValue) => void) {}
13+
14+
key(): number {
15+
throw new Error(
16+
'Method "key" cannot be called on "VirtualKeyedArray" instance'
17+
);
18+
}
19+
20+
push(value: FieldValue): void {
21+
this.setValue([value]);
22+
}
23+
24+
swap(): void {
25+
throw new Error(
26+
'Method "swap" cannot be called on "VirtualKeyedArray" instance'
27+
);
28+
}
29+
30+
insert(index: number, value: FieldValue): void {
31+
if (index !== 0) {
32+
throw new Error(
33+
`Method "insert" cannot be called on "VirtualKeyedArray" instance with those args (index=${index}), expected (0)`
34+
);
35+
}
36+
this.setValue([value]);
37+
}
38+
39+
remove(): void {
40+
throw new Error(
41+
'Method "remove" cannot be called on "VirtualKeyedArray" instance'
42+
);
43+
}
44+
45+
splice(start: number, count: number, ...items: FieldValue[]): FieldValue[] {
46+
if (start !== 0 || count !== 0) {
47+
throw new Error(
48+
`Method "splice" cannot be called on "VirtualKeyedArray" instance with those args(start=${start}, count=${count}) expected (0, 0)`
49+
);
50+
}
51+
this.setValue(items);
52+
return [];
53+
}
54+
}
55+
56+
export function createKeyedArrayDeriver<V extends Validator>(
57+
ctx: FormInternalContext<V>,
58+
value: () => SchemaArrayValue | undefined,
59+
setValue: (v: SchemaArrayValue) => void
60+
) {
61+
let lastKey = Number.MIN_SAFE_INTEGER;
62+
return () => {
63+
let stored: KeyedArray2<number, FieldValue> | undefined;
64+
const v = value();
65+
if (v === undefined) {
66+
stored = new VirtualKeyedArray(setValue);
67+
} else {
68+
stored = ctx.keyedArrays.get(v);
69+
if (stored === undefined) {
70+
stored = new SimpleKeyedArray(v, () => lastKey++);
71+
ctx.keyedArrays.set(v, stored);
72+
}
73+
}
74+
return stored;
75+
};
76+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { Icons } from "../icons.js";
2121
import type { FormMerger } from "../merger.js";
2222
import type { Theme } from "../components.js";
2323
import type { Id, IdOptions } from "../id.js";
24-
import type { FormValue } from "../model.js";
24+
import type { FormValue, KeyedArraysMap } from "../model.js";
2525
import type { ResolveFieldType } from "../fields.js";
2626

2727
/** @deprecated don't use this type */
@@ -32,6 +32,7 @@ export interface FormInternalContext<V extends Validator>
3232
Readonly<Required<IdOptions>> {
3333
value: FormValue;
3434
isChanged: boolean;
35+
readonly keyedArrays: KeyedArraysMap;
3536
readonly rootId: Id;
3637
readonly fieldsValidationMode: number;
3738
readonly isSubmitted: boolean;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from "./event-handlers.svelte.js";
66
export * from "./attributes.js";
77
export * from "./files.js";
88
export * from "./components.js";
9+
export * from "./array.js";

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
} from "./id.js";
5858
import type { Config } from "./config.js";
5959
import type { Theme } from "./components.js";
60-
import type { FormValue, ValueRef } from "./model.js";
60+
import type { FormValue, KeyedArraysMap, ValueRef } from "./model.js";
6161
import type { ResolveFieldType } from "./fields.js";
6262

6363
export const DEFAULT_FIELDS_VALIDATION_DEBOUNCE_MS = 300;
@@ -231,6 +231,7 @@ export interface FormOptions<T, V extends Validator>
231231
*/
232232
onReset?: (e: Event) => void;
233233
schedulerYield?: SchedulerYield;
234+
keyedArraysMap?: KeyedArraysMap;
234235
}
235236

236237
export interface FormState<T, V extends Validator> {
@@ -454,6 +455,10 @@ export function createForm<T, V extends Validator>(
454455
...uiSchema["ui:options"],
455456
});
456457

458+
const keyedArrays: KeyedArraysMap = $derived(
459+
options.keyedArraysMap ?? new WeakMap()
460+
);
461+
457462
const context: FormInternalContext<V> = {
458463
...({} as FormContext),
459464
get rootId() {
@@ -473,6 +478,9 @@ export function createForm<T, V extends Validator>(
473478
get dataUrlToBlob() {
474479
return dataUrlToBlob;
475480
},
481+
get keyedArrays() {
482+
return keyedArrays;
483+
},
476484
get isSubmitted() {
477485
return isSubmitted;
478486
},

packages/form/src/form/model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import type { KeyedArray2 } from "@/lib/keyed-array.svelte.js";
12
import {
23
getSchemaDefinitionByPath,
34
type Path,
45
type Schema,
6+
type SchemaArrayValue,
57
type SchemaValue,
68
} from "@/core/index.js";
79

810
export type FieldValue = SchemaValue | undefined;
911

1012
export type FormValue = SchemaValue | undefined;
1113

14+
export type KeyedArraysMap = WeakMap<SchemaArrayValue, KeyedArray2<number, FieldValue>>;
15+
1216
export interface ValueRef<T> {
1317
current: T;
1418
}

0 commit comments

Comments
 (0)