create-start-app 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-app.js +2 -2
- package/package.json +1 -2
- package/src/create-app.ts +2 -2
- package/templates/react/add-on/form/assets/src/components/demo.FormComponents.tsx.ejs +300 -0
- package/templates/react/add-on/form/assets/src/routes/demo.form.address.tsx.ejs +14 -11
- package/templates/react/add-on/form/info.json +9 -0
- package/templates/react/add-on/table/assets/src/data/demo-table-data.ts +50 -0
- package/templates/react/add-on/table/assets/src/routes/demo.table.tsx.ejs +373 -0
- package/templates/react/add-on/table/info.json +13 -0
- package/templates/react/add-on/table/package.json +7 -0
- package/templates/react/add-on/form/assets/src/components/demo.FormComponents.tsx +0 -121
package/dist/create-app.js
CHANGED
|
@@ -307,7 +307,7 @@ export async function createApp(options, { silent = false, environment, }) {
|
|
|
307
307
|
if (shadcnComponents.size > 0) {
|
|
308
308
|
s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
|
|
309
309
|
await environment.execute('npx', ['shadcn@canary', 'add', ...shadcnComponents], targetDir);
|
|
310
|
-
s?.stop(`Installed
|
|
310
|
+
s?.stop(`Installed additional shadcn components`);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
const integrations = [];
|
|
@@ -430,7 +430,7 @@ export async function createApp(options, { silent = false, environment, }) {
|
|
|
430
430
|
if (environment.getErrors().length) {
|
|
431
431
|
errorStatement = `
|
|
432
432
|
|
|
433
|
-
${chalk.red('
|
|
433
|
+
${chalk.red('Errors were encountered during this process:')}
|
|
434
434
|
|
|
435
435
|
${environment.getErrors().join('\n')}`;
|
|
436
436
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-start-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Tanstack Application Builder",
|
|
5
5
|
"bin": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"scripts": {},
|
|
8
7
|
"repository": {
|
|
9
8
|
"type": "git",
|
|
10
9
|
"url": "https://github.com/TanStack/create-tsrouter-app.git"
|
package/src/create-app.ts
CHANGED
|
@@ -490,7 +490,7 @@ export async function createApp(
|
|
|
490
490
|
['shadcn@canary', 'add', ...shadcnComponents],
|
|
491
491
|
targetDir,
|
|
492
492
|
)
|
|
493
|
-
s?.stop(`Installed
|
|
493
|
+
s?.stop(`Installed additional shadcn components`)
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
@@ -701,7 +701,7 @@ export async function createApp(
|
|
|
701
701
|
if (environment.getErrors().length) {
|
|
702
702
|
errorStatement = `
|
|
703
703
|
|
|
704
|
-
${chalk.red('
|
|
704
|
+
${chalk.red('Errors were encountered during this process:')}
|
|
705
705
|
|
|
706
706
|
${environment.getErrors().join('\n')}`
|
|
707
707
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useStore } from '@tanstack/react-form'
|
|
2
|
+
|
|
3
|
+
import { useFieldContext, useFormContext } from '../hooks/demo.form-context'
|
|
4
|
+
<% if (addOnEnabled.shadcn) { %>
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { Textarea as ShadcnTextarea } from '@/components/ui/textarea'
|
|
8
|
+
import * as ShadcnSelect from '@/components/ui/select'
|
|
9
|
+
import { Slider as ShadcnSlider } from '@/components/ui/slider'
|
|
10
|
+
import { Switch as ShadcnSwitch } from '@/components/ui/switch'
|
|
11
|
+
import { Label } from '@/components/ui/label'
|
|
12
|
+
|
|
13
|
+
export function SubscribeButton({ label }: { label: string }) {
|
|
14
|
+
const form = useFormContext()
|
|
15
|
+
return (
|
|
16
|
+
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
17
|
+
{(isSubmitting) => (
|
|
18
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
19
|
+
{label}
|
|
20
|
+
</Button>
|
|
21
|
+
)}
|
|
22
|
+
</form.Subscribe>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ErrorMessages({
|
|
27
|
+
errors,
|
|
28
|
+
}: {
|
|
29
|
+
errors: Array<string | { message: string }>
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{errors.map((error) => (
|
|
34
|
+
<div
|
|
35
|
+
key={typeof error === 'string' ? error : error.message}
|
|
36
|
+
className="text-red-500 mt-1 font-bold"
|
|
37
|
+
>
|
|
38
|
+
{typeof error === 'string' ? error : error.message}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function TextField({
|
|
46
|
+
label,
|
|
47
|
+
placeholder,
|
|
48
|
+
}: {
|
|
49
|
+
label: string
|
|
50
|
+
placeholder?: string
|
|
51
|
+
}) {
|
|
52
|
+
const field = useFieldContext<string>()
|
|
53
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<Label htmlFor={label} className="mb-2 text-xl font-bold">
|
|
58
|
+
{label}
|
|
59
|
+
</Label>
|
|
60
|
+
<Input
|
|
61
|
+
value={field.state.value}
|
|
62
|
+
placeholder={placeholder}
|
|
63
|
+
onBlur={field.handleBlur}
|
|
64
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
65
|
+
/>
|
|
66
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function TextArea({
|
|
72
|
+
label,
|
|
73
|
+
rows = 3,
|
|
74
|
+
}: {
|
|
75
|
+
label: string
|
|
76
|
+
rows?: number
|
|
77
|
+
}) {
|
|
78
|
+
const field = useFieldContext<string>()
|
|
79
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<Label htmlFor={label} className="mb-2 text-xl font-bold">
|
|
84
|
+
{label}
|
|
85
|
+
</Label>
|
|
86
|
+
<ShadcnTextarea
|
|
87
|
+
id={label}
|
|
88
|
+
value={field.state.value}
|
|
89
|
+
onBlur={field.handleBlur}
|
|
90
|
+
rows={rows}
|
|
91
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function Select({
|
|
99
|
+
label,
|
|
100
|
+
values,
|
|
101
|
+
placeholder,
|
|
102
|
+
}: {
|
|
103
|
+
label: string
|
|
104
|
+
values: Array<{ label: string; value: string }>
|
|
105
|
+
placeholder?: string
|
|
106
|
+
}) {
|
|
107
|
+
const field = useFieldContext<string>()
|
|
108
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
<ShadcnSelect.Select
|
|
113
|
+
name={field.name}
|
|
114
|
+
value={field.state.value}
|
|
115
|
+
onValueChange={(value) => field.handleChange(value)}
|
|
116
|
+
>
|
|
117
|
+
<ShadcnSelect.SelectTrigger className="w-full">
|
|
118
|
+
<ShadcnSelect.SelectValue placeholder={placeholder} />
|
|
119
|
+
</ShadcnSelect.SelectTrigger>
|
|
120
|
+
<ShadcnSelect.SelectContent>
|
|
121
|
+
<ShadcnSelect.SelectGroup>
|
|
122
|
+
<ShadcnSelect.SelectLabel>{label}</ShadcnSelect.SelectLabel>
|
|
123
|
+
{values.map((value) => (
|
|
124
|
+
<ShadcnSelect.SelectItem key={value.value} value={value.value}>
|
|
125
|
+
{value.label}
|
|
126
|
+
</ShadcnSelect.SelectItem>
|
|
127
|
+
))}
|
|
128
|
+
</ShadcnSelect.SelectGroup>
|
|
129
|
+
</ShadcnSelect.SelectContent>
|
|
130
|
+
</ShadcnSelect.Select>
|
|
131
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function Slider({ label }: { label: string }) {
|
|
137
|
+
const field = useFieldContext<number>()
|
|
138
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<Label htmlFor={label} className="mb-2 text-xl font-bold">
|
|
143
|
+
{label}
|
|
144
|
+
</Label>
|
|
145
|
+
<ShadcnSlider
|
|
146
|
+
id={label}
|
|
147
|
+
onBlur={field.handleBlur}
|
|
148
|
+
value={[field.state.value]}
|
|
149
|
+
onValueChange={(value) => field.handleChange(value[0])}
|
|
150
|
+
/>
|
|
151
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function Switch({ label }: { label: string }) {
|
|
157
|
+
const field = useFieldContext<boolean>()
|
|
158
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<ShadcnSwitch
|
|
164
|
+
id={label}
|
|
165
|
+
onBlur={field.handleBlur}
|
|
166
|
+
checked={field.state.value}
|
|
167
|
+
onCheckedChange={(checked) => field.handleChange(checked)}
|
|
168
|
+
/>
|
|
169
|
+
<Label htmlFor={label}>{label}</Label>
|
|
170
|
+
</div>
|
|
171
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
<% } else { %>
|
|
177
|
+
export function SubscribeButton({ label }: { label: string }) {
|
|
178
|
+
const form = useFormContext()
|
|
179
|
+
return (
|
|
180
|
+
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
181
|
+
{(isSubmitting) => (
|
|
182
|
+
<button
|
|
183
|
+
type="submit"
|
|
184
|
+
disabled={isSubmitting}
|
|
185
|
+
className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
|
186
|
+
>
|
|
187
|
+
{label}
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
</form.Subscribe>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ErrorMessages({
|
|
195
|
+
errors,
|
|
196
|
+
}: {
|
|
197
|
+
errors: Array<string | { message: string }>
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
{errors.map((error) => (
|
|
202
|
+
<div
|
|
203
|
+
key={typeof error === 'string' ? error : error.message}
|
|
204
|
+
className="text-red-500 mt-1 font-bold"
|
|
205
|
+
>
|
|
206
|
+
{typeof error === 'string' ? error : error.message}
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function TextField({
|
|
214
|
+
label,
|
|
215
|
+
placeholder,
|
|
216
|
+
}: {
|
|
217
|
+
label: string
|
|
218
|
+
placeholder?: string
|
|
219
|
+
}) {
|
|
220
|
+
const field = useFieldContext<string>()
|
|
221
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
226
|
+
{label}
|
|
227
|
+
<input
|
|
228
|
+
value={field.state.value}
|
|
229
|
+
placeholder={placeholder}
|
|
230
|
+
onBlur={field.handleBlur}
|
|
231
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
232
|
+
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
233
|
+
/>
|
|
234
|
+
</label>
|
|
235
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function TextArea({
|
|
241
|
+
label,
|
|
242
|
+
rows = 3,
|
|
243
|
+
}: {
|
|
244
|
+
label: string
|
|
245
|
+
rows?: number
|
|
246
|
+
}) {
|
|
247
|
+
const field = useFieldContext<string>()
|
|
248
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div>
|
|
252
|
+
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
253
|
+
{label}
|
|
254
|
+
<textarea
|
|
255
|
+
value={field.state.value}
|
|
256
|
+
onBlur={field.handleBlur}
|
|
257
|
+
rows={rows}
|
|
258
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
259
|
+
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
260
|
+
/>
|
|
261
|
+
</label>
|
|
262
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function Select({
|
|
268
|
+
label,
|
|
269
|
+
values,
|
|
270
|
+
}: {
|
|
271
|
+
label: string
|
|
272
|
+
values: Array<{ label: string; value: string }>
|
|
273
|
+
placeholder?: string
|
|
274
|
+
}) {
|
|
275
|
+
const field = useFieldContext<string>()
|
|
276
|
+
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
281
|
+
{label}
|
|
282
|
+
</label>
|
|
283
|
+
<select
|
|
284
|
+
name={field.name}
|
|
285
|
+
value={field.state.value}
|
|
286
|
+
onBlur={field.handleBlur}
|
|
287
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
288
|
+
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
289
|
+
>
|
|
290
|
+
{values.map((value) => (
|
|
291
|
+
<option key={value.value} value={value.value}>
|
|
292
|
+
{value.label}
|
|
293
|
+
</option>
|
|
294
|
+
))}
|
|
295
|
+
</select>
|
|
296
|
+
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
<% } %>
|
|
@@ -153,18 +153,21 @@ function AddressForm() {
|
|
|
153
153
|
}}
|
|
154
154
|
>
|
|
155
155
|
{(field) => (
|
|
156
|
-
<field.Select
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
<field.Select
|
|
157
|
+
label="Country"
|
|
158
|
+
values={[
|
|
159
|
+
{ label: 'United States', value: 'US' },
|
|
160
|
+
{ label: 'Canada', value: 'CA' },
|
|
161
|
+
{ label: 'United Kingdom', value: 'UK' },
|
|
162
|
+
{ label: 'Australia', value: 'AU' },
|
|
163
|
+
{ label: 'Germany', value: 'DE' },
|
|
164
|
+
{ label: 'France', value: 'FR' },
|
|
165
|
+
{ label: 'Japan', value: 'JP' },
|
|
166
|
+
]}
|
|
167
|
+
placeholder="Select a country"
|
|
168
|
+
/>
|
|
166
169
|
)}
|
|
167
|
-
|
|
170
|
+
</form.AppField>
|
|
168
171
|
|
|
169
172
|
<form.AppField
|
|
170
173
|
name="phone"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker'
|
|
2
|
+
|
|
3
|
+
export type Person = {
|
|
4
|
+
id: number
|
|
5
|
+
firstName: string
|
|
6
|
+
lastName: string
|
|
7
|
+
age: number
|
|
8
|
+
visits: number
|
|
9
|
+
progress: number
|
|
10
|
+
status: 'relationship' | 'complicated' | 'single'
|
|
11
|
+
subRows?: Person[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const range = (len: number) => {
|
|
15
|
+
const arr: number[] = []
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
arr.push(i)
|
|
18
|
+
}
|
|
19
|
+
return arr
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const newPerson = (num: number): Person => {
|
|
23
|
+
return {
|
|
24
|
+
id: num,
|
|
25
|
+
firstName: faker.person.firstName(),
|
|
26
|
+
lastName: faker.person.lastName(),
|
|
27
|
+
age: faker.number.int(40),
|
|
28
|
+
visits: faker.number.int(1000),
|
|
29
|
+
progress: faker.number.int(100),
|
|
30
|
+
status: faker.helpers.shuffle<Person['status']>([
|
|
31
|
+
'relationship',
|
|
32
|
+
'complicated',
|
|
33
|
+
'single',
|
|
34
|
+
])[0]!,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeData(...lens: number[]) {
|
|
39
|
+
const makeDataLevel = (depth = 0): Person[] => {
|
|
40
|
+
const len = lens[depth]!
|
|
41
|
+
return range(len).map((index): Person => {
|
|
42
|
+
return {
|
|
43
|
+
...newPerson(index),
|
|
44
|
+
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return makeDataLevel()
|
|
50
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { <% if (fileRouter) { %>createFileRoute<% } else { %>createRoute<% } %> } from '@tanstack/react-router'
|
|
3
|
+
import {
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getFilteredRowModel,
|
|
7
|
+
getPaginationRowModel,
|
|
8
|
+
getSortedRowModel,
|
|
9
|
+
sortingFns,
|
|
10
|
+
useReactTable,
|
|
11
|
+
} from '@tanstack/react-table'
|
|
12
|
+
import { compareItems, rankItem } from '@tanstack/match-sorter-utils'
|
|
13
|
+
|
|
14
|
+
import { makeData } from '../data/demo-table-data'
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Column,
|
|
18
|
+
ColumnDef,
|
|
19
|
+
ColumnFiltersState,
|
|
20
|
+
FilterFn,
|
|
21
|
+
SortingFn,
|
|
22
|
+
} from '@tanstack/react-table'
|
|
23
|
+
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
|
24
|
+
<% if (codeRouter) { %>
|
|
25
|
+
import type { RootRoute } from '@tanstack/react-router'
|
|
26
|
+
<% } %>
|
|
27
|
+
import type { Person } from '../data/demo-table-data'
|
|
28
|
+
<% if (fileRouter) { %>
|
|
29
|
+
export const Route = createFileRoute('/demo/table')({
|
|
30
|
+
component: TableDemo,
|
|
31
|
+
})
|
|
32
|
+
<% } %>
|
|
33
|
+
|
|
34
|
+
declare module '@tanstack/react-table' {
|
|
35
|
+
interface FilterFns {
|
|
36
|
+
fuzzy: FilterFn<unknown>
|
|
37
|
+
}
|
|
38
|
+
interface FilterMeta {
|
|
39
|
+
itemRank: RankingInfo
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Define a custom fuzzy filter function that will apply ranking info to rows (using match-sorter utils)
|
|
44
|
+
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
|
45
|
+
// Rank the item
|
|
46
|
+
const itemRank = rankItem(row.getValue(columnId), value)
|
|
47
|
+
|
|
48
|
+
// Store the itemRank info
|
|
49
|
+
addMeta({
|
|
50
|
+
itemRank,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Return if the item should be filtered in/out
|
|
54
|
+
return itemRank.passed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Define a custom fuzzy sort function that will sort by rank if the row has ranking information
|
|
58
|
+
const fuzzySort: SortingFn<any> = (rowA, rowB, columnId) => {
|
|
59
|
+
let dir = 0
|
|
60
|
+
|
|
61
|
+
// Only sort by rank if the column has ranking information
|
|
62
|
+
if (rowA.columnFiltersMeta[columnId]) {
|
|
63
|
+
dir = compareItems(
|
|
64
|
+
rowA.columnFiltersMeta[columnId]?.itemRank!,
|
|
65
|
+
rowB.columnFiltersMeta[columnId]?.itemRank!,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Provide an alphanumeric fallback for when the item ranks are equal
|
|
70
|
+
return dir === 0 ? sortingFns.alphanumeric(rowA, rowB, columnId) : dir
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function TableDemo() {
|
|
74
|
+
const rerender = React.useReducer(() => ({}), {})[1]
|
|
75
|
+
|
|
76
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
|
77
|
+
[],
|
|
78
|
+
)
|
|
79
|
+
const [globalFilter, setGlobalFilter] = React.useState('')
|
|
80
|
+
|
|
81
|
+
const columns = React.useMemo<ColumnDef<Person, any>[]>(
|
|
82
|
+
() => [
|
|
83
|
+
{
|
|
84
|
+
accessorKey: 'id',
|
|
85
|
+
filterFn: 'equalsString', //note: normal non-fuzzy filter column - exact match required
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
accessorKey: 'firstName',
|
|
89
|
+
cell: (info) => info.getValue(),
|
|
90
|
+
filterFn: 'includesStringSensitive', //note: normal non-fuzzy filter column - case sensitive
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
accessorFn: (row) => row.lastName,
|
|
94
|
+
id: 'lastName',
|
|
95
|
+
cell: (info) => info.getValue(),
|
|
96
|
+
header: () => <span>Last Name</span>,
|
|
97
|
+
filterFn: 'includesString', //note: normal non-fuzzy filter column - case insensitive
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
|
|
101
|
+
id: 'fullName',
|
|
102
|
+
header: 'Full Name',
|
|
103
|
+
cell: (info) => info.getValue(),
|
|
104
|
+
filterFn: 'fuzzy', //using our custom fuzzy filter function
|
|
105
|
+
// filterFn: fuzzyFilter, //or just define with the function
|
|
106
|
+
sortingFn: fuzzySort, //sort by fuzzy rank (falls back to alphanumeric)
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
[],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const [data, setData] = React.useState<Person[]>(() => makeData(5_000))
|
|
113
|
+
const refreshData = () => setData((_old) => makeData(50_000)) //stress test
|
|
114
|
+
|
|
115
|
+
const table = useReactTable({
|
|
116
|
+
data,
|
|
117
|
+
columns,
|
|
118
|
+
filterFns: {
|
|
119
|
+
fuzzy: fuzzyFilter, //define as a filter function that can be used in column definitions
|
|
120
|
+
},
|
|
121
|
+
state: {
|
|
122
|
+
columnFilters,
|
|
123
|
+
globalFilter,
|
|
124
|
+
},
|
|
125
|
+
onColumnFiltersChange: setColumnFilters,
|
|
126
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
127
|
+
globalFilterFn: 'fuzzy', //apply fuzzy filter to the global filter (most common use case for fuzzy filter)
|
|
128
|
+
getCoreRowModel: getCoreRowModel(),
|
|
129
|
+
getFilteredRowModel: getFilteredRowModel(), //client side filtering
|
|
130
|
+
getSortedRowModel: getSortedRowModel(),
|
|
131
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
132
|
+
debugTable: true,
|
|
133
|
+
debugHeaders: true,
|
|
134
|
+
debugColumns: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
//apply the fuzzy sort if the fullName column is being filtered
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
if (table.getState().columnFilters[0]?.id === 'fullName') {
|
|
140
|
+
if (table.getState().sorting[0]?.id !== 'fullName') {
|
|
141
|
+
table.setSorting([{ id: 'fullName', desc: false }])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, [table.getState().columnFilters[0]?.id])
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="min-h-screen bg-gray-900 p-6">
|
|
148
|
+
<div>
|
|
149
|
+
<DebouncedInput
|
|
150
|
+
value={globalFilter ?? ''}
|
|
151
|
+
onChange={(value) => setGlobalFilter(String(value))}
|
|
152
|
+
className="w-full p-3 bg-gray-800 text-white rounded-lg border border-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
|
153
|
+
placeholder="Search all columns..."
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="h-4" />
|
|
157
|
+
<div className="overflow-x-auto rounded-lg border border-gray-700">
|
|
158
|
+
<table className="w-full text-sm text-gray-200">
|
|
159
|
+
<thead className="bg-gray-800 text-gray-100">
|
|
160
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
161
|
+
<tr key={headerGroup.id}>
|
|
162
|
+
{headerGroup.headers.map((header) => {
|
|
163
|
+
return (
|
|
164
|
+
<th
|
|
165
|
+
key={header.id}
|
|
166
|
+
colSpan={header.colSpan}
|
|
167
|
+
className="px-4 py-3 text-left"
|
|
168
|
+
>
|
|
169
|
+
{header.isPlaceholder ? null : (
|
|
170
|
+
<>
|
|
171
|
+
<div
|
|
172
|
+
{...{
|
|
173
|
+
className: header.column.getCanSort()
|
|
174
|
+
? 'cursor-pointer select-none hover:text-blue-400 transition-colors'
|
|
175
|
+
: '',
|
|
176
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
{flexRender(
|
|
180
|
+
header.column.columnDef.header,
|
|
181
|
+
header.getContext(),
|
|
182
|
+
)}
|
|
183
|
+
{{
|
|
184
|
+
asc: ' 🔼',
|
|
185
|
+
desc: ' 🔽',
|
|
186
|
+
}[header.column.getIsSorted() as string] ?? null}
|
|
187
|
+
</div>
|
|
188
|
+
{header.column.getCanFilter() ? (
|
|
189
|
+
<div className="mt-2">
|
|
190
|
+
<Filter column={header.column} />
|
|
191
|
+
</div>
|
|
192
|
+
) : null}
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
</th>
|
|
196
|
+
)
|
|
197
|
+
})}
|
|
198
|
+
</tr>
|
|
199
|
+
))}
|
|
200
|
+
</thead>
|
|
201
|
+
<tbody className="divide-y divide-gray-700">
|
|
202
|
+
{table.getRowModel().rows.map((row) => {
|
|
203
|
+
return (
|
|
204
|
+
<tr
|
|
205
|
+
key={row.id}
|
|
206
|
+
className="hover:bg-gray-800 transition-colors"
|
|
207
|
+
>
|
|
208
|
+
{row.getVisibleCells().map((cell) => {
|
|
209
|
+
return (
|
|
210
|
+
<td key={cell.id} className="px-4 py-3">
|
|
211
|
+
{flexRender(
|
|
212
|
+
cell.column.columnDef.cell,
|
|
213
|
+
cell.getContext(),
|
|
214
|
+
)}
|
|
215
|
+
</td>
|
|
216
|
+
)
|
|
217
|
+
})}
|
|
218
|
+
</tr>
|
|
219
|
+
)
|
|
220
|
+
})}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="h-4" />
|
|
225
|
+
<div className="flex flex-wrap items-center gap-2 text-gray-200">
|
|
226
|
+
<button
|
|
227
|
+
className="px-3 py-1 bg-gray-800 rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
228
|
+
onClick={() => table.setPageIndex(0)}
|
|
229
|
+
disabled={!table.getCanPreviousPage()}
|
|
230
|
+
>
|
|
231
|
+
{'<<'}
|
|
232
|
+
</button>
|
|
233
|
+
<button
|
|
234
|
+
className="px-3 py-1 bg-gray-800 rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
235
|
+
onClick={() => table.previousPage()}
|
|
236
|
+
disabled={!table.getCanPreviousPage()}
|
|
237
|
+
>
|
|
238
|
+
{'<'}
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
className="px-3 py-1 bg-gray-800 rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
242
|
+
onClick={() => table.nextPage()}
|
|
243
|
+
disabled={!table.getCanNextPage()}
|
|
244
|
+
>
|
|
245
|
+
{'>'}
|
|
246
|
+
</button>
|
|
247
|
+
<button
|
|
248
|
+
className="px-3 py-1 bg-gray-800 rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
249
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
250
|
+
disabled={!table.getCanNextPage()}
|
|
251
|
+
>
|
|
252
|
+
{'>>'}
|
|
253
|
+
</button>
|
|
254
|
+
<span className="flex items-center gap-1">
|
|
255
|
+
<div>Page</div>
|
|
256
|
+
<strong>
|
|
257
|
+
{table.getState().pagination.pageIndex + 1} of{' '}
|
|
258
|
+
{table.getPageCount()}
|
|
259
|
+
</strong>
|
|
260
|
+
</span>
|
|
261
|
+
<span className="flex items-center gap-1">
|
|
262
|
+
| Go to page:
|
|
263
|
+
<input
|
|
264
|
+
type="number"
|
|
265
|
+
defaultValue={table.getState().pagination.pageIndex + 1}
|
|
266
|
+
onChange={(e) => {
|
|
267
|
+
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
|
268
|
+
table.setPageIndex(page)
|
|
269
|
+
}}
|
|
270
|
+
className="w-16 px-2 py-1 bg-gray-800 rounded-md border border-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
|
271
|
+
/>
|
|
272
|
+
</span>
|
|
273
|
+
<select
|
|
274
|
+
value={table.getState().pagination.pageSize}
|
|
275
|
+
onChange={(e) => {
|
|
276
|
+
table.setPageSize(Number(e.target.value))
|
|
277
|
+
}}
|
|
278
|
+
className="px-2 py-1 bg-gray-800 rounded-md border border-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
|
279
|
+
>
|
|
280
|
+
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
281
|
+
<option key={pageSize} value={pageSize}>
|
|
282
|
+
Show {pageSize}
|
|
283
|
+
</option>
|
|
284
|
+
))}
|
|
285
|
+
</select>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="mt-4 text-gray-400">
|
|
288
|
+
{table.getPrePaginationRowModel().rows.length} Rows
|
|
289
|
+
</div>
|
|
290
|
+
<div className="mt-4 flex gap-2">
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => rerender()}
|
|
293
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
294
|
+
>
|
|
295
|
+
Force Rerender
|
|
296
|
+
</button>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => refreshData()}
|
|
299
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
300
|
+
>
|
|
301
|
+
Refresh Data
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
<pre className="mt-4 p-4 bg-gray-800 rounded-lg text-gray-300 overflow-auto">
|
|
305
|
+
{JSON.stringify(
|
|
306
|
+
{
|
|
307
|
+
columnFilters: table.getState().columnFilters,
|
|
308
|
+
globalFilter: table.getState().globalFilter,
|
|
309
|
+
},
|
|
310
|
+
null,
|
|
311
|
+
2,
|
|
312
|
+
)}
|
|
313
|
+
</pre>
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function Filter({ column }: { column: Column<any, unknown> }) {
|
|
319
|
+
const columnFilterValue = column.getFilterValue()
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<DebouncedInput
|
|
323
|
+
type="text"
|
|
324
|
+
value={(columnFilterValue ?? '') as string}
|
|
325
|
+
onChange={(value) => column.setFilterValue(value)}
|
|
326
|
+
placeholder={`Search...`}
|
|
327
|
+
className="w-full px-2 py-1 bg-gray-700 text-white rounded-md border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
|
328
|
+
/>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// A typical debounced input react component
|
|
333
|
+
function DebouncedInput({
|
|
334
|
+
value: initialValue,
|
|
335
|
+
onChange,
|
|
336
|
+
debounce = 500,
|
|
337
|
+
...props
|
|
338
|
+
}: {
|
|
339
|
+
value: string | number
|
|
340
|
+
onChange: (value: string | number) => void
|
|
341
|
+
debounce?: number
|
|
342
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
|
|
343
|
+
const [value, setValue] = React.useState(initialValue)
|
|
344
|
+
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
setValue(initialValue)
|
|
347
|
+
}, [initialValue])
|
|
348
|
+
|
|
349
|
+
React.useEffect(() => {
|
|
350
|
+
const timeout = setTimeout(() => {
|
|
351
|
+
onChange(value)
|
|
352
|
+
}, debounce)
|
|
353
|
+
|
|
354
|
+
return () => clearTimeout(timeout)
|
|
355
|
+
}, [value])
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<input
|
|
359
|
+
{...props}
|
|
360
|
+
value={value}
|
|
361
|
+
onChange={(e) => setValue(e.target.value)}
|
|
362
|
+
/>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
<% if (codeRouter) { %>
|
|
367
|
+
export default (parentRoute: RootRoute) => createRoute({
|
|
368
|
+
path: '/demo/table',
|
|
369
|
+
component: TableDemo,
|
|
370
|
+
getParentRoute: () => parentRoute,
|
|
371
|
+
})
|
|
372
|
+
<% } %>
|
|
373
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Table",
|
|
3
|
+
"description": "Integrate TanStack Table into your application.",
|
|
4
|
+
"phase": "add-on",
|
|
5
|
+
"templates": ["file-router", "code-router"],
|
|
6
|
+
"link": "https://tanstack.com/table/latest",
|
|
7
|
+
"routes": [
|
|
8
|
+
{
|
|
9
|
+
"url": "/demo/table",
|
|
10
|
+
"name": "TanStack Table"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { useStore } from '@tanstack/react-form'
|
|
2
|
-
import { useFieldContext, useFormContext } from '../hooks/demo.form-context'
|
|
3
|
-
|
|
4
|
-
export function SubscribeButton({ label }: { label: string }) {
|
|
5
|
-
const form = useFormContext()
|
|
6
|
-
return (
|
|
7
|
-
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
8
|
-
{(isSubmitting) => (
|
|
9
|
-
<button
|
|
10
|
-
type="submit"
|
|
11
|
-
disabled={isSubmitting}
|
|
12
|
-
className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
|
|
13
|
-
>
|
|
14
|
-
{label}
|
|
15
|
-
</button>
|
|
16
|
-
)}
|
|
17
|
-
</form.Subscribe>
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ErrorMessages({
|
|
22
|
-
errors,
|
|
23
|
-
}: {
|
|
24
|
-
errors: Array<string | { message: string }>
|
|
25
|
-
}) {
|
|
26
|
-
return (
|
|
27
|
-
<>
|
|
28
|
-
{errors.map((error) => (
|
|
29
|
-
<div
|
|
30
|
-
key={typeof error === 'string' ? error : error.message}
|
|
31
|
-
className="text-red-500 mt-1 font-bold"
|
|
32
|
-
>
|
|
33
|
-
{typeof error === 'string' ? error : error.message}
|
|
34
|
-
</div>
|
|
35
|
-
))}
|
|
36
|
-
</>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function TextField({
|
|
41
|
-
label,
|
|
42
|
-
placeholder,
|
|
43
|
-
}: {
|
|
44
|
-
label: string
|
|
45
|
-
placeholder?: string
|
|
46
|
-
}) {
|
|
47
|
-
const field = useFieldContext<string>()
|
|
48
|
-
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div>
|
|
52
|
-
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
53
|
-
{label}
|
|
54
|
-
<input
|
|
55
|
-
value={field.state.value}
|
|
56
|
-
placeholder={placeholder}
|
|
57
|
-
onBlur={field.handleBlur}
|
|
58
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
59
|
-
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
60
|
-
/>
|
|
61
|
-
</label>
|
|
62
|
-
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
63
|
-
</div>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function TextArea({
|
|
68
|
-
label,
|
|
69
|
-
rows = 3,
|
|
70
|
-
}: {
|
|
71
|
-
label: string
|
|
72
|
-
rows?: number
|
|
73
|
-
}) {
|
|
74
|
-
const field = useFieldContext<string>()
|
|
75
|
-
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div>
|
|
79
|
-
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
80
|
-
{label}
|
|
81
|
-
<textarea
|
|
82
|
-
value={field.state.value}
|
|
83
|
-
onBlur={field.handleBlur}
|
|
84
|
-
rows={rows}
|
|
85
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
86
|
-
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
87
|
-
/>
|
|
88
|
-
</label>
|
|
89
|
-
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
90
|
-
</div>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function Select({
|
|
95
|
-
label,
|
|
96
|
-
children,
|
|
97
|
-
}: {
|
|
98
|
-
label: string
|
|
99
|
-
children: React.ReactNode
|
|
100
|
-
}) {
|
|
101
|
-
const field = useFieldContext<string>()
|
|
102
|
-
const errors = useStore(field.store, (state) => state.meta.errors)
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div>
|
|
106
|
-
<label htmlFor={label} className="block font-bold mb-1 text-xl">
|
|
107
|
-
{label}
|
|
108
|
-
</label>
|
|
109
|
-
<select
|
|
110
|
-
name={field.name}
|
|
111
|
-
value={field.state.value}
|
|
112
|
-
onBlur={field.handleBlur}
|
|
113
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
114
|
-
className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
115
|
-
>
|
|
116
|
-
{children}
|
|
117
|
-
</select>
|
|
118
|
-
{field.state.meta.isTouched && <ErrorMessages errors={errors} />}
|
|
119
|
-
</div>
|
|
120
|
-
)
|
|
121
|
-
}
|