From b7f42f69207a22552fe2a4b73570fcb09e73420e Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 5 Jan 2025 21:52:23 +0000 Subject: [PATCH 01/17] wip: add svelte-form Absolutely untested, written without running it or trying it out in any way. Just a very rough start written off the top of my head with the help of various bits of docs. --- packages/svelte-form/README.md | 1 + packages/svelte-form/eslint.config.js | 5 + packages/svelte-form/package.json | 56 +++++++ packages/svelte-form/src/Field.svelte | 50 +++++++ packages/svelte-form/src/createField.ts | 68 +++++++++ packages/svelte-form/src/createForm.ts | 41 +++++ packages/svelte-form/src/index.ts | 6 + packages/svelte-form/tests/simple.svelte | 175 ++++++++++++++++++++++ packages/svelte-form/tests/simple.test.ts | 76 ++++++++++ packages/svelte-form/tsconfig.build.json | 12 ++ packages/svelte-form/tsconfig.docs.json | 9 ++ packages/svelte-form/tsconfig.json | 7 + packages/svelte-form/vite.config.ts | 14 ++ pnpm-lock.yaml | 69 +++++++++ 14 files changed, 589 insertions(+) create mode 100644 packages/svelte-form/README.md create mode 100644 packages/svelte-form/eslint.config.js create mode 100644 packages/svelte-form/package.json create mode 100644 packages/svelte-form/src/Field.svelte create mode 100644 packages/svelte-form/src/createField.ts create mode 100644 packages/svelte-form/src/createForm.ts create mode 100644 packages/svelte-form/src/index.ts create mode 100644 packages/svelte-form/tests/simple.svelte create mode 100644 packages/svelte-form/tests/simple.test.ts create mode 100644 packages/svelte-form/tsconfig.build.json create mode 100644 packages/svelte-form/tsconfig.docs.json create mode 100644 packages/svelte-form/tsconfig.json create mode 100644 packages/svelte-form/vite.config.ts diff --git a/packages/svelte-form/README.md b/packages/svelte-form/README.md new file mode 100644 index 000000000..3e608b1f8 --- /dev/null +++ b/packages/svelte-form/README.md @@ -0,0 +1 @@ +TODO - add a readme diff --git a/packages/svelte-form/eslint.config.js b/packages/svelte-form/eslint.config.js new file mode 100644 index 000000000..8ce6ad05f --- /dev/null +++ b/packages/svelte-form/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +export default [...rootConfig] diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json new file mode 100644 index 000000000..892f427f0 --- /dev/null +++ b/packages/svelte-form/package.json @@ -0,0 +1,56 @@ +{ + "name": "@tanstack/svelte-form", + "version": "0.0.1", + "description": "Powerful, type-safe forms for Svelte.", + "author": "James Garbutt (https://github.com/43081j)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/form.git", + "directory": "packages/svelte-form" + }, + "homepage": "https://tanstack.com/form", + "scripts": { + "clean": "premove ./build ./coverage", + "test:eslint": "eslint ./src ./tests", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", + "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", + "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "tsc", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict", + "build": "tsc -p tsconfig.build.json" + }, + "type": "module", + "types": "dist/index.d.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "dependencies": { + "@tanstack/form-core": "workspace:*" + }, + "devDependencies": { + "svelte": "^5.16.1", + "premove": "^4.0.0" + }, + "peerDependencies": { + "svelte": "^5.16.1" + } +} diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte new file mode 100644 index 000000000..fa4194300 --- /dev/null +++ b/packages/svelte-form/src/Field.svelte @@ -0,0 +1,50 @@ +<!-- TODO (43081j): figure out how to reference types in generics --> +<script type="ts" generics="TParentData, + TName extends DeepKeys<TParentData>, + TFieldValidator extends + | Validator<DeepValue<TParentData, TName>, unknown> + | undefined = undefined, + TFormValidator extends + | Validator<TParentData, unknown> + | undefined = undefined, + TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName> +"> + import type { Snippet } from 'svelte'; + // TODO (43081j): somehow remove this circular reference + import { createField } from './createField'; + + type Props = { + children: Snippet<[ + FieldApi< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + > + ]> + [key: string]: unknown; + } & FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >; + + let { + children + }: Props = $props(); + + const fieldApi = createField< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >(() => { + return fieldOptions + }) +</script> + +{@render children(fieldApi)} diff --git a/packages/svelte-form/src/createField.ts b/packages/svelte-form/src/createField.ts new file mode 100644 index 000000000..21880ba66 --- /dev/null +++ b/packages/svelte-form/src/createField.ts @@ -0,0 +1,68 @@ +import { FieldApi } from '@tanstack/form-core' +import { onDestroy, onMount } from 'svelte' +import Field from './Field.js' + +import type { + DeepKeys, + DeepValue, + FieldApiOptions, + Validator, +} from '@tanstack/form-core' + +interface SvelteFieldApi< + TParentData, + TFormValidator extends + | Validator<TParentData, unknown> + | undefined = undefined, +> { + Field: Field<TParentData, TFormValidator> +} + +export function createField< + TParentData, + TName extends DeepKeys<TParentData>, + TFieldValidator extends + | Validator<DeepValue<TParentData, TName>, unknown> + | undefined = undefined, + TFormValidator extends + | Validator<TParentData, unknown> + | undefined = undefined, + TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, +>( + opts: () => FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >, +) { + const options = opts() + + const api = new FieldApi(options) + + const extendedApi: typeof api & SvelteFieldApi<TParentData, TFormValidator> = + api as never + + extendedApi.Field = Field as never + + let mounted = false + // Instantiates field meta and removes it when unrendered + onMount(() => { + const cleanupFn = api.mount() + mounted = true + onDestroy(() => { + cleanupFn() + mounted = false + }) + }) + + // TODO (43081j): does this do what i think? we don't access anything + // svelte is aware of, so maybe it'll never call this? + $effect(() => { + if (!mounted) return + api.update(opts()) + }) + + return extendedApi +} diff --git a/packages/svelte-form/src/createForm.ts b/packages/svelte-form/src/createForm.ts new file mode 100644 index 000000000..3259ac141 --- /dev/null +++ b/packages/svelte-form/src/createForm.ts @@ -0,0 +1,41 @@ +import { FormApi } from '@tanstack/form-core' +import { onMount } from 'svelte' +import { Field, createField } from './createField' +import type { FormOptions, Validator } from '@tanstack/form-core' + +export interface SvelteFormApi< + TFormData, + TFormValidator extends Validator<TFormData, unknown> | undefined = undefined, +> { + Field: Field<TFormData, TFormValidator> + createField: typeof createField<TFormData, TFormValidator> +} + +export function createForm< + TParentData, + TFormValidator extends + | Validator<TParentData, unknown> + | undefined = undefined, +>(opts?: () => FormOptions<TParentData, TFormValidator>) { + const options = opts?.() + const api = new FormApi<TParentData, TFormValidator>(options) + const extendedApi: typeof api & SvelteFormApi<TParentData, TFormValidator> = + api as never + + // TODO (43081j): somehow this needs to actually be + // `<Field ...props form={api}>`. + // No clue right now how we do that + extendedApi.Field = Field + extendedApi.createField = (props) => + createField(() => { + return { ...props(), form: api } + }) + + onMount(api.mount) + + // TODO (43081j): does this actually work? we don't use any observed + // data, so maybe svelte won't re-run this effect? + $effect(() => api.update(opts?.())) + + return extendedApi +} diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts new file mode 100644 index 000000000..2f308b888 --- /dev/null +++ b/packages/svelte-form/src/index.ts @@ -0,0 +1,6 @@ +export * from '@tanstack/form-core' + +export { createForm, type SvelteFormApi } from './createForm.js' + +export type { Field } from './Field.js' +export { createField } from './createField.js' diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte new file mode 100644 index 000000000..38a510ac1 --- /dev/null +++ b/packages/svelte-form/tests/simple.svelte @@ -0,0 +1,175 @@ +<script type="ts"> +import { createForm } from '../src/index.js' +import type { FieldApi, FormOptions } from '../src/index.js' + +interface Employee { + firstName: string + lastName: string + color?: '#FF0000' | '#00FF00' | '#0000FF' + employed: boolean + jobTitle: string +} + +const sampleData: Employee = { + firstName: 'Bob', + lastName: '', + employed: false, + jobTitle: '', +} + +const formConfig: FormOptions<Employee, undefined> = { + defaultValues: sampleData, +} + +const form = createForm(() => ({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, +})) + +</script> +<form + id="form" + on:submit|preventDefault={form.onSubmit} +> + <h1>TanStack Form - Svelte Demo</h1> + + <form.Field + name="firstName" + validators={{ + onChange: ({ value }) => + value.length < 3 ? 'Not long enough' : undefined, + }} + > + {#snippet children(field)} + <div> + <label>First Name</label> + <input + id="firstName" + type="text" + placeholder="First Name" + value={field.state.value} + on:blur={() => field.handleBlur()} + on:input={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + </div> + {/snippet} + </form.Field> + <form.Field + name="lastName" + validators={{ + onChange: ({ value }) => + value.length < 3 ? 'Not long enough' : undefined, + }} + > + {#snippet children(field)} + <div> + <label>Last Name</label> + <input + id="lastName" + type="text" + placeholder="Last Name" + value={field.state.value} + on:blur={() => field.handleBlur()} + on:input={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + </div> + {/snippet} + </form.Field> + <form.Field + name="color" + > + {#snippet children(field)} + <div> + <label>Favorite Color</label> + <select + value={field.state.value} + on:blur={() => field.handleBlur()} + on:input={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange( + target.value as '#FF0000' | '#00FF00' | '#0000FF', + ) + }} + > + <option value="#FF0000">Red</option> + <option value="#00FF00">Green</option> + <option value="#0000FF">Blue</option> + </select> + </div> + {/snippet} + </form.Field> + <form.Field + name="employed" + > + {#snippet children(field)} + <div> + <label>Employed?</label> + <input + on:input={() => field.handleChange(!field.state.value)} + checked={field.state.value} + on:blur={() => field.handleBlur()} + id="employed" + type="checkbox" + /> + </div> + {#if field.state.value} + <form.Field + name="jobTitle" + validators={{ + onChange: ({ value }) => + value.length === 0 + ? 'Needs to have a job here' + : null, + }} + > + {#snippet children(field)} + <div> + <label>Job Title</label> + <input + type="text" + id="jobTitle" + placeholder="Job Title" + value={subField.state.value} + on:blur={() => subField.handleBlur()} + on:input={(e: Event) => { + const target = e.target as HTMLInputElement + subField.handleChange(target.value) + }} + /> + </div> + {/snippet} + </form.Field> + {/if} + {/snippet} + </form.Field> + <div> + <button + type="submit" + disabled={form.api.state.isSubmitting} + > + {form.api.state.isSubmitting ? html` Submitting` : 'Submit'} + </button> + <button + type="button" + id="reset" + on:click={() => { + form.api.reset() + }} + > + Reset + </button> + </div> +</form> +<pre>{JSON.stringify(form.api.state, null, 2)}</pre> diff --git a/packages/svelte-form/tests/simple.test.ts b/packages/svelte-form/tests/simple.test.ts new file mode 100644 index 000000000..586643e51 --- /dev/null +++ b/packages/svelte-form/tests/simple.test.ts @@ -0,0 +1,76 @@ +/// <reference lib="dom" /> +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import '@testing-library/jest-dom' +import { userEvent } from '@testing-library/user-event' +import { mount } from 'svelte' +import TestForm from './simple' + +describe('Svelte Tests', () => { + let element: TestForm + beforeEach(async () => { + element = document.createElement('div') + document.body.appendChild(element) + mount(TestForm, { + target: element, + props: {}, + }) + }) + + afterEach(() => { + element.remove() + }) + + it('should have initial values', async () => { + expect( + await element.shadowRoot!.querySelector<HTMLInputElement>('#firstName'), + ).toHaveValue(sampleData.firstName) + expect( + await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName'), + ).toHaveValue('') + const form = element.form! + expect(form.api.getFieldValue('firstName')).toBe('Bob') + expect(form.api.getFieldMeta('firstName')?.isTouched).toBeFalsy() + expect(form.api.getFieldValue('lastName')).toBe('') + expect(form.api.getFieldMeta('lastName')?.isTouched).toBeFalsy() + }) + it('should mirror user input', async () => { + const lastName = + await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName')! + const lastNameValue = 'Jobs' + await userEvent.type(lastName, lastNameValue) + + const form = element.form! + expect(form.api.getFieldValue('lastName')).toBe(lastNameValue) + expect(form.api.getFieldMeta('lastName')?.isTouched).toBeTruthy() + }) + it('Reset form to initial value', async () => { + const firstName = + await element.shadowRoot!.querySelector<HTMLInputElement>('#firstName')! + await userEvent.type(firstName, '-Joseph') + + expect(firstName).toHaveValue('Christian-Joseph') + + const form = element.form + await element + .shadowRoot!.querySelector<HTMLButtonElement>('#reset') + ?.click() + expect(form.api.getFieldValue('firstName')).toBe('Bob') + }) + + it('should display validation', async () => { + const lastName = + await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName')! + const lastNameValue = 'Jo' + await userEvent.type(lastName, lastNameValue) + expect(lastName).toHaveValue('Jo') + const form = element.form + expect(form.api.getFieldMeta('lastName')?.errors[0]).toBe('Not long enough') + + await userEvent.type(lastName, lastNameValue) + + expect(await lastName.getAttribute('error-text')).toBeFalsy() + expect(form.api.getFieldValue('lastName')).toBe('JoJo') + expect(form.api.getFieldMeta('lastName')?.isTouched).toBeTruthy() + expect(form.api.getFieldMeta('lastName')?.errors.length).toBeFalsy() + }) +}) diff --git a/packages/svelte-form/tsconfig.build.json b/packages/svelte-form/tsconfig.build.json new file mode 100644 index 000000000..25a44d38f --- /dev/null +++ b/packages/svelte-form/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "node16", + "rootDir": "./src", + "outDir": "./dist", + "noEmit": false, + "declaration": true, + "sourceMap": true + }, + "include": ["src"] +} diff --git a/packages/svelte-form/tsconfig.docs.json b/packages/svelte-form/tsconfig.docs.json new file mode 100644 index 000000000..2c9444e16 --- /dev/null +++ b/packages/svelte-form/tsconfig.docs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/form-core": ["../form-core/src"] + } + }, + "exclude": ["tests", "eslint.config.js", "vite.config.ts"] +} diff --git a/packages/svelte-form/tsconfig.json b/packages/svelte-form/tsconfig.json new file mode 100644 index 000000000..c7651b3dc --- /dev/null +++ b/packages/svelte-form/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "node16" + }, + "include": ["src", "tests", "eslint.config.js", "vite.config.ts"] +} diff --git a/packages/svelte-form/vite.config.ts b/packages/svelte-form/vite.config.ts new file mode 100644 index 000000000..8de48919a --- /dev/null +++ b/packages/svelte-form/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' +import packageJson from './package.json' + +export default defineConfig({ + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + globals: true, + coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, + typecheck: { enabled: true }, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b5b642c9..15ea96f6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1229,6 +1229,19 @@ importers: specifier: ^2.11.0 version: 2.11.0(@testing-library/jest-dom@6.6.3)(solid-js@1.9.3)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) + packages/svelte-form: + dependencies: + '@tanstack/form-core': + specifier: workspace:* + version: link:../form-core + devDependencies: + premove: + specifier: ^4.0.0 + version: 4.0.0 + svelte: + specifier: ^5.16.1 + version: 5.16.1 + packages/valibot-form-adapter: dependencies: '@tanstack/form-core': @@ -5204,6 +5217,10 @@ packages: axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -6249,6 +6266,9 @@ packages: jiti: optional: true + esm-env@1.2.1: + resolution: {integrity: sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==} + espree@10.3.0: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6262,6 +6282,9 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@1.3.2: + resolution: {integrity: sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -7467,6 +7490,9 @@ packages: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -8597,6 +8623,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + premove@4.0.0: + resolution: {integrity: sha512-zim/Hr4+FVdCIM7zL9b9Z0Wfd5Ya3mnKtiuDv7L5lzYzanSq6cOcVJ7EFcgK4I0pt28l8H0jX/x3nyog380XgQ==} + engines: {node: '>=6'} + hasBin: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -9510,6 +9541,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte@5.16.1: + resolution: {integrity: sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==} + engines: {node: '>=18'} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -10546,6 +10581,9 @@ packages: yup@1.5.0: resolution: {integrity: sha512-NJfBIHnp1QbqZwxcgl6irnDMIsb/7d1prNhFx02f1kp8h+orpi4xs3w90szNpOh68a/iHPdMsYvhZWoDmUvXBQ==} + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -14850,6 +14888,8 @@ snapshots: transitivePeerDependencies: - debug + axobject-query@4.1.0: {} + b4a@1.6.7: {} babel-dead-code-elimination@1.0.6: @@ -16155,6 +16195,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.1: {} + espree@10.3.0: dependencies: acorn: 8.14.0 @@ -16167,6 +16209,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@1.3.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -17508,6 +17554,8 @@ snapshots: mlly: 1.7.3 pkg-types: 1.2.1 + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -19014,6 +19062,8 @@ snapshots: prelude-ls@1.2.1: {} + premove@4.0.0: {} + prettier@2.8.8: {} prettier@3.4.2: {} @@ -19981,6 +20031,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.16.1: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + acorn: 8.14.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.1 + esrap: 1.3.2 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.12 + zimmerframe: 1.1.2 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} @@ -21112,6 +21179,8 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zimmerframe@1.1.2: {} + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 From 4e53bdb0e6527889cb745e962d2749682690aa21 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:08:22 +0000 Subject: [PATCH 02/17] wip: add vite and fix some types --- packages/svelte-form/package.json | 3 +- packages/svelte-form/src/createField.ts | 36 +++++++- packages/svelte-form/src/createForm.ts | 6 +- packages/svelte-form/src/index.ts | 2 +- packages/svelte-form/tsconfig.build.json | 2 +- packages/svelte-form/vite.config.ts | 5 ++ pnpm-lock.yaml | 106 ++++++++++++++++++----- 7 files changed, 130 insertions(+), 30 deletions(-) diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json index 892f427f0..7c0fddc92 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -48,7 +48,8 @@ }, "devDependencies": { "svelte": "^5.16.1", - "premove": "^4.0.0" + "premove": "^4.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.4" }, "peerDependencies": { "svelte": "^5.16.1" diff --git a/packages/svelte-form/src/createField.ts b/packages/svelte-form/src/createField.ts index 21880ba66..adfff9f6a 100644 --- a/packages/svelte-form/src/createField.ts +++ b/packages/svelte-form/src/createField.ts @@ -1,11 +1,12 @@ import { FieldApi } from '@tanstack/form-core' import { onDestroy, onMount } from 'svelte' -import Field from './Field.js' +import Field from './Field.svelte' import type { DeepKeys, DeepValue, FieldApiOptions, + Narrow, Validator, } from '@tanstack/form-core' @@ -18,6 +19,37 @@ interface SvelteFieldApi< Field: Field<TParentData, TFormValidator> } +export type CreateField< + TParentData, + TFormValidator extends + | Validator<TParentData, unknown> + | undefined = undefined, +> = < + TName extends DeepKeys<TParentData>, + TFieldValidator extends + | Validator<DeepValue<TParentData, TName>, unknown> + | undefined = undefined, + TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, +>( + opts: () => { name: Narrow<TName> } & Omit< + FieldApiOptions< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData + >, + 'form' + >, +) => () => FieldApi< + TParentData, + TName, + TFieldValidator, + TFormValidator, + TData +> & + SvelteFieldApi<TParentData, TFormValidator> + export function createField< TParentData, TName extends DeepKeys<TParentData>, @@ -64,5 +96,5 @@ export function createField< api.update(opts()) }) - return extendedApi + return () => extendedApi } diff --git a/packages/svelte-form/src/createForm.ts b/packages/svelte-form/src/createForm.ts index 3259ac141..6abce87ad 100644 --- a/packages/svelte-form/src/createForm.ts +++ b/packages/svelte-form/src/createForm.ts @@ -1,14 +1,16 @@ import { FormApi } from '@tanstack/form-core' import { onMount } from 'svelte' -import { Field, createField } from './createField' +import { createField } from './createField.js' +import Field from './Field.svelte'; import type { FormOptions, Validator } from '@tanstack/form-core' +import type { CreateField } from './createField.js'; export interface SvelteFormApi< TFormData, TFormValidator extends Validator<TFormData, unknown> | undefined = undefined, > { Field: Field<TFormData, TFormValidator> - createField: typeof createField<TFormData, TFormValidator> + createField: CreateField<TFormData, TFormValidator> } export function createForm< diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts index 2f308b888..51faab167 100644 --- a/packages/svelte-form/src/index.ts +++ b/packages/svelte-form/src/index.ts @@ -2,5 +2,5 @@ export * from '@tanstack/form-core' export { createForm, type SvelteFormApi } from './createForm.js' -export type { Field } from './Field.js' +export type { Field } from './Field.svelte' export { createField } from './createField.js' diff --git a/packages/svelte-form/tsconfig.build.json b/packages/svelte-form/tsconfig.build.json index 25a44d38f..3832dfb1a 100644 --- a/packages/svelte-form/tsconfig.build.json +++ b/packages/svelte-form/tsconfig.build.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "moduleResolution": "node16", + "moduleResolution": "bundler", "rootDir": "./src", "outDir": "./dist", "noEmit": false, diff --git a/packages/svelte-form/vite.config.ts b/packages/svelte-form/vite.config.ts index 8de48919a..3ec4909d6 100644 --- a/packages/svelte-form/vite.config.ts +++ b/packages/svelte-form/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vitest/config' +import { svelte } from '@sveltejs/vite-plugin-svelte'; import packageJson from './package.json' export default defineConfig({ @@ -11,4 +12,8 @@ export default defineConfig({ coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, }, + plugins: [ + svelte({ + }) + ] }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15ea96f6b..12e353efd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1235,6 +1235,9 @@ importers: specifier: workspace:* version: link:../form-core devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^4.0.4 + version: 4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) premove: specifier: ^4.0.0 version: 4.0.0 @@ -4271,6 +4274,21 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@sveltejs/vite-plugin-svelte-inspector@3.0.1': + resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^4.0.0-next.0||^4.0.0 + svelte: ^5.0.0-next.96 || ^5.0.0 + vite: ^5.0.0 + + '@sveltejs/vite-plugin-svelte@4.0.4': + resolution: {integrity: sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0-next.96 || ^5.0.0 + vite: ^5.0.0 + '@swc/core-darwin-arm64@1.7.42': resolution: {integrity: sha512-fWhaCs2+8GDRIcjExVDEIfbptVrxDqG8oHkESnXgymmvqTWzWei5SOnPNMS8Q+MYsn/b++Y2bDxkcwmq35Bvxg==} engines: {node: '>=10'} @@ -5810,6 +5828,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -7572,6 +7599,9 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magicast@0.2.11: resolution: {integrity: sha512-6saXbRDA1HMkqbsvHOU6HBjCVgZT460qheRkLhJQHWAbhXoWESI3Kn/dGGXyKs15FFKR85jsUqFx2sMK0wy/5g==} @@ -10964,7 +10994,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-plugin-utils': 7.25.9 - debug: 4.3.7 + debug: 4.4.0 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -12590,7 +12620,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -13355,7 +13385,7 @@ snapshots: estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 - magic-string: 0.30.12 + magic-string: 0.30.17 optionalDependencies: rollup: 4.26.0 @@ -13363,7 +13393,7 @@ snapshots: dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.26.0) estree-walker: 2.0.2 - magic-string: 0.30.12 + magic-string: 0.30.17 optionalDependencies: rollup: 4.26.0 @@ -13386,7 +13416,7 @@ snapshots: '@rollup/plugin-replace@5.0.7(rollup@4.26.0)': dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.26.0) - magic-string: 0.30.12 + magic-string: 0.30.17 optionalDependencies: rollup: 4.26.0 @@ -13578,6 +13608,28 @@ snapshots: eslint-visitor-keys: 4.2.0 espree: 10.3.0 + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)))(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) + debug: 4.4.0 + svelte: 5.16.1 + vite: 5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)))(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) + debug: 4.4.0 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.16.1 + vite: 5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0) + vitefu: 1.0.4(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) + transitivePeerDependencies: + - supports-color + '@swc/core-darwin-arm64@1.7.42': optional: true @@ -14141,7 +14193,7 @@ snapshots: '@typescript-eslint/types': 8.17.0 '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.17.0 - debug: 4.3.7 + debug: 4.4.0 eslint: 9.16.0(jiti@2.4.0) optionalDependencies: typescript: 5.6.3 @@ -14171,7 +14223,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.17.0 '@typescript-eslint/visitor-keys': 8.17.0 - debug: 4.3.7 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14405,7 +14457,7 @@ snapshots: dependencies: '@vitest/spy': 2.1.4 estree-walker: 3.0.3 - magic-string: 0.30.12 + magic-string: 0.30.17 optionalDependencies: vite: 5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0) @@ -14421,7 +14473,7 @@ snapshots: '@vitest/snapshot@2.1.4': dependencies: '@vitest/pretty-format': 2.1.4 - magic-string: 0.30.12 + magic-string: 0.30.17 pathe: 1.1.2 '@vitest/spy@2.1.4': @@ -14686,13 +14738,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -15523,6 +15575,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decimal.js@10.4.3: {} decode-formdata@0.8.0: {} @@ -16849,7 +16905,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -16889,14 +16945,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.7 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -16985,7 +17041,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.7 + debug: 4.4.0 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -17628,6 +17684,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.2.11: dependencies: '@babel/parser': 7.26.2 @@ -18030,7 +18090,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -19807,7 +19867,7 @@ snapshots: socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 - debug: 4.3.7 + debug: 4.4.0 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -19869,7 +19929,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.3.7 + debug: 4.4.0 detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -19880,7 +19940,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.3.7 + debug: 4.4.0 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -20235,7 +20295,7 @@ snapshots: tuf-js@3.0.1: dependencies: '@tufjs/models': 3.0.1 - debug: 4.3.7 + debug: 4.4.0 make-fetch-happen: 14.0.3 transitivePeerDependencies: - supports-color @@ -20381,7 +20441,7 @@ snapshots: estree-walker: 3.0.3 fast-glob: 3.3.2 local-pkg: 0.5.1 - magic-string: 0.30.12 + magic-string: 0.30.17 mlly: 1.7.3 pathe: 1.1.2 pkg-types: 1.2.1 @@ -20485,7 +20545,7 @@ snapshots: unwasm@0.3.9(webpack-sources@3.2.3): dependencies: knitwork: 1.1.0 - magic-string: 0.30.12 + magic-string: 0.30.17 mlly: 1.7.3 pathe: 1.1.2 pkg-types: 1.2.1 @@ -20711,7 +20771,7 @@ snapshots: vite-node@2.1.4(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.4.0 pathe: 1.1.2 vite: 5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0) transitivePeerDependencies: From a105341e533b7986dffe3698e79705791f8db812 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:45:16 +0000 Subject: [PATCH 03/17] fix: correct language attribute --- packages/svelte-form/src/Field.svelte | 2 +- packages/svelte-form/tests/simple.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index fa4194300..852f2ba67 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -1,5 +1,5 @@ <!-- TODO (43081j): figure out how to reference types in generics --> -<script type="ts" generics="TParentData, +<script lang="ts" generics="TParentData, TName extends DeepKeys<TParentData>, TFieldValidator extends | Validator<DeepValue<TParentData, TName>, unknown> diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte index 38a510ac1..dbc98cf90 100644 --- a/packages/svelte-form/tests/simple.svelte +++ b/packages/svelte-form/tests/simple.svelte @@ -1,4 +1,4 @@ -<script type="ts"> +<script lang="ts"> import { createForm } from '../src/index.js' import type { FieldApi, FormOptions } from '../src/index.js' From 3bd7f332482e4bbeaf46c72c2feba55adefe5a12 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:58:39 +0000 Subject: [PATCH 04/17] chore: shuffle things around to use svelte files --- packages/svelte-form/index.html | 20 +++++++++++++++++++ packages/svelte-form/src/Field.svelte | 5 +++-- .../{createField.ts => createField.svelte.ts} | 0 .../{createForm.ts => createForm.svelte.ts} | 4 ++-- packages/svelte-form/src/index.ts | 4 ++-- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 packages/svelte-form/index.html rename packages/svelte-form/src/{createField.ts => createField.svelte.ts} (100%) rename packages/svelte-form/src/{createForm.ts => createForm.svelte.ts} (91%) diff --git a/packages/svelte-form/index.html b/packages/svelte-form/index.html new file mode 100644 index 000000000..5691a0f20 --- /dev/null +++ b/packages/svelte-form/index.html @@ -0,0 +1,20 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Svelte</title> + </head> + <body> + <div id="app"></div> + <script type="module"> + import * as ts from './src/index.ts'; + import { mount } from 'svelte'; + import App from './tests/simple.svelte'; + + const app = mount(App, { + target: document.getElementById('app') + }); + </script> + </body> +</html> diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index 852f2ba67..dc3392eb8 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -11,7 +11,7 @@ "> import type { Snippet } from 'svelte'; // TODO (43081j): somehow remove this circular reference - import { createField } from './createField'; + import { createField } from './createField.svelte.js'; type Props = { children: Snippet<[ @@ -33,7 +33,8 @@ >; let { - children + children, + ...fieldOptions }: Props = $props(); const fieldApi = createField< diff --git a/packages/svelte-form/src/createField.ts b/packages/svelte-form/src/createField.svelte.ts similarity index 100% rename from packages/svelte-form/src/createField.ts rename to packages/svelte-form/src/createField.svelte.ts diff --git a/packages/svelte-form/src/createForm.ts b/packages/svelte-form/src/createForm.svelte.ts similarity index 91% rename from packages/svelte-form/src/createForm.ts rename to packages/svelte-form/src/createForm.svelte.ts index 6abce87ad..633afad51 100644 --- a/packages/svelte-form/src/createForm.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,9 +1,9 @@ import { FormApi } from '@tanstack/form-core' import { onMount } from 'svelte' -import { createField } from './createField.js' +import { createField } from './createField.svelte.js' import Field from './Field.svelte'; import type { FormOptions, Validator } from '@tanstack/form-core' -import type { CreateField } from './createField.js'; +import type { CreateField } from './createField.svelte.js'; export interface SvelteFormApi< TFormData, diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts index 51faab167..db0fbed4b 100644 --- a/packages/svelte-form/src/index.ts +++ b/packages/svelte-form/src/index.ts @@ -1,6 +1,6 @@ export * from '@tanstack/form-core' -export { createForm, type SvelteFormApi } from './createForm.js' +export { createForm, type SvelteFormApi } from './createForm.svelte.js' export type { Field } from './Field.svelte' -export { createField } from './createField.js' +export { createField } from './createField.svelte.js' From 5b87ca554045301c2c02761c7af74f1fd7e5f8f9 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:27:57 +0000 Subject: [PATCH 05/17] fix: correct a whole bunch of bad refs/types --- packages/svelte-form/src/Field.svelte | 4 ++-- packages/svelte-form/src/createField.svelte.ts | 2 +- packages/svelte-form/tests/simple.svelte | 12 ++++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index dc3392eb8..45de85957 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -12,6 +12,7 @@ import type { Snippet } from 'svelte'; // TODO (43081j): somehow remove this circular reference import { createField } from './createField.svelte.js'; + import type { FieldOptions } from '@tanstack/form-core'; type Props = { children: Snippet<[ @@ -23,8 +24,7 @@ TData > ]> - [key: string]: unknown; - } & FieldApiOptions< + } & FieldOptions< TParentData, TName, TFieldValidator, diff --git a/packages/svelte-form/src/createField.svelte.ts b/packages/svelte-form/src/createField.svelte.ts index adfff9f6a..7af842621 100644 --- a/packages/svelte-form/src/createField.svelte.ts +++ b/packages/svelte-form/src/createField.svelte.ts @@ -96,5 +96,5 @@ export function createField< api.update(opts()) }) - return () => extendedApi + return extendedApi } diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte index dbc98cf90..29ba29405 100644 --- a/packages/svelte-form/tests/simple.svelte +++ b/packages/svelte-form/tests/simple.svelte @@ -41,6 +41,7 @@ const form = createForm(() => ({ <form.Field name="firstName" + form={form} validators={{ onChange: ({ value }) => value.length < 3 ? 'Not long enough' : undefined, @@ -65,6 +66,7 @@ const form = createForm(() => ({ </form.Field> <form.Field name="lastName" + form={form} validators={{ onChange: ({ value }) => value.length < 3 ? 'Not long enough' : undefined, @@ -89,6 +91,7 @@ const form = createForm(() => ({ </form.Field> <form.Field name="color" + form={form} > {#snippet children(field)} <div> @@ -112,6 +115,7 @@ const form = createForm(() => ({ </form.Field> <form.Field name="employed" + form={form} > {#snippet children(field)} <div> @@ -157,19 +161,19 @@ const form = createForm(() => ({ <div> <button type="submit" - disabled={form.api.state.isSubmitting} + disabled={form.state.isSubmitting} > - {form.api.state.isSubmitting ? html` Submitting` : 'Submit'} + {form.state.isSubmitting ? html` Submitting` : 'Submit'} </button> <button type="button" id="reset" on:click={() => { - form.api.reset() + form.reset() }} > Reset </button> </div> </form> -<pre>{JSON.stringify(form.api.state, null, 2)}</pre> +<pre>{JSON.stringify(form.state, null, 2)}</pre> From 43aa84cec07392ff5aa7d760799b429901a8dfe3 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:35:04 +0000 Subject: [PATCH 06/17] fix: return FieldApi directly --- packages/svelte-form/src/createField.svelte.ts | 2 +- packages/svelte-form/src/createForm.svelte.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte-form/src/createField.svelte.ts b/packages/svelte-form/src/createField.svelte.ts index 7af842621..0e25747d2 100644 --- a/packages/svelte-form/src/createField.svelte.ts +++ b/packages/svelte-form/src/createField.svelte.ts @@ -41,7 +41,7 @@ export type CreateField< >, 'form' >, -) => () => FieldApi< +) => FieldApi< TParentData, TName, TFieldValidator, diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index 633afad51..e45a76c7d 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -29,6 +29,7 @@ export function createForm< // No clue right now how we do that extendedApi.Field = Field extendedApi.createField = (props) => + // TODO (43081j): type is excessively deep.. no clue why yet createField(() => { return { ...props(), form: api } }) From 2b5208110f305ab2fa3f2b613bc9a914c8bca237 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:42:28 +1100 Subject: [PATCH 07/17] Build with @sveltejs/package --- .gitignore | 1 + packages/svelte-form/package.json | 20 +++++----- pnpm-lock.yaml | 65 ++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index d60197e0c..cc376bd6c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ stats.html .nx/workspace-data .pnpm-store .tsup +.svelte-kit vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json index 7c0fddc92..08c7b7f8b 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -11,7 +11,7 @@ }, "homepage": "https://tanstack.com/form", "scripts": { - "clean": "premove ./build ./coverage", + "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src ./tests", "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", @@ -23,18 +23,17 @@ "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", - "build": "tsc -p tsconfig.build.json" + "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", - "main": "dist/index.js", "module": "dist/index.js", + "svelte": "./dist/index.js", "exports": { ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "import": "./dist/index.js" }, "./package.json": "./package.json" }, @@ -47,11 +46,12 @@ "@tanstack/form-core": "workspace:*" }, "devDependencies": { - "svelte": "^5.16.1", + "@sveltejs/package": "^2.3.7", + "@sveltejs/vite-plugin-svelte": "^4.0.4", "premove": "^4.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.4" + "svelte": "^5.16.1" }, "peerDependencies": { - "svelte": "^5.16.1" + "svelte": "^5.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12e353efd..f34655404 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1235,6 +1235,9 @@ importers: specifier: workspace:* version: link:../form-core devDependencies: + '@sveltejs/package': + specifier: ^2.3.7 + version: 2.3.7(svelte@5.16.1)(typescript@5.7.2) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.4 version: 4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) @@ -4274,6 +4277,13 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@sveltejs/package@2.3.7': + resolution: {integrity: sha512-LYgUkde5GUYqOpXbcoCGUpEH4Ctl3Wj4u4CVZBl56dEeLW5fGHE037ZL1qlK0Ky+QD5uUfwONSeGwIOIighFMQ==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + '@sveltejs/vite-plugin-svelte-inspector@3.0.1': resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} @@ -5846,6 +5856,9 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -7568,6 +7581,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -8064,6 +8080,9 @@ packages: xml2js: optional: true + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-addon-api@6.1.0: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} @@ -8424,6 +8443,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -9571,6 +9593,12 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte2tsx@0.7.34: + resolution: {integrity: sha512-WTMhpNhFf8/h3SMtR5dkdSy2qfveomkhYei/QW9gSPccb0/b82tjHvLop6vT303ZkGswU/da1s6XvrLgthQPCw==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + svelte@5.16.1: resolution: {integrity: sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==} engines: {node: '>=18'} @@ -13608,6 +13636,17 @@ snapshots: eslint-visitor-keys: 4.2.0 espree: 10.3.0 + '@sveltejs/package@2.3.7(svelte@5.16.1)(typescript@5.7.2)': + dependencies: + chokidar: 4.0.1 + kleur: 4.1.5 + sade: 1.8.1 + semver: 7.6.3 + svelte: 5.16.1 + svelte2tsx: 0.7.34(svelte@5.16.1)(typescript@5.7.2) + transitivePeerDependencies: + - typescript + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)))(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0))': dependencies: '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.16.1)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)) @@ -15587,6 +15626,8 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent-js@1.0.1: {} + dedent@1.5.3(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -17657,6 +17698,10 @@ snapshots: loupe@3.1.2: {} + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + lru-cache@10.4.3: {} lru-cache@11.0.2: {} @@ -18453,6 +18498,11 @@ snapshots: - uWebSockets.js - webpack-sources + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + node-addon-api@6.1.0: optional: true @@ -18921,6 +18971,11 @@ snapshots: parseurl@1.3.3: {} + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -20091,6 +20146,13 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte2tsx@0.7.34(svelte@5.16.1)(typescript@5.7.2): + dependencies: + dedent-js: 1.0.1 + pascal-case: 3.1.2 + svelte: 5.16.1 + typescript: 5.7.2 + svelte@5.16.1: dependencies: '@ampproject/remapping': 2.3.0 @@ -20372,8 +20434,7 @@ snapshots: typescript@5.6.3: {} - typescript@5.7.2: - optional: true + typescript@5.7.2: {} uc.micro@2.1.0: {} From 2f0f6a1715351e11795c15a3487dde85603fbb0a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:44:24 +0000 Subject: [PATCH 08/17] ci: apply automated fixes and generate docs --- packages/svelte-form/index.html | 18 +++++++++--------- packages/svelte-form/src/createField.svelte.ts | 16 ++-------------- packages/svelte-form/src/createForm.svelte.ts | 4 ++-- packages/svelte-form/vite.config.ts | 7 ++----- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/packages/svelte-form/index.html b/packages/svelte-form/index.html index 5691a0f20..f8b7e0579 100644 --- a/packages/svelte-form/index.html +++ b/packages/svelte-form/index.html @@ -6,15 +6,15 @@ <title>Svelte</title> </head> <body> - <div id="app"></div> - <script type="module"> - import * as ts from './src/index.ts'; - import { mount } from 'svelte'; - import App from './tests/simple.svelte'; + <div id="app"></div> + <script type="module"> + import * as ts from './src/index.ts' + import { mount } from 'svelte' + import App from './tests/simple.svelte' - const app = mount(App, { - target: document.getElementById('app') - }); - </script> + const app = mount(App, { + target: document.getElementById('app'), + }) + </script> </body> </html> diff --git a/packages/svelte-form/src/createField.svelte.ts b/packages/svelte-form/src/createField.svelte.ts index 0e25747d2..75017799c 100644 --- a/packages/svelte-form/src/createField.svelte.ts +++ b/packages/svelte-form/src/createField.svelte.ts @@ -32,22 +32,10 @@ export type CreateField< TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, >( opts: () => { name: Narrow<TName> } & Omit< - FieldApiOptions< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData - >, + FieldApiOptions<TParentData, TName, TFieldValidator, TFormValidator, TData>, 'form' >, -) => FieldApi< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData -> & +) => FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData> & SvelteFieldApi<TParentData, TFormValidator> export function createField< diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index e45a76c7d..fd97db98d 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,9 +1,9 @@ import { FormApi } from '@tanstack/form-core' import { onMount } from 'svelte' import { createField } from './createField.svelte.js' -import Field from './Field.svelte'; +import Field from './Field.svelte' import type { FormOptions, Validator } from '@tanstack/form-core' -import type { CreateField } from './createField.svelte.js'; +import type { CreateField } from './createField.svelte.js' export interface SvelteFormApi< TFormData, diff --git a/packages/svelte-form/vite.config.ts b/packages/svelte-form/vite.config.ts index 3ec4909d6..59194a81b 100644 --- a/packages/svelte-form/vite.config.ts +++ b/packages/svelte-form/vite.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vitest/config' -import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { svelte } from '@sveltejs/vite-plugin-svelte' import packageJson from './package.json' export default defineConfig({ @@ -12,8 +12,5 @@ export default defineConfig({ coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, }, - plugins: [ - svelte({ - }) - ] + plugins: [svelte({})], }) From c2657de944b9324bc48fb406d8ffc3dab9cfdabb Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Fri, 7 Mar 2025 20:51:51 +0100 Subject: [PATCH 09/17] implement svelte version - one Field component with a module script with createField in it - createForm file - bunch of types that pass along generics, closely modeled after the other adapters - tests --- packages/svelte-form/package.json | 13 +- packages/svelte-form/src/Field.svelte | 287 ++++++++++++-- packages/svelte-form/src/Subscribe.svelte | 16 + .../svelte-form/src/createField.svelte.ts | 88 ----- packages/svelte-form/src/createForm.svelte.ts | 240 +++++++++-- packages/svelte-form/src/index.ts | 8 +- packages/svelte-form/src/types.ts | 374 ++++++++++++++++++ packages/svelte-form/tests/simple.svelte | 168 +++----- packages/svelte-form/tests/simple.test.ts | 61 ++- packages/svelte-form/tsconfig.json | 3 +- packages/svelte-form/vite.config.ts | 6 + 11 files changed, 956 insertions(+), 308 deletions(-) create mode 100644 packages/svelte-form/src/Subscribe.svelte delete mode 100644 packages/svelte-form/src/createField.svelte.ts create mode 100644 packages/svelte-form/src/types.ts diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json index 08c7b7f8b..047b822b0 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -1,8 +1,8 @@ { "name": "@tanstack/svelte-form", - "version": "0.0.1", + "version": "1.0.0", "description": "Powerful, type-safe forms for Svelte.", - "author": "James Garbutt (https://github.com/43081j)", + "author": "tannerlinsley", "license": "MIT", "repository": { "type": "git", @@ -43,13 +43,14 @@ "src" ], "dependencies": { - "@tanstack/form-core": "workspace:*" + "@tanstack/form-core": "workspace:*", + "@tanstack/svelte-store": "^0.7.0" }, "devDependencies": { - "@sveltejs/package": "^2.3.7", - "@sveltejs/vite-plugin-svelte": "^4.0.4", + "@sveltejs/package": "^2.3.10", + "@sveltejs/vite-plugin-svelte": "^5.0.3", "premove": "^4.0.0", - "svelte": "^5.16.1" + "svelte": "^5.20.2" }, "peerDependencies": { "svelte": "^5.0.0" diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index 45de85957..62032794c 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -1,48 +1,267 @@ -<!-- TODO (43081j): figure out how to reference types in generics --> -<script lang="ts" generics="TParentData, - TName extends DeepKeys<TParentData>, - TFieldValidator extends - | Validator<DeepValue<TParentData, TName>, unknown> - | undefined = undefined, - TFormValidator extends - | Validator<TParentData, unknown> - | undefined = undefined, - TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName> -"> - import type { Snippet } from 'svelte'; - // TODO (43081j): somehow remove this circular reference - import { createField } from './createField.svelte.js'; - import type { FieldOptions } from '@tanstack/form-core'; - - type Props = { - children: Snippet<[ - FieldApi< +<script module lang="ts"> + import { + type DeepKeys, + type DeepValue, + FieldApi, + type FieldAsyncValidateOrFn, + type FieldValidateOrFn, + type FormAsyncValidateOrFn, + type FormValidateOrFn, + } from '@tanstack/form-core' + import { useStore } from '@tanstack/svelte-store' + import { onMount, type Snippet } from 'svelte' + import Field from './Field.svelte' + import type { CreateFieldOptions, SvelteFieldApi } from './types.js' + + export function createField< + TParentData, + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, + >( + opts: () => CreateFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + ) { + const options = opts() + + const api = new FieldApi(options) + + const extendedApi: typeof api & + SvelteFieldApi< TParentData, - TName, - TFieldValidator, - TFormValidator, - TData - > - ]> - } & FieldOptions< + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > = api as never + + extendedApi.Field = Field as never + + let mounted = false + // Instantiates field meta and removes it when unrendered + onMount(() => { + const cleanupFn = api.mount() + mounted = true + return () => { + cleanupFn() + mounted = false + } + }) + + // TODO (43081j): does this do what i think? we don't access anything + // svelte is aware of, so maybe it'll never call this? + $effect.pre(() => { + const current = opts() + Object.values(current) // make sure we're watching all the things + if (!mounted) return + api.update(current) + }) + + const storeSub = useStore(api.store) + Object.defineProperty(extendedApi, 'state', { + get() { + return storeSub.current + }, + }) + + return extendedApi + } +</script> + +<script + lang="ts" + generics=" + TParentData, + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, +" +> + type Props< + TParentData, + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, + > = { + children: Snippet< + [ + FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + ] + > + } & CreateFieldOptions< TParentData, TName, - TFieldValidator, - TFormValidator, - TData - >; + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > let { children, ...fieldOptions - }: Props = $props(); + }: Props< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > = $props() const fieldApi = createField< TParentData, TName, - TFieldValidator, - TFormValidator, - TData + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta >(() => { return fieldOptions }) diff --git a/packages/svelte-form/src/Subscribe.svelte b/packages/svelte-form/src/Subscribe.svelte new file mode 100644 index 000000000..4f5f255a2 --- /dev/null +++ b/packages/svelte-form/src/Subscribe.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + import { useStore } from '@tanstack/svelte-store' + + // Don't bother typing this out, this component is only usable through a wrapper + interface Props { + children: any + store: any + selector?: (state: any) => any + } + + let { children, store, selector = (state) => state }: Props = $props() + + const value = useStore(store, selector) +</script> + +{@render children(value.current)} diff --git a/packages/svelte-form/src/createField.svelte.ts b/packages/svelte-form/src/createField.svelte.ts deleted file mode 100644 index 75017799c..000000000 --- a/packages/svelte-form/src/createField.svelte.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { FieldApi } from '@tanstack/form-core' -import { onDestroy, onMount } from 'svelte' -import Field from './Field.svelte' - -import type { - DeepKeys, - DeepValue, - FieldApiOptions, - Narrow, - Validator, -} from '@tanstack/form-core' - -interface SvelteFieldApi< - TParentData, - TFormValidator extends - | Validator<TParentData, unknown> - | undefined = undefined, -> { - Field: Field<TParentData, TFormValidator> -} - -export type CreateField< - TParentData, - TFormValidator extends - | Validator<TParentData, unknown> - | undefined = undefined, -> = < - TName extends DeepKeys<TParentData>, - TFieldValidator extends - | Validator<DeepValue<TParentData, TName>, unknown> - | undefined = undefined, - TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, ->( - opts: () => { name: Narrow<TName> } & Omit< - FieldApiOptions<TParentData, TName, TFieldValidator, TFormValidator, TData>, - 'form' - >, -) => FieldApi<TParentData, TName, TFieldValidator, TFormValidator, TData> & - SvelteFieldApi<TParentData, TFormValidator> - -export function createField< - TParentData, - TName extends DeepKeys<TParentData>, - TFieldValidator extends - | Validator<DeepValue<TParentData, TName>, unknown> - | undefined = undefined, - TFormValidator extends - | Validator<TParentData, unknown> - | undefined = undefined, - TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>, ->( - opts: () => FieldApiOptions< - TParentData, - TName, - TFieldValidator, - TFormValidator, - TData - >, -) { - const options = opts() - - const api = new FieldApi(options) - - const extendedApi: typeof api & SvelteFieldApi<TParentData, TFormValidator> = - api as never - - extendedApi.Field = Field as never - - let mounted = false - // Instantiates field meta and removes it when unrendered - onMount(() => { - const cleanupFn = api.mount() - mounted = true - onDestroy(() => { - cleanupFn() - mounted = false - }) - }) - - // TODO (43081j): does this do what i think? we don't access anything - // svelte is aware of, so maybe it'll never call this? - $effect(() => { - if (!mounted) return - api.update(opts()) - }) - - return extendedApi -} diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index fd97db98d..e44c37a5b 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,44 +1,236 @@ import { FormApi } from '@tanstack/form-core' +import { useStore } from '@tanstack/svelte-store' import { onMount } from 'svelte' -import { createField } from './createField.svelte.js' -import Field from './Field.svelte' -import type { FormOptions, Validator } from '@tanstack/form-core' -import type { CreateField } from './createField.svelte.js' +// @ts-ignore tsc doesn't know about named exports from svelte files +import Field, { createField } from './Field.svelte' +import Subscribe from './Subscribe.svelte' +import type { + Component, + ComponentConstructorOptions, + Snippet, + SvelteComponent, +} from 'svelte' +import type { + FormAsyncValidateOrFn, + FormOptions, + FormState, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { CreateField, FieldComponent, WithoutFunction } from './types.js' export interface SvelteFormApi< - TFormData, - TFormValidator extends Validator<TFormData, unknown> | undefined = undefined, + TParentData, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TSubmitMeta, > { - Field: Field<TFormData, TFormValidator> - createField: CreateField<TFormData, TFormValidator> + Field: FieldComponent< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + > + createField: CreateField< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + > + useStore: < + TSelected = NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + >( + selector?: ( + state: NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + ) => TSelected, + ) => { current: TSelected } + // This giant type allows the type + // - to be used as a function (which they are now in Svelte 5) + // - to be used as a class (which they were in Svelte 4, and which Svelte intellisense still uses for backwards compat) + // - to preserve the generics correctly + // Once Svelte intellisense no longer has/needs backwards compat, we can remove the class constructor part + Subscribe: (< + TSelected = NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + >( + internal: any, + props: { + selector?: ( + state: NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + ) => TSelected + children: Snippet<[NoInfer<TSelected>]> + }, + ) => {}) & + (new < + TSelected = NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + >( + opts: ComponentConstructorOptions<{ + selector?: ( + state: NoInfer< + FormState< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer + > + >, + ) => TSelected + children: Snippet<[NoInfer<TSelected>]> + }>, + ) => SvelteComponent) & + WithoutFunction<Component> } export function createForm< TParentData, - TFormValidator extends - | Validator<TParentData, unknown> - | undefined = undefined, ->(opts?: () => FormOptions<TParentData, TFormValidator>) { + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TSubmitMeta, +>( + opts?: () => FormOptions< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + >, +) { const options = opts?.() - const api = new FormApi<TParentData, TFormValidator>(options) - const extendedApi: typeof api & SvelteFormApi<TParentData, TFormValidator> = - api as never + const api = new FormApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + >(options) + const extendedApi: typeof api & + SvelteFormApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + > = api as never - // TODO (43081j): somehow this needs to actually be - // `<Field ...props form={api}>`. - // No clue right now how we do that - extendedApi.Field = Field + // @ts-expect-error constructor definition exists only on a type level + extendedApi.Field = (internal, props) => + Field(internal, { ...props, form: api }) extendedApi.createField = (props) => - // TODO (43081j): type is excessively deep.. no clue why yet createField(() => { return { ...props(), form: api } - }) + }) as never + extendedApi.useStore = (selector) => useStore(api.store, selector) + // @ts-expect-error constructor definition exists only on a type level + extendedApi.Subscribe = (internal, props) => + Subscribe(internal, { ...props, store: api.store }) onMount(api.mount) - // TODO (43081j): does this actually work? we don't use any observed - // data, so maybe svelte won't re-run this effect? - $effect(() => api.update(opts?.())) + // formApi.update should not have any side effects. Think of it like a `useRef` + // that we need to keep updated every render with the most up-to-date information. + $effect.pre(() => api.update(opts?.())) return extendedApi } diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts index db0fbed4b..0b24263e3 100644 --- a/packages/svelte-form/src/index.ts +++ b/packages/svelte-form/src/index.ts @@ -1,6 +1,10 @@ export * from '@tanstack/form-core' +export { useStore } from '@tanstack/svelte-store' + export { createForm, type SvelteFormApi } from './createForm.svelte.js' -export type { Field } from './Field.svelte' -export { createField } from './createField.svelte.js' +// @ts-ignore tsc doesn't know about named exports from svelte files +export { default as Field, createField } from './Field.svelte' + +export type { CreateField, FieldComponent } from './types.js' diff --git a/packages/svelte-form/src/types.ts b/packages/svelte-form/src/types.ts new file mode 100644 index 000000000..6b4ad3133 --- /dev/null +++ b/packages/svelte-form/src/types.ts @@ -0,0 +1,374 @@ +import type { + DeepKeys, + DeepValue, + FieldApi, + FieldApiOptions, + FieldAsyncValidateOrFn, + FieldValidateOrFn, + FormAsyncValidateOrFn, + FormValidateOrFn, + Narrow, +} from '@tanstack/form-core' +import type { + Component, + ComponentConstructorOptions, + Snippet, + SvelteComponent, +} from 'svelte' + +export type CreateFieldOptions< + TParentData, + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TSubmitMeta, +> = FieldApiOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta +> & { + mode?: 'value' | 'array' +} + +export type WithoutFunction<T> = { + [K in keyof T as T[K] extends Function ? never : K]: T[K] +} + +export interface SvelteFieldApi< + TParentData, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, +> { + Field: FieldComponent< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > +} + +export type FieldComponent< + TParentData, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, +> = + // This giant type allows the type + // - to be used as a function (which they are now in Svelte 5) + // - to be used as a class (which they were in Svelte 4, and which Svelte intellisense still uses for backwards compat) + // - to preserve the generics correctly + // Once Svelte intellisense no longer has/needs backwards compat, we can remove the class constructor part + (< + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + >( + internal: any, + { + children, + ...fieldOptions + }: Omit< + FieldComponentProps< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + 'form' + >, + ) => {}) & + (new < + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends + | undefined + | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends + | undefined + | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + >( + opts: ComponentConstructorOptions< + Omit< + FieldComponentProps< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + 'form' + > + >, + ) => SvelteComponent) & + WithoutFunction<Component> + +type FieldComponentProps< + TParentData, + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, +> = { + children: Snippet< + [ + FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + ] + > +} & Omit< + CreateFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + >, + 'form' +> + +export type CreateField< + TParentData, + TFormOnMount extends undefined | FormValidateOrFn<TParentData>, + TFormOnChange extends undefined | FormValidateOrFn<TParentData>, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnBlur extends undefined | FormValidateOrFn<TParentData>, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>, + TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>, + TParentSubmitMeta, +> = < + TName extends DeepKeys<TParentData>, + TData extends DeepValue<TParentData, TName>, + TOnMount extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChange extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnBlur extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TOnSubmit extends undefined | FieldValidateOrFn<TParentData, TName, TData>, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn<TParentData, TName, TData>, + TSubmitMeta, +>( + opts: () => { name: Narrow<TName> } & Omit< + CreateFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TSubmitMeta + >, + 'form' + >, +) => () => FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta +> & + SvelteFieldApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnServer, + TParentSubmitMeta + > diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte index 29ba29405..c48080ce5 100644 --- a/packages/svelte-form/tests/simple.svelte +++ b/packages/svelte-form/tests/simple.svelte @@ -1,47 +1,41 @@ -<script lang="ts"> -import { createForm } from '../src/index.js' -import type { FieldApi, FormOptions } from '../src/index.js' - -interface Employee { - firstName: string - lastName: string - color?: '#FF0000' | '#00FF00' | '#0000FF' - employed: boolean - jobTitle: string -} +<script module lang="ts"> + interface Employee { + firstName: string + lastName: string + } -const sampleData: Employee = { - firstName: 'Bob', - lastName: '', - employed: false, - jobTitle: '', -} + export const sampleData: Employee = { + firstName: 'Christian', + lastName: '', + } +</script> -const formConfig: FormOptions<Employee, undefined> = { - defaultValues: sampleData, -} +<script lang="ts"> + import { createForm } from '@tanstack/svelte-form' -const form = createForm(() => ({ - defaultValues: { - firstName: '', - lastName: '', - }, - onSubmit: async ({ value }) => { - // Do something with form data - console.log(value) - }, -})) + const form = createForm(() => ({ + defaultValues: sampleData, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(JSON.stringify(value)) + }, + })) + const state = form.useStore() </script> + <form id="form" - on:submit|preventDefault={form.onSubmit} + onsubmit={(e) => { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} > <h1>TanStack Form - Svelte Demo</h1> <form.Field name="firstName" - form={form} validators={{ onChange: ({ value }) => value.length < 3 ? 'Not long enough' : undefined, @@ -49,14 +43,14 @@ const form = createForm(() => ({ > {#snippet children(field)} <div> - <label>First Name</label> + <label for={field.name}>First Name</label> <input - id="firstName" + id={field.name} type="text" placeholder="First Name" value={field.state.value} - on:blur={() => field.handleBlur()} - on:input={(e: Event) => { + onblur={() => field.handleBlur()} + oninput={(e: Event) => { const target = e.target as HTMLInputElement field.handleChange(target.value) }} @@ -66,7 +60,6 @@ const form = createForm(() => ({ </form.Field> <form.Field name="lastName" - form={form} validators={{ onChange: ({ value }) => value.length < 3 ? 'Not long enough' : undefined, @@ -74,101 +67,41 @@ const form = createForm(() => ({ > {#snippet children(field)} <div> - <label>Last Name</label> + <label for={field.name}>Last Name</label> <input - id="lastName" + id={field.name} type="text" placeholder="Last Name" value={field.state.value} - on:blur={() => field.handleBlur()} - on:input={(e: Event) => { + onblur={() => field.handleBlur()} + oninput={(e: Event) => { const target = e.target as HTMLInputElement field.handleChange(target.value) }} /> + {#each field.state.meta.errors as error} + <em>{error}</em> + {/each} </div> {/snippet} </form.Field> - <form.Field - name="color" - form={form} - > - {#snippet children(field)} - <div> - <label>Favorite Color</label> - <select - value={field.state.value} - on:blur={() => field.handleBlur()} - on:input={(e: Event) => { - const target = e.target as HTMLInputElement - field.handleChange( - target.value as '#FF0000' | '#00FF00' | '#0000FF', - ) - }} - > - <option value="#FF0000">Red</option> - <option value="#00FF00">Green</option> - <option value="#0000FF">Blue</option> - </select> - </div> - {/snippet} - </form.Field> - <form.Field - name="employed" - form={form} - > - {#snippet children(field)} - <div> - <label>Employed?</label> - <input - on:input={() => field.handleChange(!field.state.value)} - checked={field.state.value} - on:blur={() => field.handleBlur()} - id="employed" - type="checkbox" - /> - </div> - {#if field.state.value} - <form.Field - name="jobTitle" - validators={{ - onChange: ({ value }) => - value.length === 0 - ? 'Needs to have a job here' - : null, - }} - > - {#snippet children(field)} - <div> - <label>Job Title</label> - <input - type="text" - id="jobTitle" - placeholder="Job Title" - value={subField.state.value} - on:blur={() => subField.handleBlur()} - on:input={(e: Event) => { - const target = e.target as HTMLInputElement - subField.handleChange(target.value) - }} - /> - </div> - {/snippet} - </form.Field> - {/if} - {/snippet} - </form.Field> <div> - <button - type="submit" - disabled={form.state.isSubmitting} + <form.Subscribe + selector={(state) => ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} > - {form.state.isSubmitting ? html` Submitting` : 'Submit'} - </button> + {#snippet children({ canSubmit, isSubmitting })} + <button type="submit" disabled={!canSubmit}> + {isSubmitting ? 'Submitting' : 'Submit'} + </button> + {/snippet} + </form.Subscribe> <button type="button" id="reset" - on:click={() => { + onclick={() => { form.reset() }} > @@ -176,4 +109,5 @@ const form = createForm(() => ({ </button> </div> </form> -<pre>{JSON.stringify(form.state, null, 2)}</pre> + +<pre>{JSON.stringify(state.current, null, 2)}</pre> diff --git a/packages/svelte-form/tests/simple.test.ts b/packages/svelte-form/tests/simple.test.ts index 586643e51..3fa23d8c6 100644 --- a/packages/svelte-form/tests/simple.test.ts +++ b/packages/svelte-form/tests/simple.test.ts @@ -2,75 +2,64 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import '@testing-library/jest-dom' import { userEvent } from '@testing-library/user-event' -import { mount } from 'svelte' -import TestForm from './simple' +import { mount, unmount } from 'svelte' +// @ts-ignore tsc doesn't know about named exports from svelte files +import TestForm, { sampleData } from './simple.svelte' describe('Svelte Tests', () => { - let element: TestForm + let element: HTMLDivElement + let instance: any beforeEach(async () => { element = document.createElement('div') document.body.appendChild(element) - mount(TestForm, { + instance = mount(TestForm, { target: element, - props: {}, }) }) afterEach(() => { + unmount(instance) element.remove() }) it('should have initial values', async () => { - expect( - await element.shadowRoot!.querySelector<HTMLInputElement>('#firstName'), - ).toHaveValue(sampleData.firstName) - expect( - await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName'), - ).toHaveValue('') - const form = element.form! - expect(form.api.getFieldValue('firstName')).toBe('Bob') - expect(form.api.getFieldMeta('firstName')?.isTouched).toBeFalsy() - expect(form.api.getFieldValue('lastName')).toBe('') - expect(form.api.getFieldMeta('lastName')?.isTouched).toBeFalsy() + expect(element.querySelector<HTMLInputElement>('#firstName')).toHaveValue( + sampleData.firstName, + ) + expect(element.querySelector<HTMLInputElement>('#lastName')).toHaveValue( + sampleData.lastName, + ) }) + it('should mirror user input', async () => { - const lastName = - await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName')! + const lastName = element.querySelector<HTMLInputElement>('#lastName')! const lastNameValue = 'Jobs' await userEvent.type(lastName, lastNameValue) - const form = element.form! - expect(form.api.getFieldValue('lastName')).toBe(lastNameValue) - expect(form.api.getFieldMeta('lastName')?.isTouched).toBeTruthy() + const form = JSON.parse(element.querySelector('pre')!.textContent!) + expect(form.values.lastName).toBe(lastNameValue) }) + it('Reset form to initial value', async () => { - const firstName = - await element.shadowRoot!.querySelector<HTMLInputElement>('#firstName')! + const firstName = element.querySelector<HTMLInputElement>('#firstName')! await userEvent.type(firstName, '-Joseph') - expect(firstName).toHaveValue('Christian-Joseph') + expect(firstName).toHaveValue(sampleData.firstName + '-Joseph') - const form = element.form - await element - .shadowRoot!.querySelector<HTMLButtonElement>('#reset') - ?.click() - expect(form.api.getFieldValue('firstName')).toBe('Bob') + await userEvent.click(element.querySelector<HTMLButtonElement>('#reset')!) + expect(firstName).toHaveValue(sampleData.firstName) }) it('should display validation', async () => { - const lastName = - await element.shadowRoot!.querySelector<HTMLInputElement>('#lastName')! + const lastName = element.querySelector<HTMLInputElement>('#lastName')! const lastNameValue = 'Jo' await userEvent.type(lastName, lastNameValue) expect(lastName).toHaveValue('Jo') - const form = element.form - expect(form.api.getFieldMeta('lastName')?.errors[0]).toBe('Not long enough') + expect(element.querySelector('em')?.textContent).toBe('Not long enough') await userEvent.type(lastName, lastNameValue) expect(await lastName.getAttribute('error-text')).toBeFalsy() - expect(form.api.getFieldValue('lastName')).toBe('JoJo') - expect(form.api.getFieldMeta('lastName')?.isTouched).toBeTruthy() - expect(form.api.getFieldMeta('lastName')?.errors.length).toBeFalsy() + expect(element.querySelector('em')).not.toBeInTheDocument() }) }) diff --git a/packages/svelte-form/tsconfig.json b/packages/svelte-form/tsconfig.json index c7651b3dc..0d8e5e4c6 100644 --- a/packages/svelte-form/tsconfig.json +++ b/packages/svelte-form/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "moduleResolution": "node16" + "module": "NodeNext", + "moduleResolution": "NodeNext" }, "include": ["src", "tests", "eslint.config.js", "vite.config.ts"] } diff --git a/packages/svelte-form/vite.config.ts b/packages/svelte-form/vite.config.ts index 59194a81b..1317cf4eb 100644 --- a/packages/svelte-form/vite.config.ts +++ b/packages/svelte-form/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vitest/config' import { svelte } from '@sveltejs/vite-plugin-svelte' +// @ts-expect-error tsconfig with NodeNext throws an annoying error here which is not relevant import packageJson from './package.json' export default defineConfig({ @@ -12,5 +13,10 @@ export default defineConfig({ coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, typecheck: { enabled: true }, }, + resolve: process.env.VITEST + ? { + conditions: ['browser'], + } + : undefined, plugins: [svelte({})], }) From c4e8350952658607ef096d8791bc31f8804246a0 Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Fri, 7 Mar 2025 20:51:58 +0100 Subject: [PATCH 10/17] add svelte examples --- examples/svelte/array/.gitignore | 24 ++++ examples/svelte/array/README.md | 6 + examples/svelte/array/index.html | 13 ++ examples/svelte/array/package.json | 21 +++ examples/svelte/array/src/App.svelte | 55 +++++++ examples/svelte/array/src/main.ts | 8 ++ examples/svelte/array/src/vite-env.d.ts | 2 + examples/svelte/array/svelte.config.js | 7 + examples/svelte/array/tsconfig.json | 20 +++ examples/svelte/array/vite.config.ts | 7 + examples/svelte/simple/.gitignore | 24 ++++ examples/svelte/simple/README.md | 6 + examples/svelte/simple/index.html | 13 ++ examples/svelte/simple/package.json | 21 +++ examples/svelte/simple/src/App.svelte | 150 ++++++++++++++++++++ examples/svelte/simple/src/FieldInfo.svelte | 12 ++ examples/svelte/simple/src/main.ts | 8 ++ examples/svelte/simple/src/vite-env.d.ts | 2 + examples/svelte/simple/svelte.config.js | 7 + examples/svelte/simple/tsconfig.json | 20 +++ examples/svelte/simple/vite.config.ts | 7 + pnpm-lock.yaml | 96 ++++++++++--- pnpm-workspace.yaml | 1 + 23 files changed, 513 insertions(+), 17 deletions(-) create mode 100644 examples/svelte/array/.gitignore create mode 100644 examples/svelte/array/README.md create mode 100644 examples/svelte/array/index.html create mode 100644 examples/svelte/array/package.json create mode 100644 examples/svelte/array/src/App.svelte create mode 100644 examples/svelte/array/src/main.ts create mode 100644 examples/svelte/array/src/vite-env.d.ts create mode 100644 examples/svelte/array/svelte.config.js create mode 100644 examples/svelte/array/tsconfig.json create mode 100644 examples/svelte/array/vite.config.ts create mode 100644 examples/svelte/simple/.gitignore create mode 100644 examples/svelte/simple/README.md create mode 100644 examples/svelte/simple/index.html create mode 100644 examples/svelte/simple/package.json create mode 100644 examples/svelte/simple/src/App.svelte create mode 100644 examples/svelte/simple/src/FieldInfo.svelte create mode 100644 examples/svelte/simple/src/main.ts create mode 100644 examples/svelte/simple/src/vite-env.d.ts create mode 100644 examples/svelte/simple/svelte.config.js create mode 100644 examples/svelte/simple/tsconfig.json create mode 100644 examples/svelte/simple/vite.config.ts diff --git a/examples/svelte/array/.gitignore b/examples/svelte/array/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/svelte/array/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/svelte/array/README.md b/examples/svelte/array/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/svelte/array/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/array/index.html b/examples/svelte/array/index.html new file mode 100644 index 000000000..b6c5f0afa --- /dev/null +++ b/examples/svelte/array/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Vite + Svelte + TS</title> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.ts"></script> + </body> +</html> diff --git a/examples/svelte/array/package.json b/examples/svelte/array/package.json new file mode 100644 index 000000000..dc04b9fe9 --- /dev/null +++ b/examples/svelte/array/package.json @@ -0,0 +1,21 @@ +{ + "name": "@tanstack/form-example-svelte-array", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/svelte-form": "^1.0.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.20.2", + "typescript": "5.8.2", + "vite": "^6.1.1" + } +} diff --git a/examples/svelte/array/src/App.svelte b/examples/svelte/array/src/App.svelte new file mode 100644 index 000000000..4d8c02dcf --- /dev/null +++ b/examples/svelte/array/src/App.svelte @@ -0,0 +1,55 @@ +<script lang="ts"> + import { createForm } from '@tanstack/svelte-form' + + const form = createForm(() => ({ + defaultValues: { + people: [] as Array<{ age: number; name: string }>, + }, + onSubmit: ({ value }) => alert(JSON.stringify(value)), + })) +</script> + +<form + id="form" + onsubmit={(e) => { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} +> + <h1>TanStack Form - Svelte Demo</h1> + + <form.Field name="people"> + {#snippet children(field)} + <div> + {#each field.state.value as person, i} + <form.Field name={`people[${i}].name`}> + {#snippet children(subField)} + <div> + <label> + <div>Name for person {i}</div> + <input + value={person.name} + oninput={(e: Event) => { + const target = e.target as HTMLInputElement + subField.handleChange(target.value) + }} + /> + </label> + </div> + {/snippet} + </form.Field> + {/each} + + <button + onclick={() => field.pushValue({ name: '', age: 0 })} + type="button" + > + Add person + </button> + </div> + {/snippet} + </form.Field> + + <button type="submit"> Submit </button> +</form> diff --git a/examples/svelte/array/src/main.ts b/examples/svelte/array/src/main.ts new file mode 100644 index 000000000..928b6c527 --- /dev/null +++ b/examples/svelte/array/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/array/src/vite-env.d.ts b/examples/svelte/array/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/examples/svelte/array/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// <reference types="svelte" /> +/// <reference types="vite/client" /> diff --git a/examples/svelte/array/svelte.config.js b/examples/svelte/array/svelte.config.js new file mode 100644 index 000000000..b0683fd24 --- /dev/null +++ b/examples/svelte/array/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/array/tsconfig.json b/examples/svelte/array/tsconfig.json new file mode 100644 index 000000000..55a2f9b65 --- /dev/null +++ b/examples/svelte/array/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/examples/svelte/array/vite.config.ts b/examples/svelte/array/vite.config.ts new file mode 100644 index 000000000..d32eba1d6 --- /dev/null +++ b/examples/svelte/array/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/svelte/simple/.gitignore b/examples/svelte/simple/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/examples/svelte/simple/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/svelte/simple/README.md b/examples/svelte/simple/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/svelte/simple/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/simple/index.html b/examples/svelte/simple/index.html new file mode 100644 index 000000000..b6c5f0afa --- /dev/null +++ b/examples/svelte/simple/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Vite + Svelte + TS</title> + </head> + <body> + <div id="app"></div> + <script type="module" src="/src/main.ts"></script> + </body> +</html> diff --git a/examples/svelte/simple/package.json b/examples/svelte/simple/package.json new file mode 100644 index 000000000..13b7f3925 --- /dev/null +++ b/examples/svelte/simple/package.json @@ -0,0 +1,21 @@ +{ + "name": "@tanstack/form-example-svelte-simple", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/svelte-form": "^1.0.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.20.2", + "typescript": "5.8.2", + "vite": "^6.1.1" + } +} diff --git a/examples/svelte/simple/src/App.svelte b/examples/svelte/simple/src/App.svelte new file mode 100644 index 000000000..1ed0374c9 --- /dev/null +++ b/examples/svelte/simple/src/App.svelte @@ -0,0 +1,150 @@ +<script lang="ts"> + import { createForm } from '@tanstack/svelte-form' + import FieldInfo from './FieldInfo.svelte' + + const form = createForm(() => ({ + defaultValues: { + firstName: '', + lastName: '', + employed: false, + jobTitle: '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + alert(JSON.stringify(value)) + }, + })) +</script> + +<form + id="form" + onsubmit={(e) => { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} +> + <h1>TanStack Form - Svelte Demo</h1> + + <form.Field + name="firstName" + validators={{ + onChange: ({ value }) => + value.length < 3 ? 'Not long enough' : undefined, + onChangeAsyncDebounceMs: 500, + onChangeAsync: async ({ value }) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return value.includes('error') && 'No "error" allowed in first name' + }, + }} + > + {#snippet children(field)} + <div> + <label for={field.name}>First Name</label> + <input + id={field.name} + type="text" + placeholder="First Name" + value={field.state.value} + onblur={() => field.handleBlur()} + oninput={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + <FieldInfo {field} /> + </div> + {/snippet} + </form.Field> + <form.Field + name="lastName" + validators={{ + onChange: ({ value }) => + value.length < 3 ? 'Not long enough' : undefined, + }} + > + {#snippet children(field)} + <div> + <label for={field.name}>Last Name</label> + <input + id={field.name} + type="text" + placeholder="Last Name" + value={field.state.value} + onblur={() => field.handleBlur()} + oninput={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + <FieldInfo {field} /> + </div> + {/snippet} + </form.Field> + <form.Field name="employed"> + {#snippet children(field)} + <div> + <label for={field.name}>Employed?</label> + <input + oninput={() => field.handleChange(!field.state.value)} + checked={field.state.value} + onblur={() => field.handleBlur()} + id={field.name} + type="checkbox" + /> + </div> + {#if field.state.value} + <form.Field + name="jobTitle" + validators={{ + onChange: ({ value }) => + value.length === 0 ? 'If you have a job, you need a title' : null, + }} + > + {#snippet children(field)} + <div> + <label for={field.name}>Job Title</label> + <input + type="text" + id={field.name} + placeholder="Job Title" + value={field.state.value} + onblur={field.handleBlur} + oninput={(e: Event) => { + const target = e.target as HTMLInputElement + field.handleChange(target.value) + }} + /> + <FieldInfo {field} /> + </div> + {/snippet} + </form.Field> + {/if} + {/snippet} + </form.Field> + <div> + <form.Subscribe + selector={(state) => ({ + canSubmit: state.canSubmit, + isSubmitting: state.isSubmitting, + })} + > + {#snippet children({ canSubmit, isSubmitting })} + <button type="submit" disabled={!canSubmit}> + {isSubmitting ? 'Submitting' : 'Submit'} + </button> + {/snippet} + </form.Subscribe> + <button + type="button" + id="reset" + onclick={() => { + form.reset() + }} + > + Reset + </button> + </div> +</form> + +<pre>{JSON.stringify(form.state, null, 2)}</pre> diff --git a/examples/svelte/simple/src/FieldInfo.svelte b/examples/svelte/simple/src/FieldInfo.svelte new file mode 100644 index 000000000..45e950412 --- /dev/null +++ b/examples/svelte/simple/src/FieldInfo.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + import type { AnyFieldApi } from '@tanstack/svelte-form' + + let { field }: { field: AnyFieldApi } = $props() +</script> + +{#if field.state.meta.isTouched} + {#each field.state.meta.errors as error} + <em>{error}</em> + {/each} + {field.state.meta.isValidating ? 'Validating...' : ''} +{/if} diff --git a/examples/svelte/simple/src/main.ts b/examples/svelte/simple/src/main.ts new file mode 100644 index 000000000..928b6c527 --- /dev/null +++ b/examples/svelte/simple/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/simple/src/vite-env.d.ts b/examples/svelte/simple/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/examples/svelte/simple/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// <reference types="svelte" /> +/// <reference types="vite/client" /> diff --git a/examples/svelte/simple/svelte.config.js b/examples/svelte/simple/svelte.config.js new file mode 100644 index 000000000..b0683fd24 --- /dev/null +++ b/examples/svelte/simple/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/simple/tsconfig.json b/examples/svelte/simple/tsconfig.json new file mode 100644 index 000000000..55a2f9b65 --- /dev/null +++ b/examples/svelte/simple/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/examples/svelte/simple/vite.config.ts b/examples/svelte/simple/vite.config.ts new file mode 100644 index 000000000..d32eba1d6 --- /dev/null +++ b/examples/svelte/simple/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef34e30e6..f2c632cbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -635,6 +635,50 @@ importers: specifier: ^2.11.2 version: 2.11.6(@testing-library/jest-dom@6.6.3)(solid-js@1.9.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + examples/svelte/array: + dependencies: + '@tanstack/svelte-form': + specifier: ^1.0.0 + version: link:../../../packages/svelte-form + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.3 + version: 5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@tsconfig/svelte': + specifier: ^5.0.4 + version: 5.0.4 + svelte: + specifier: ^5.20.2 + version: 5.22.5 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vite: + specifier: ^6.1.1 + version: 6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1(postcss@8.5.1))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + + examples/svelte/simple: + dependencies: + '@tanstack/svelte-form': + specifier: ^1.0.0 + version: link:../../../packages/svelte-form + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.3 + version: 5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@tsconfig/svelte': + specifier: ^5.0.4 + version: 5.0.4 + svelte: + specifier: ^5.20.2 + version: 5.22.5 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vite: + specifier: ^6.1.1 + version: 6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1(postcss@8.5.1))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) + examples/vue/array: dependencies: '@tanstack/vue-form': @@ -818,18 +862,21 @@ importers: '@tanstack/form-core': specifier: workspace:* version: link:../form-core + '@tanstack/svelte-store': + specifier: ^0.7.0 + version: 0.7.0(svelte@5.22.5) devDependencies: '@sveltejs/package': - specifier: ^2.3.7 + specifier: ^2.3.10 version: 2.3.10(svelte@5.22.5)(typescript@5.8.2) '@sveltejs/vite-plugin-svelte': - specifier: ^4.0.4 - version: 4.0.4(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + specifier: ^5.0.3 + version: 5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) premove: specifier: ^4.0.0 version: 4.0.0 svelte: - specifier: ^5.16.1 + specifier: ^5.20.2 version: 5.22.5 packages/vue-form: @@ -3907,20 +3954,20 @@ packages: peerDependencies: svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 - '@sveltejs/vite-plugin-svelte-inspector@3.0.1': - resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} peerDependencies: - '@sveltejs/vite-plugin-svelte': ^4.0.0-next.0||^4.0.0 - svelte: ^5.0.0-next.96 || ^5.0.0 - vite: ^5.0.0 + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 - '@sveltejs/vite-plugin-svelte@4.0.4': - resolution: {integrity: sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==} + '@sveltejs/vite-plugin-svelte@5.0.3': + resolution: {integrity: sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} peerDependencies: - svelte: ^5.0.0-next.96 || ^5.0.0 - vite: ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 '@swc/core-darwin-arm64@1.11.7': resolution: {integrity: sha512-3+LhCP2H50CLI6yv/lhOtoZ5B/hi7Q/23dye1KhbSDeDprLTm/KfLJh/iQqwaHUponf5m8C2U0y6DD+HGLz8Yw==} @@ -4158,6 +4205,11 @@ packages: '@tanstack/store@0.7.0': resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} + '@tanstack/svelte-store@0.7.0': + resolution: {integrity: sha512-FVuPuCLkGV/YcsJImIiJAZRh3s8DqI8C9jQMNXymELwsrpe538Vmb81fxHHNZx9MCtzsFcHfYOkImFB4r4QcfA==} + peerDependencies: + svelte: ^5.0.0 + '@tanstack/virtual-file-routes@1.99.0': resolution: {integrity: sha512-XvX8bfdo4CYiCW+ItVdBfCorh3PwQFqYqd7ll+XKWiWOJpqUGIG7VlziVavARZpUySiY2VBlHadiUYS7jhgjRg==} engines: {node: '>=12'} @@ -4226,6 +4278,9 @@ packages: '@ts-morph/common@0.22.0': resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} + '@tsconfig/svelte@5.0.4': + resolution: {integrity: sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==} + '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -13602,18 +13657,18 @@ snapshots: transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)))(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)))(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) debug: 4.4.0(supports-color@9.4.0) svelte: 5.22.5 vite: 6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1(postcss@8.5.1))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)))(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)))(svelte@5.22.5)(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(less@4.2.2)(sass@1.85.1)(sugarss@4.0.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)) debug: 4.4.0(supports-color@9.4.0) deepmerge: 4.3.1 kleur: 4.1.5 @@ -14445,6 +14500,11 @@ snapshots: '@tanstack/store@0.7.0': {} + '@tanstack/svelte-store@0.7.0(svelte@5.22.5)': + dependencies: + '@tanstack/store': 0.7.0 + svelte: 5.22.5 + '@tanstack/virtual-file-routes@1.99.0': {} '@tanstack/vue-store@0.7.0(vue@3.5.13(typescript@5.8.2))': @@ -14524,6 +14584,8 @@ snapshots: mkdirp: 3.0.1 path-browserify: 1.0.1 + '@tsconfig/svelte@5.0.4': {} + '@tufjs/canonical-json@2.0.0': {} '@tufjs/models@3.0.1': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 029aff20d..47d0b79bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,4 @@ packages: - 'examples/solid/**' - 'examples/vue/**' - 'examples/lit/**' + - 'examples/svelte/**' From 69a6eb6d7324ed4fa7b48e0acb3d7da58af1e011 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:06:41 +0000 Subject: [PATCH 11/17] ci: apply automated fixes and generate docs --- docs/framework/solid/reference/functions/usestore.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/solid/reference/functions/usestore.md b/docs/framework/solid/reference/functions/usestore.md index a2456113e..aee840203 100644 --- a/docs/framework/solid/reference/functions/usestore.md +++ b/docs/framework/solid/reference/functions/usestore.md @@ -13,7 +13,7 @@ title: useStore function useStore<TState, TSelected>(store, selector?): Accessor<TSelected> ``` -Defined in: node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.4/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:8 +Defined in: node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.5/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:8 ### Type Parameters @@ -41,7 +41,7 @@ Defined in: node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.4/node function useStore<TState, TSelected>(store, selector?): Accessor<TSelected> ``` -Defined in: node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.4/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:9 +Defined in: node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.5/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:9 ### Type Parameters From 032e1f76f365f844bb8f156cca9b7377ab87d279 Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 10 Mar 2025 16:42:20 +0100 Subject: [PATCH 12/17] use svelte-check for type checking --- packages/svelte-form/package.json | 11 +++------ packages/svelte-form/src/createForm.svelte.ts | 1 - packages/svelte-form/src/index.ts | 1 - pnpm-lock.yaml | 23 +++++++++++++++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json index 047b822b0..6cb4cfe27 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -13,13 +13,7 @@ "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src ./tests", - "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", - "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", - "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", - "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", - "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", - "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", - "test:types:ts56": "tsc", + "test:types": "svelte-check", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", @@ -50,7 +44,8 @@ "@sveltejs/package": "^2.3.10", "@sveltejs/vite-plugin-svelte": "^5.0.3", "premove": "^4.0.0", - "svelte": "^5.20.2" + "svelte": "^5.20.2", + "svelte-check": "^4.1.5" }, "peerDependencies": { "svelte": "^5.0.0" diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index e44c37a5b..a687e6a10 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -1,7 +1,6 @@ import { FormApi } from '@tanstack/form-core' import { useStore } from '@tanstack/svelte-store' import { onMount } from 'svelte' -// @ts-ignore tsc doesn't know about named exports from svelte files import Field, { createField } from './Field.svelte' import Subscribe from './Subscribe.svelte' import type { diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts index 0b24263e3..a76cbb7f2 100644 --- a/packages/svelte-form/src/index.ts +++ b/packages/svelte-form/src/index.ts @@ -4,7 +4,6 @@ export { useStore } from '@tanstack/svelte-store' export { createForm, type SvelteFormApi } from './createForm.svelte.js' -// @ts-ignore tsc doesn't know about named exports from svelte files export { default as Field, createField } from './Field.svelte' export type { CreateField, FieldComponent } from './types.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2c632cbb..e6fe5eec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -878,6 +878,9 @@ importers: svelte: specifier: ^5.20.2 version: 5.22.5 + svelte-check: + specifier: ^4.1.5 + version: 4.1.5(picomatch@4.0.2)(svelte@5.22.5)(typescript@5.8.2) packages/vue-form: dependencies: @@ -9232,6 +9235,14 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte-check@4.1.5: + resolution: {integrity: sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + svelte2tsx@0.7.35: resolution: {integrity: sha512-z2lnOnrfb5nrlRfFQI8Qdz03xQqMHUfPj0j8l/fQuydrH89cCeN+v9jgDwK9GyMtdTRUkE7Neu9Gh+vfXJAfuQ==} peerDependencies: @@ -20546,6 +20557,18 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.1.5(picomatch@4.0.2)(svelte@5.22.5)(typescript@5.8.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + chokidar: 4.0.3 + fdir: 6.4.3(picomatch@4.0.2) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.22.5 + typescript: 5.8.2 + transitivePeerDependencies: + - picomatch + svelte2tsx@0.7.35(svelte@5.22.5)(typescript@5.8.2): dependencies: dedent-js: 1.0.1 From 08daf3bcd163d34819a71777c246688bacea0b5d Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 10 Mar 2025 16:46:35 +0100 Subject: [PATCH 13/17] remove accidental leftover --- examples/svelte/simple/src/App.svelte | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/svelte/simple/src/App.svelte b/examples/svelte/simple/src/App.svelte index 1ed0374c9..6bf6e805d 100644 --- a/examples/svelte/simple/src/App.svelte +++ b/examples/svelte/simple/src/App.svelte @@ -146,5 +146,3 @@ </button> </div> </form> - -<pre>{JSON.stringify(form.state, null, 2)}</pre> From 34dadc437871146d7e5ba24a4b87a61bf975ab9a Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 10 Mar 2025 16:53:30 +0100 Subject: [PATCH 14/17] add svelte to keywords --- packages/svelte-form/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte-form/package.json b/packages/svelte-form/package.json index 6cb4cfe27..b2e158b9b 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -4,6 +4,9 @@ "description": "Powerful, type-safe forms for Svelte.", "author": "tannerlinsley", "license": "MIT", + "keywords": [ + "svelte" + ], "repository": { "type": "git", "url": "https://github.com/TanStack/form.git", From 151c2b21c63ab22ee02988ca1558f7568eedc463 Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 10 Mar 2025 17:35:56 +0100 Subject: [PATCH 15/17] clarify, fix --- packages/svelte-form/src/createForm.svelte.ts | 2 +- packages/svelte-form/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts index a687e6a10..f902f1a96 100644 --- a/packages/svelte-form/src/createForm.svelte.ts +++ b/packages/svelte-form/src/createForm.svelte.ts @@ -219,7 +219,7 @@ export function createForm< extendedApi.createField = (props) => createField(() => { return { ...props(), form: api } - }) as never + }) as never // Type cast because else "Error: Type instantiation is excessively deep and possibly infinite." extendedApi.useStore = (selector) => useStore(api.store, selector) // @ts-expect-error constructor definition exists only on a type level extendedApi.Subscribe = (internal, props) => diff --git a/packages/svelte-form/src/types.ts b/packages/svelte-form/src/types.ts index 6b4ad3133..45aa74b3c 100644 --- a/packages/svelte-form/src/types.ts +++ b/packages/svelte-form/src/types.ts @@ -339,7 +339,7 @@ export type CreateField< >, 'form' >, -) => () => FieldApi< +) => FieldApi< TParentData, TName, TData, From 55e5c82d7c8df979c2ff121ff37e06b3818495dd Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 17 Mar 2025 11:03:35 +0100 Subject: [PATCH 16/17] fix --- packages/svelte-form/tests/simple.svelte | 2 +- packages/svelte-form/tests/simple.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte index c48080ce5..bcca2193c 100644 --- a/packages/svelte-form/tests/simple.svelte +++ b/packages/svelte-form/tests/simple.svelte @@ -11,7 +11,7 @@ </script> <script lang="ts"> - import { createForm } from '@tanstack/svelte-form' + import { createForm } from '../src/index.js' const form = createForm(() => ({ defaultValues: sampleData, diff --git a/packages/svelte-form/tests/simple.test.ts b/packages/svelte-form/tests/simple.test.ts index 3fa23d8c6..d39cd594b 100644 --- a/packages/svelte-form/tests/simple.test.ts +++ b/packages/svelte-form/tests/simple.test.ts @@ -59,7 +59,7 @@ describe('Svelte Tests', () => { await userEvent.type(lastName, lastNameValue) - expect(await lastName.getAttribute('error-text')).toBeFalsy() + expect(lastName.getAttribute('error-text')).toBeFalsy() expect(element.querySelector('em')).not.toBeInTheDocument() }) }) From f01a213eec9590400cd17b8729716ccb31bd8b54 Mon Sep 17 00:00:00 2001 From: Simon Holthausen <simon.holthausen@vercel.com> Date: Mon, 17 Mar 2025 13:39:07 +0100 Subject: [PATCH 17/17] resolve todo, tweak code, add test --- packages/svelte-form/src/Field.svelte | 5 ++--- packages/svelte-form/tests/simple.svelte | 19 +++++++++++++++---- packages/svelte-form/tests/simple.test.ts | 19 ++++++++++++++----- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index 62032794c..ebaf1f854 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -93,11 +93,10 @@ } }) - // TODO (43081j): does this do what i think? we don't access anything - // svelte is aware of, so maybe it'll never call this? $effect.pre(() => { + // Invoke options function before mounted check, else it wouldn't rerun on changes to options. + // Changes to options are seen by the effect because signals inside them are picked up. const current = opts() - Object.values(current) // make sure we're watching all the things if (!mounted) return api.update(current) }) diff --git a/packages/svelte-form/tests/simple.svelte b/packages/svelte-form/tests/simple.svelte index bcca2193c..147556ea0 100644 --- a/packages/svelte-form/tests/simple.svelte +++ b/packages/svelte-form/tests/simple.svelte @@ -1,13 +1,17 @@ +<svelte:options runes /> + <script module lang="ts"> interface Employee { firstName: string lastName: string } - export const sampleData: Employee = { + let sampleData: Employee = $state({ firstName: 'Christian', lastName: '', - } + }) + + export const getSampleData = () => sampleData </script> <script lang="ts"> @@ -21,7 +25,7 @@ }, })) - const state = form.useStore() + const formState = form.useStore() </script> <form @@ -110,4 +114,11 @@ </div> </form> -<pre>{JSON.stringify(state.current, null, 2)}</pre> +<button + id="change" + onclick={() => (sampleData = { firstName: 'Julian', lastName: '' })} +> + Change Sample Data +</button> + +<pre>{JSON.stringify(formState.current, null, 2)}</pre> diff --git a/packages/svelte-form/tests/simple.test.ts b/packages/svelte-form/tests/simple.test.ts index d39cd594b..ab0a433e7 100644 --- a/packages/svelte-form/tests/simple.test.ts +++ b/packages/svelte-form/tests/simple.test.ts @@ -4,7 +4,7 @@ import '@testing-library/jest-dom' import { userEvent } from '@testing-library/user-event' import { mount, unmount } from 'svelte' // @ts-ignore tsc doesn't know about named exports from svelte files -import TestForm, { sampleData } from './simple.svelte' +import TestForm, { getSampleData } from './simple.svelte' describe('Svelte Tests', () => { let element: HTMLDivElement @@ -24,13 +24,22 @@ describe('Svelte Tests', () => { it('should have initial values', async () => { expect(element.querySelector<HTMLInputElement>('#firstName')).toHaveValue( - sampleData.firstName, + getSampleData().firstName, ) expect(element.querySelector<HTMLInputElement>('#lastName')).toHaveValue( - sampleData.lastName, + getSampleData().lastName, ) }) + it('should change initial values when defaults update', async () => { + await userEvent.click(element.querySelector<HTMLButtonElement>('#change')!) + + expect(element.querySelector<HTMLInputElement>('#firstName')).toHaveValue( + getSampleData().firstName, + ) + expect(getSampleData().firstName).toBe('Julian') + }) + it('should mirror user input', async () => { const lastName = element.querySelector<HTMLInputElement>('#lastName')! const lastNameValue = 'Jobs' @@ -44,10 +53,10 @@ describe('Svelte Tests', () => { const firstName = element.querySelector<HTMLInputElement>('#firstName')! await userEvent.type(firstName, '-Joseph') - expect(firstName).toHaveValue(sampleData.firstName + '-Joseph') + expect(firstName).toHaveValue(getSampleData().firstName + '-Joseph') await userEvent.click(element.querySelector<HTMLButtonElement>('#reset')!) - expect(firstName).toHaveValue(sampleData.firstName) + expect(firstName).toHaveValue(getSampleData().firstName) }) it('should display validation', async () => {