Skip to content

Commit ad16469

Browse files
dummdidumm43081jlachlancollinsautofix-ci[bot]
authored
feat(svelte-form): add Svelte adapter (#1247)
* 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. * wip: add vite and fix some types * fix: correct language attribute * chore: shuffle things around to use svelte files * fix: correct a whole bunch of bad refs/types * fix: return FieldApi directly * Build with @sveltejs/package * ci: apply automated fixes and generate docs * 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 * add svelte examples * ci: apply automated fixes and generate docs * use svelte-check for type checking * remove accidental leftover * add svelte to keywords * clarify, fix * fix * resolve todo, tweak code, add test * Update packages/svelte-form/tests/simple.test.ts * Update package config --------- Co-authored-by: James Garbutt <[email protected]> Co-authored-by: Lachlan Collins <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b5ea568 commit ad16469

37 files changed

+1920
-0
lines changed

examples/svelte/array/.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

examples/svelte/array/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`

examples/svelte/array/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + Svelte + TS</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>

examples/svelte/array/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@tanstack/form-example-svelte-array",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"@tanstack/svelte-form": "^1.0.0"
13+
},
14+
"devDependencies": {
15+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
16+
"@tsconfig/svelte": "^5.0.4",
17+
"svelte": "^5.27.2",
18+
"typescript": "5.8.2",
19+
"vite": "^6.2.6"
20+
}
21+
}

examples/svelte/array/src/App.svelte

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts">
2+
import { createForm } from '@tanstack/svelte-form'
3+
4+
const form = createForm(() => ({
5+
defaultValues: {
6+
people: [] as Array<{ age: number; name: string }>,
7+
},
8+
onSubmit: ({ value }) => alert(JSON.stringify(value)),
9+
}))
10+
</script>
11+
12+
<form
13+
id="form"
14+
onsubmit={(e) => {
15+
e.preventDefault()
16+
e.stopPropagation()
17+
form.handleSubmit()
18+
}}
19+
>
20+
<h1>TanStack Form - Svelte Demo</h1>
21+
22+
<form.Field name="people">
23+
{#snippet children(field)}
24+
<div>
25+
{#each field.state.value as person, i}
26+
<form.Field name={`people[${i}].name`}>
27+
{#snippet children(subField)}
28+
<div>
29+
<label>
30+
<div>Name for person {i}</div>
31+
<input
32+
value={person.name}
33+
oninput={(e: Event) => {
34+
const target = e.target as HTMLInputElement
35+
subField.handleChange(target.value)
36+
}}
37+
/>
38+
</label>
39+
</div>
40+
{/snippet}
41+
</form.Field>
42+
{/each}
43+
44+
<button
45+
onclick={() => field.pushValue({ name: '', age: 0 })}
46+
type="button"
47+
>
48+
Add person
49+
</button>
50+
</div>
51+
{/snippet}
52+
</form.Field>
53+
54+
<button type="submit"> Submit </button>
55+
</form>

examples/svelte/array/src/main.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { mount } from 'svelte'
2+
import App from './App.svelte'
3+
4+
const app = mount(App, {
5+
target: document.getElementById('app')!,
6+
})
7+
8+
export default app
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="svelte" />
2+
/// <reference types="vite/client" />
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2+
3+
export default {
4+
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5+
// for more information about preprocessors
6+
preprocess: vitePreprocess(),
7+
}

examples/svelte/array/tsconfig.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"extends": "@tsconfig/svelte/tsconfig.json",
3+
"compilerOptions": {
4+
"target": "ESNext",
5+
"useDefineForClassFields": true,
6+
"module": "ESNext",
7+
"resolveJsonModule": true,
8+
/**
9+
* Typecheck JS in `.svelte` and `.js` files by default.
10+
* Disable checkJs if you'd like to use dynamic types in JS.
11+
* Note that setting allowJs false does not prevent the use
12+
* of JS in `.svelte` files.
13+
*/
14+
"allowJs": true,
15+
"checkJs": true,
16+
"isolatedModules": true,
17+
"moduleDetection": "force"
18+
},
19+
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
20+
}

examples/svelte/array/vite.config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vite'
2+
import { svelte } from '@sveltejs/vite-plugin-svelte'
3+
4+
// https://vite.dev/config/
5+
export default defineConfig({
6+
plugins: [svelte()],
7+
})

examples/svelte/simple/.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

examples/svelte/simple/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install`
6+
- `npm run dev`

examples/svelte/simple/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + Svelte + TS</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>

examples/svelte/simple/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@tanstack/form-example-svelte-simple",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"@tanstack/svelte-form": "^1.0.0"
13+
},
14+
"devDependencies": {
15+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
16+
"@tsconfig/svelte": "^5.0.4",
17+
"svelte": "^5.27.2",
18+
"typescript": "5.8.2",
19+
"vite": "^6.2.6"
20+
}
21+
}

