diff --git a/docs/complex-structures.md b/docs/complex-structures.md index 824f3989..44708895 100644 --- a/docs/complex-structures.md +++ b/docs/complex-structures.md @@ -110,7 +110,7 @@ function Example() { } ``` -For information about modifying list (e.g. append / remove / reorder), see the [list intent](/docs/intent-button.md#list-intent) section. +For information about modifying list (e.g. insert / remove / reorder), see the [list intent](/docs/intent-button.md#list-intent) section. ## Nested List diff --git a/docs/intent-button.md b/docs/intent-button.md index 9d220aad..06aeb8b4 100644 --- a/docs/intent-button.md +++ b/docs/intent-button.md @@ -99,7 +99,7 @@ export default function Todos() { ))}
- +
@@ -159,7 +159,7 @@ export default function Todos() { ))}
-
diff --git a/examples/react-router/src/todos.tsx b/examples/react-router/src/todos.tsx index 7a2144b7..32c324c3 100644 --- a/examples/react-router/src/todos.tsx +++ b/examples/react-router/src/todos.tsx @@ -74,7 +74,7 @@ export function Component() {

))} - +
diff --git a/examples/remix/app/routes/todos.tsx b/examples/remix/app/routes/todos.tsx index f8394597..c43802fa 100644 --- a/examples/remix/app/routes/todos.tsx +++ b/examples/remix/app/routes/todos.tsx @@ -76,7 +76,7 @@ export default function TodoForm() {

))} - +
diff --git a/package-lock.json b/package-lock.json index 29b5ed5d..d6e39f3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -265,62 +265,6 @@ "node": ">=14" } }, - "examples/yup": { - "name": "@conform-example/yup", - "license": "MIT", - "dependencies": { - "@conform-to/react": "0.8.0", - "@conform-to/yup": "0.8.0", - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "clsx": "^1.2.0", - "isbot": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "yup": "^0.32.11" - }, - "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "cross-env": "^7.0.3", - "eslint": "^8.35.0", - "typescript": "^4.9.5" - }, - "engines": { - "node": ">=14" - } - }, - "examples/zod": { - "name": "@conform-example/zod", - "license": "MIT", - "dependencies": { - "@conform-to/react": "0.8.0", - "@conform-to/zod": "0.8.0", - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "clsx": "^1.2.0", - "isbot": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "zod": "^3.21.0" - }, - "devDependencies": { - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "cross-env": "^7.0.3", - "eslint": "^8.35.0", - "typescript": "^4.9.5" - }, - "engines": { - "node": ">=14" - } - }, "guide": { "dependencies": { "@markdoc/markdoc": "^0.1.7", @@ -3502,14 +3446,6 @@ "resolved": "examples/remix", "link": true }, - "node_modules/@conform-example/yup": { - "resolved": "examples/yup", - "link": true - }, - "node_modules/@conform-example/zod": { - "resolved": "examples/zod", - "link": true - }, "node_modules/@conform-to/dom": { "resolved": "packages/conform-dom", "link": true @@ -20124,7 +20060,8 @@ "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -21753,7 +21690,8 @@ "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", - "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", + "dev": true }, "node_modules/nanoid": { "version": "3.3.4", @@ -24251,7 +24189,8 @@ "node_modules/property-expr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", - "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", + "dev": true }, "node_modules/property-information": { "version": "6.1.1", @@ -27642,7 +27581,8 @@ "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true }, "node_modules/tough-cookie": { "version": "4.1.1", @@ -30076,6 +30016,7 @@ "version": "0.32.11", "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dev": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/lodash": "^4.14.175", @@ -32557,50 +32498,6 @@ "zod": "^3.21.0" } }, - "@conform-example/yup": { - "version": "file:examples/yup", - "requires": { - "@conform-to/react": "0.8.0", - "@conform-to/yup": "0.8.0", - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "clsx": "^1.2.0", - "cross-env": "^7.0.3", - "eslint": "^8.35.0", - "isbot": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^4.9.5", - "yup": "^0.32.11" - } - }, - "@conform-example/zod": { - "version": "file:examples/zod", - "requires": { - "@conform-to/react": "0.8.0", - "@conform-to/zod": "0.8.0", - "@remix-run/dev": "^1.19.3", - "@remix-run/eslint-config": "^1.19.3", - "@remix-run/node": "^1.19.3", - "@remix-run/react": "^1.19.3", - "@remix-run/serve": "^1.19.3", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "clsx": "^1.2.0", - "cross-env": "^7.0.3", - "eslint": "^8.35.0", - "isbot": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^4.9.5", - "zod": "^3.21.0" - } - }, "@conform-to/dom": { "version": "file:packages/conform-dom" }, @@ -44295,7 +44192,8 @@ "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true }, "lodash.camelcase": { "version": "4.3.0", @@ -45428,7 +45326,8 @@ "nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", - "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", + "dev": true }, "nanoid": { "version": "3.3.4", @@ -47076,7 +46975,8 @@ "property-expr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", - "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", + "dev": true }, "property-information": { "version": "6.1.1", @@ -49644,7 +49544,8 @@ "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true }, "tough-cookie": { "version": "4.1.1", @@ -51355,6 +51256,7 @@ "version": "0.32.11", "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dev": true, "requires": { "@babel/runtime": "^7.15.4", "@types/lodash": "^4.14.175", diff --git a/packages/conform-dom/intent.ts b/packages/conform-dom/intent.ts index 5ac52f67..41a7007c 100644 --- a/packages/conform-dom/intent.ts +++ b/packages/conform-dom/intent.ts @@ -8,6 +8,7 @@ export interface IntentButtonProps { } export type ListIntentPayload = + | { name: string; operation: 'insert'; defaultValue?: Schema; index?: number } | { name: string; operation: 'prepend'; defaultValue?: Schema } | { name: string; operation: 'append'; defaultValue?: Schema } | { name: string; operation: 'replace'; defaultValue: Schema; index: number } @@ -21,21 +22,34 @@ type ExtractListIntentPayload = Pretty< > >; +type ListIntent = {} extends ExtractListIntentPayload + ? ( + name: string, + payload?: ExtractListIntentPayload, + ) => IntentButtonProps + : ( + name: string, + payload: ExtractListIntentPayload, + ) => IntentButtonProps; + /** * Helpers to configure an intent button for modifying a list * * @see https://conform.guide/api/react#list */ export const list = new Proxy<{ - [Operation in ListIntentPayload['operation']]: {} extends ExtractListIntentPayload - ? ( - name: string, - payload?: ExtractListIntentPayload, - ) => IntentButtonProps - : ( - name: string, - payload: ExtractListIntentPayload, - ) => IntentButtonProps; + /** + * @deprecated You can use `insert` without specifying an index instead + */ + append: ListIntent<'append'>; + /** + * @deprecated You can use `insert` with zero index instead + */ + prepend: ListIntent<'prepend'>; + insert: ListIntent<'insert'>; + replace: ListIntent<'replace'>; + remove: ListIntent<'remove'>; + reorder: ListIntent<'reorder'>; }>({} as any, { get(_target, operation: any) { return (name: string, payload = {}): IntentButtonProps => ({ @@ -94,6 +108,7 @@ export function requestIntent( }, ): void { if (!form) { + // eslint-disable-next-line no-console console.warn('No form element is provided'); return; } @@ -152,6 +167,9 @@ export function updateList( case 'append': list.push(payload.defaultValue as any); break; + case 'insert': + list.splice(payload.index ?? list.length, 0, payload.defaultValue as any); + break; case 'replace': list.splice(payload.index, 1, payload.defaultValue); break; diff --git a/packages/conform-react/README.md b/packages/conform-react/README.md index 136e5764..3271dc07 100644 --- a/packages/conform-react/README.md +++ b/packages/conform-react/README.md @@ -270,7 +270,7 @@ function Example() { ))} {/* Setup a button that can append a new row with optional default value */} - + ); } @@ -502,11 +502,11 @@ import { list } from '@conform-to/react'; function Example() { return (
- {/* To append a new row with optional defaultValue */} - - - {/* To prepend a new row with optional defaultValue */} - + {/* + To insert a new row with optional defaultValue at a given index. + If no index is given, then the element will be appended at the end of the list. + */} + {/* To remove a row by index */} diff --git a/packages/conform-react/hooks.ts b/packages/conform-react/hooks.ts index 296a529e..ffb65ed2 100644 --- a/packages/conform-react/hooks.ts +++ b/packages/conform-react/hooks.ts @@ -712,6 +712,7 @@ export function useFieldList | undefined>( switch (intent.payload.operation) { case 'append': case 'prepend': + case 'insert': case 'replace': return updateList(list, { ...intent.payload, diff --git a/playground/app/routes/simple-list.tsx b/playground/app/routes/simple-list.tsx index d3a495fb..efe79fd7 100644 --- a/playground/app/routes/simple-list.tsx +++ b/playground/app/routes/simple-list.tsx @@ -84,13 +84,13 @@ export default function SimpleList() {
diff --git a/playwright.config.ts b/playwright.config.ts index dc9a28a5..28a59f62 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -40,6 +40,8 @@ const config: PlaywrightTestConfig = { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + /* Take screenshot on testrun failure. */ + screenshot: 'only-on-failure', }, /* Configure projects for major browsers */ diff --git a/tests/conform-dom.spec.ts b/tests/conform-dom.spec.ts index 673da063..4166c4f3 100644 --- a/tests/conform-dom.spec.ts +++ b/tests/conform-dom.spec.ts @@ -143,7 +143,7 @@ test.describe('conform-dom', () => { error: {}, }; - const intent1 = list.prepend('tasks'); + const intent1 = list.insert('tasks', { index: 0 }); expect( parse(createFormData([...entries, [intent1.name, intent1.value]])), @@ -155,8 +155,9 @@ test.describe('conform-dom', () => { }, }); - const intent2 = list.prepend('tasks', { + const intent2 = list.insert('tasks', { defaultValue: { content: 'Something' }, + index: 0, }); expect( @@ -169,7 +170,7 @@ test.describe('conform-dom', () => { }, }); - const intent3 = list.append('tasks'); + const intent3 = list.insert('tasks'); expect( parse(createFormData([...entries, [intent3.name, intent3.value]])), @@ -181,7 +182,7 @@ test.describe('conform-dom', () => { }, }); - const intent4 = list.append('tasks', { + const intent4 = list.insert('tasks', { defaultValue: { content: 'Something' }, }); @@ -343,6 +344,20 @@ test.describe('conform-dom', () => { defaultValue: 'testing/seperator', }, }); + expect( + parseIntent( + list.insert('tasks', { index: 3, defaultValue: 'testing/seperator' }) + .value, + ), + ).toEqual({ + type: 'list', + payload: { + name: 'tasks', + operation: 'insert', + defaultValue: 'testing/seperator', + index: 3, + }, + }); expect(parseIntent(list.remove('tasks', { index: 0 }).value)).toEqual({ type: 'list', payload: {