Skip to content

Commit 239d795

Browse files
authored
Merge pull request #65 from x0k/fix-moving-one-of-item
Fix moving `oneOf` item inside array field
2 parents 00be16c + 01d5f56 commit 239d795

19 files changed

+1034
-203
lines changed

.changeset/hip-otters-think.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+
Use keyed array inside an array field to preserve items state

mkfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ cfw/:
3333

3434
f/:
3535
pushd packages/form
36+
d:
37+
pnpm run dev
3638
b:
3739
pnpm run build
3840
c:
3941
pnpm run check
4042
t:
4143
pnpm run test $@
44+
tui:
45+
pnpm run test:ui
4246
popd
4347

4448
docs/:

packages/form/index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>SJSF</title>
8+
</head>
9+
10+
<body>
11+
<div id="app"></div>
12+
<script type="module" src="/src/app.ts"></script>
13+
</body>
14+
15+
</html>

packages/form/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"files": [
1313
"dist",
1414
"LICENSE-APACHE",
15+
"!app.*",
1516
"!dist/**/*.test.*",
1617
"!dist/**/*.spec.*"
1718
],
@@ -26,7 +27,9 @@
2627
"bugs": "https://github.com/x0k/svelte-jsonschema-form/issues",
2728
"homepage": "https://x0k.github.io/svelte-jsonschema-form/",
2829
"scripts": {
30+
"dev": "vite",
2931
"test": "vitest run --exclude '.svelte-kit/**'",
32+
"test:ui": "vitest --ui --exclude '.svelte-kit/**'",
3033
"build": "svelte-package && publint",
3134
"check": "svelte-check --tsconfig ./tsconfig.json"
3235
},
@@ -42,10 +45,15 @@
4245
"devDependencies": {
4346
"@sveltejs/package": "catalog:",
4447
"@sveltejs/vite-plugin-svelte": "catalog:",
48+
"@testing-library/jest-dom": "^6.6.3",
49+
"@testing-library/svelte": "^5.2.6",
50+
"@testing-library/user-event": "^14.6.1",
4551
"@tsconfig/svelte": "catalog:",
4652
"@types/json-schema": "^7.0.15",
4753
"@types/json-schema-merge-allof": "^0.6.5",
54+
"@vitest/ui": "^3.0.4",
4855
"deep-freeze-es6": "^4.0.0",
56+
"jsdom": "^26.0.0",
4957
"svelte": "catalog:",
5058
"svelte-check": "catalog:",
5159
"vite": "catalog:",

packages/form/src/app.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import { Form2, type Schema } from "./form/index.js";
3+
import { theme } from "./basic-theme/index.js";
4+
import { translation } from "./translations/en.js";
5+
import { createValidator } from "./fake-validator.js";
6+
7+
const schema: Schema = {
8+
type: 'string'
9+
};
10+
11+
const validator = createValidator();
12+
</script>
13+
14+
<Form2 {...theme} {schema} {translation} {validator} />

packages/form/src/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { mount } from "svelte";
2+
3+
import App from "./app.svelte";
4+
5+
const target = document.getElementById("app");
6+
7+
export default mount(App, { target: target! });
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect, it } from "vitest";
2+
import { render, screen, getByText } from "@testing-library/svelte";
3+
import { userEvent } from '@testing-library/user-event';
4+
5+
import type { Schema } from "@/core/index.js";
6+
import { theme } from "@/basic-theme/index.js";
7+
import { createValidator } from "@/fake-validator.js";
8+
import { translation } from "@/translations/en.js";
9+
10+
import { FORM_CONTEXT } from "./context/index.js";
11+
import { createForm3 } from "./create-form.svelte.js";
12+
import Content from "./content.svelte";
13+
import { computePseudoId, DEFAULT_ID_PREFIX, DEFAULT_ID_SEPARATOR, DEFAULT_PSEUDO_ID_SEPARATOR, pathToId } from './id-schema.js';
14+
15+
it("should preserve state of multi select field in array", async () => {
16+
const user = userEvent.setup()
17+
const schema: Schema = {
18+
type: "array",
19+
title: "Array",
20+
items: {
21+
anyOf: [
22+
{
23+
title: "Foo option",
24+
properties: {
25+
foo: {
26+
type: "string",
27+
},
28+
},
29+
},
30+
{
31+
title: "Bar option",
32+
properties: {
33+
bar: {
34+
type: "string",
35+
},
36+
},
37+
},
38+
],
39+
},
40+
};
41+
42+
const validator = createValidator();
43+
44+
const form = createForm3({
45+
...theme,
46+
validator,
47+
translation,
48+
schema,
49+
initialValue: [{}, {}],
50+
});
51+
52+
render(Content, {
53+
context: new Map([[FORM_CONTEXT, form.context]]),
54+
props: { form }
55+
})
56+
57+
const id = pathToId(DEFAULT_ID_PREFIX, DEFAULT_ID_SEPARATOR, [1])
58+
const pseudoId = computePseudoId(DEFAULT_PSEUDO_ID_SEPARATOR, id, 'anyof')
59+
const el = document.getElementById(pseudoId)
60+
if (el === null) {
61+
throw new Error(`cannot find ${pseudoId} select`)
62+
}
63+
await user.selectOptions(el, 'Bar option')
64+
65+
const input = screen.getByLabelText('bar')
66+
await user.type(input, 'bar state')
67+
68+
const firstItem = document.querySelector('[data-layout="array-item"]')
69+
if (!(firstItem instanceof HTMLElement)) {
70+
throw new Error('cannot find first item')
71+
}
72+
const delBtn = getByText(firstItem, 'Del')
73+
user.click(delBtn)
74+
75+
const input2 = screen.getByLabelText('bar')
76+
expect(input2).toHaveValue('bar state')
77+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface FormContext {
5959
>;
6060
}
6161

62-
const FORM_CONTEXT = Symbol("form-context");
62+
export const FORM_CONTEXT = Symbol("form-context");
6363

6464
export function getFormContext(): FormContext {
6565
return getContext(FORM_CONTEXT);

packages/form/src/form/fields/array/array-field.svelte

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { createKeyedArray } from "@/lib/keyed-array.svelte.js";
23
import { isFixedItems, type Schema } from "@/core/index.js";
34
45
import {
@@ -17,10 +18,7 @@
1718
1819
import type { FieldProps } from "../model.js";
1920
20-
import {
21-
setArrayContext,
22-
type ArrayContext,
23-
} from "./context.js";
21+
import { setArrayContext, type ArrayContext } from "./context.js";
2422
2523
let { value = $bindable(), config }: FieldProps<"array"> = $props();
2624
@@ -51,6 +49,8 @@
5149
validateField(ctx, config, value);
5250
}
5351
52+
const keyedArray = createKeyedArray(() => value ?? []);
53+
5454
const arrayCtx: ArrayContext = {
5555
get errors() {
5656
return errors;
@@ -74,45 +74,29 @@
7474
return copyable;
7575
},
7676
validate,
77+
key(index) {
78+
return keyedArray.key(index);
79+
},
7780
pushItem(itemSchema: Schema) {
78-
if (value === undefined) {
79-
return;
80-
}
81-
value.push(getDefaultFieldState(ctx, itemSchema, undefined));
81+
keyedArray.push(getDefaultFieldState(ctx, itemSchema, undefined));
8282
validate();
8383
},
8484
moveItemUp(index) {
85-
if (value === undefined || index < 1) {
86-
return;
87-
}
88-
const tmp = value[index];
89-
value[index] = value[index - 1];
90-
value[index - 1] = tmp;
85+
keyedArray.swap(index, index - 1);
9186
validate();
9287
},
9388
moveItemDown(index) {
94-
if (value === undefined || index > value.length - 2) {
95-
return;
96-
}
97-
const tmp = value[index];
98-
value[index] = value[index + 1];
99-
value[index + 1] = tmp;
89+
keyedArray.swap(index, index + 1);
10090
validate();
10191
},
10292
copyItem(index) {
103-
if (value === undefined) {
104-
return
105-
}
106-
value.splice(index, 0, $state.snapshot(value[index]))
107-
validate()
93+
keyedArray.insert(index, $state.snapshot(value![index]));
94+
validate();
10895
},
10996
removeItem(index) {
110-
if (value === undefined) {
111-
return
112-
}
113-
value.splice(index, 1)
114-
validate()
115-
}
97+
keyedArray.remove(index);
98+
validate();
99+
},
116100
};
117101
setArrayContext(arrayCtx);
118102

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

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

3-
import {
4-
isFilesArray2 as isFilesArrayInternal,
5-
type SchemaArrayValue,
6-
type Schema,
7-
type SchemaValue,
8-
} from "@/core/index.js";
3+
import type { SchemaArrayValue, Schema, SchemaValue } from "@/core/index.js";
94

10-
import type { ValidationError } from "../../validator.js";
115
import {
126
type FormContext,
137
makeArrayItemId,
148
makeIdSchema,
159
} from "../../context/index.js";
16-
import { type IdSchema } from "../../id-schema.js";
10+
import type { ValidationError } from "../../validator.js";
11+
import type { IdSchema } from "../../id-schema.js";
1712

1813
export interface ArrayContext {
1914
disabled: boolean;
@@ -25,6 +20,7 @@ export interface ArrayContext {
2520
errors: ValidationError<unknown>[];
2621
/** @deprecated */
2722
validate: () => void;
23+
key(index: number): number;
2824
pushItem(itemSchema: Schema): void;
2925
moveItemUp(index: number): void;
3026
moveItemDown(index: number): void;

0 commit comments

Comments
 (0)