examples/svelte/simple/src/App.svelte

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<script lang="ts">
2+
import { createForm } from '@tanstack/svelte-form'
3+
import FieldInfo from './FieldInfo.svelte'
4+
5+
const form = createForm(() => ({
6+
defaultValues: {
7+
firstName: '',
8+
lastName: '',
9+
employed: false,
10+
jobTitle: '',
11+
},
12+
onSubmit: async ({ value }) => {
13+
// Do something with form data
14+
alert(JSON.stringify(value))
15+
},
16+
}))
17+
</script>
18+
19+
<form
20+
id="form"
21+
onsubmit={(e) => {
22+
e.preventDefault()
23+
e.stopPropagation()
24+
form.handleSubmit()
25+
}}
26+
>
27+
<h1>TanStack Form - Svelte Demo</h1>
28+
29+
<form.Field
30+
name="firstName"
31+
validators={{
32+
onChange: ({ value }) =>
33+
value.length < 3 ? 'Not long enough' : undefined,
34+
onChangeAsyncDebounceMs: 500,
35+
onChangeAsync: async ({ value }) => {
36+
await new Promise((resolve) => setTimeout(resolve, 1000))
37+
return value.includes('error') && 'No "error" allowed in first name'
38+
},
39+
}}
40+
>
41+
{#snippet children(field)}
42+
<div>
43+
<label for={field.name}>First Name</label>
44+
<input
45+
id={field.name}
46+
type="text"
47+
placeholder="First Name"
48+
value={field.state.value}
49+
onblur={() => field.handleBlur()}
50+
oninput={(e: Event) => {
51+
const target = e.target as HTMLInputElement
52+
field.handleChange(target.value)
53+
}}
54+
/>
55+
<FieldInfo {field} />
56+
</div>
57+
{/snippet}
58+
</form.Field>
59+
<form.Field
60+
name="lastName"
61+
validators={{
62+
onChange: ({ value }) =>
63+
value.length < 3 ? 'Not long enough' : undefined,
64+
}}
65+
>
66+
{#snippet children(field)}
67+
<div>
68+
<label for={field.name}>Last Name</label>
69+
<input
70+
id={field.name}
71+
type="text"
72+
placeholder="Last Name"
73+
value={field.state.value}
74+
onblur={() => field.handleBlur()}
75+
oninput={(e: Event) => {
76+
const target = e.target as HTMLInputElement
77+
field.handleChange(target.value)
78+
}}
79+
/>
80+
<FieldInfo {field} />
81+
</div>
82+
{/snippet}
83+
</form.Field>
84+
<form.Field name="employed">
85+
{#snippet children(field)}
86+
<div>
87+
<label for={field.name}>Employed?</label>
88+
<input
89+
oninput={() => field.handleChange(!field.state.value)}
90+
checked={field.state.value}
91+
onblur={() => field.handleBlur()}
92+
id={field.name}
93+
type="checkbox"
94+
/>
95+
</div>
96+
{#if field.state.value}
97+
<form.Field
98+
name="jobTitle"
99+
validators={{
100+
onChange: ({ value }) =>
101+
value.length === 0 ? 'If you have a job, you need a title' : null,
102+
}}
103+
>
104+
{#snippet children(field)}
105+
<div>
106+
<label for={field.name}>Job Title</label>
107+
<input
108+
type="text"
109+
id={field.name}
110+
placeholder="Job Title"
111+
value={field.state.value}
112+
onblur={field.handleBlur}
113+
oninput={(e: Event) => {
114+
const target = e.target as HTMLInputElement
115+
field.handleChange(target.value)
116+
}}
117+
/>
118+
<FieldInfo {field} />
119+
</div>
120+
{/snippet}
121+
</form.Field>
122+
{/if}
123+
{/snippet}
124+
</form.Field>
125+
<div>
126+
<form.Subscribe
127+
selector={(state) => ({
128+
canSubmit: state.canSubmit,
129+
isSubmitting: state.isSubmitting,
130+
})}
131+
>
132+
{#snippet children({ canSubmit, isSubmitting })}
133+
<button type="submit" disabled={!canSubmit}>
134+
{isSubmitting ? 'Submitting' : 'Submit'}
135+
</button>
136+
{/snippet}
137+
</form.Subscribe>
138+
<button
139+
type="button"
140+
id="reset"
141+
onclick={() => {
142+
form.reset()
143+
}}
144+
>
145+
Reset
146+
</button>
147+
</div>
148+
</form>

0 commit comments

Comments
 (0)