@igniter-js/cli 0.1.11 → 0.2.0-alpha.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.
Files changed (65) hide show
  1. package/bin/igniter +2 -0
  2. package/dist/index.d.mts +1 -0
  3. package/dist/index.d.ts +0 -1
  4. package/dist/index.js +14390 -523
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +14427 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +37 -51
  9. package/dist/templates/components.json.hbs +0 -21
  10. package/dist/templates/copilot.feature.instructions.hbs +0 -145
  11. package/dist/templates/copilot.form.instructions.hbs +0 -1021
  12. package/dist/templates/copilot.igniter.instructions.hbs +0 -753
  13. package/dist/templates/copilot.instructions.hbs +0 -117
  14. package/dist/templates/copilot.next.instructions.hbs +0 -67
  15. package/dist/templates/copilot.review.instructions.hbs +0 -42
  16. package/dist/templates/copilot.test.instructions.hbs +0 -55
  17. package/dist/templates/docker-compose.hbs +0 -15
  18. package/dist/templates/env.hbs +0 -33
  19. package/dist/templates/eslintrc.hbs +0 -6
  20. package/dist/templates/express.server.hbs +0 -33
  21. package/dist/templates/feature.controller.hbs +0 -95
  22. package/dist/templates/feature.index.hbs +0 -5
  23. package/dist/templates/feature.interface.hbs +0 -101
  24. package/dist/templates/feature.procedure.hbs +0 -88
  25. package/dist/templates/globals.hbs +0 -123
  26. package/dist/templates/igniter.client.hbs +0 -21
  27. package/dist/templates/igniter.context.hbs +0 -23
  28. package/dist/templates/igniter.hbs +0 -8
  29. package/dist/templates/igniter.router.hbs +0 -29
  30. package/dist/templates/layout.hbs +0 -39
  31. package/dist/templates/page.hbs +0 -117
  32. package/dist/templates/prisma.hbs +0 -9
  33. package/dist/templates/readme.hbs +0 -119
  34. package/dist/templates/route.hbs +0 -4
  35. package/dist/templates/use-form-with-zod.hbs +0 -39
  36. package/dist/templates/vitest.config.hbs +0 -11
  37. package/dist/templates/vscode.settings.hbs +0 -53
  38. package/dist/utils/analyze.d.ts +0 -17
  39. package/dist/utils/analyze.js +0 -185
  40. package/dist/utils/analyze.js.map +0 -1
  41. package/dist/utils/cli-style.d.ts +0 -55
  42. package/dist/utils/cli-style.js +0 -171
  43. package/dist/utils/cli-style.js.map +0 -1
  44. package/dist/utils/consts.d.ts +0 -19
  45. package/dist/utils/consts.js +0 -30
  46. package/dist/utils/consts.js.map +0 -1
  47. package/dist/utils/handlebars-helpers.d.ts +0 -1
  48. package/dist/utils/handlebars-helpers.js +0 -88
  49. package/dist/utils/handlebars-helpers.js.map +0 -1
  50. package/dist/utils/helpers.d.ts +0 -13
  51. package/dist/utils/helpers.js +0 -112
  52. package/dist/utils/helpers.js.map +0 -1
  53. package/dist/utils/platform-utils.d.ts +0 -46
  54. package/dist/utils/platform-utils.js +0 -95
  55. package/dist/utils/platform-utils.js.map +0 -1
  56. package/dist/utils/prisma-schema-parser.d.ts +0 -60
  57. package/dist/utils/prisma-schema-parser.js +0 -255
  58. package/dist/utils/prisma-schema-parser.js.map +0 -1
  59. package/dist/utils/project-utils.d.ts +0 -32
  60. package/dist/utils/project-utils.js +0 -123
  61. package/dist/utils/project-utils.js.map +0 -1
  62. package/dist/utils/template-handler.d.ts +0 -6
  63. package/dist/utils/template-handler.js +0 -32
  64. package/dist/utils/template-handler.js.map +0 -1
  65. package/readme.md +0 -165
@@ -1,1021 +0,0 @@
1
- ---
2
- title: Form Building Guide with Igniter.js
3
- url: https://github.com/felipebarcelospro/igniter-router-next-app
4
- timestamp: 2025-02-27T10:35:00.000Z
5
- ---
6
-
7
- # Form Building Guide for Igniter.js Applications
8
-
9
- This guide outlines the best practices, patterns, and techniques for building robust, type-safe forms in applications using the Igniter.js framework with Next.js, React Hook Form, Zod, and Shadcn UI.
10
-
11
- ## Table of Contents
12
-
13
- * [Core Form Philosophy](#core-form-philosophy)
14
- * [Form Architecture](#form-architecture)
15
- * [Form Components](#form-components)
16
- * [Form Validation](#form-validation)
17
- * [Form Submission](#form-submission)
18
- * [Form State Management](#form-state-management)
19
- * [Error Handling](#error-handling)
20
- * [Advanced Form Patterns](#advanced-form-patterns)
21
- * [Best Practices](#best-practices)
22
-
23
- ## Core Form Philosophy
24
-
25
- Igniter.js forms follow these core principles:
26
-
27
- * **Type Safety**: End-to-end type safety from schema definition to form submission
28
- * **Validation First**: Schema-based validation using Zod
29
- * **Component Composition**: Forms built from composable, reusable components
30
- * **Error Resilience**: Comprehensive error handling and user feedback
31
- * **Performance Optimized**: Forms that maintain performance even with complex validation
32
- * **Accessibility**: ARIA-compliant forms that work for all users
33
-
34
- ## Form Architecture
35
-
36
- ### Key Components in the Form System
37
-
38
- 1. **Schema Definition**: Using Zod to define form shape and validation rules
39
- 2. **Form Hook**: `useFormWithZod` custom hook for connecting Zod schemas to React Hook Form
40
- 3. **Form Components**: Shadcn UI form primitives for consistent UI/UX
41
- 4. **Form State Management**: React Hook Form for handling form state
42
- 5. **Form Submission**: Integration with Igniter.js mutations for API calls
43
-
44
- ### Diagram of Form Data Flow
45
-
46
- ```
47
- ┌────────────┐ ┌───────────────┐ ┌───────────────┐
48
- │ │ │ │ │ │
49
- │ Zod │────▶│ React Hook │────▶│ Form │
50
- │ Schema │ │ Form │ │ Components │
51
- │ │ │ │ │ │
52
- └────────────┘ └───────────────┘ └───────────────┘
53
- │ │
54
- │ │
55
- ▼ ▼
56
- ┌────────────┐ ┌───────────────┐ ┌───────────────┐
57
- │ │ │ │ │ │
58
- │ Igniter │◀────│ Form │◀────│ User │
59
- │ Mutation │ │ Submission │ │ Input │
60
- │ │ │ │ │ │
61
- └────────────┘ └───────────────┘ └───────────────┘
62
-
63
-
64
-
65
- ┌────────────┐
66
- │ │
67
- │ User │
68
- │ Feedback │
69
- │ │
70
- └────────────┘
71
- ```
72
-
73
- ## Form Components
74
-
75
- ### Base Form Components
76
-
77
- Igniter.js applications use Shadcn UI's form components as building blocks:
78
-
79
- ```tsx
80
- import {
81
- Form,
82
- FormControl,
83
- FormField,
84
- FormItem,
85
- FormLabel,
86
- FormMessage,
87
- } from "@/components/ui/form"
88
- ```
89
-
90
- ### Form Container
91
-
92
- Every form starts with the `Form` component that wraps the form elements:
93
-
94
- ```tsx
95
- <Form {...form}>
96
- <form onSubmit={form.onSubmit} className="space-y-4 py-4">
97
- {/* Form fields go here */}
98
- </form>
99
- </Form>
100
- ```
101
-
102
- ### Form Fields
103
-
104
- Form fields follow this consistent pattern:
105
-
106
- ```tsx
107
- <FormField
108
- control={form.control}
109
- name="fieldName"
110
- render={({ field }) => (
111
- <FormItem>
112
- <FormLabel>Field Label</FormLabel>
113
- <FormControl>
114
- <Input {...field} />
115
- </FormControl>
116
- <FormMessage />
117
- </FormItem>
118
- )}
119
- />
120
- ```
121
-
122
- ### Field Types
123
-
124
- #### Text Input
125
-
126
- ```tsx
127
- <FormField
128
- control={form.control}
129
- name="title"
130
- render={({ field }) => (
131
- <FormItem>
132
- <FormLabel>Title</FormLabel>
133
- <FormControl>
134
- <Input placeholder="Enter title..." {...field} />
135
- </FormControl>
136
- <FormMessage />
137
- </FormItem>
138
- )}
139
- />
140
- ```
141
-
142
- #### Text Area
143
-
144
- ```tsx
145
- <FormField
146
- control={form.control}
147
- name="description"
148
- render={({ field }) => (
149
- <FormItem>
150
- <FormLabel>Description</FormLabel>
151
- <FormControl>
152
- <Textarea
153
- placeholder="Enter description..."
154
- {...field}
155
- />
156
- </FormControl>
157
- <FormMessage />
158
- </FormItem>
159
- )}
160
- />
161
- ```
162
-
163
- #### Date Picker
164
-
165
- ```tsx
166
- <FormField
167
- control={form.control}
168
- name="dueDate"
169
- render={({ field }) => (
170
- <FormItem className="flex flex-col">
171
- <FormLabel>Due Date</FormLabel>
172
- <Popover>
173
- <PopoverTrigger asChild>
174
- <FormControl>
175
- <Button
176
- variant="outline"
177
- className={cn(
178
- 'w-full pl-3 text-left font-normal',
179
- !field.value && 'text-muted-foreground'
180
- )}
181
- >
182
- {field.value ? (
183
- format(field.value, 'PPP')
184
- ) : (
185
- <span>Pick a date</span>
186
- )}
187
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
188
- </Button>
189
- </FormControl>
190
- </PopoverTrigger>
191
- <PopoverContent className="w-auto p-0" align="start">
192
- <Calendar
193
- mode="single"
194
- selected={field.value ? new Date(field.value) : undefined}
195
- onSelect={field.onChange}
196
- disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
197
- initialFocus
198
- />
199
- </PopoverContent>
200
- </Popover>
201
- <FormMessage />
202
- </FormItem>
203
- )}
204
- />
205
- ```
206
-
207
- #### Select Field
208
-
209
- ```tsx
210
- <FormField
211
- control={form.control}
212
- name="category"
213
- render={({ field }) => (
214
- <FormItem>
215
- <FormLabel>Category</FormLabel>
216
- <Select onValueChange={field.onChange} defaultValue={field.value}>
217
- <FormControl>
218
- <SelectTrigger>
219
- <SelectValue placeholder="Select a category" />
220
- </SelectTrigger>
221
- </FormControl>
222
- <SelectContent>
223
- <SelectItem value="work">Work</SelectItem>
224
- <SelectItem value="personal">Personal</SelectItem>
225
- <SelectItem value="education">Education</SelectItem>
226
- </SelectContent>
227
- </Select>
228
- <FormMessage />
229
- </FormItem>
230
- )}
231
- />
232
- ```
233
-
234
- #### Checkbox
235
-
236
- ```tsx
237
- <FormField
238
- control={form.control}
239
- name="isCompleted"
240
- render={({ field }) => (
241
- <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
242
- <FormControl>
243
- <Checkbox
244
- checked={field.value}
245
- onCheckedChange={field.onChange}
246
- />
247
- </FormControl>
248
- <div className="space-y-1 leading-none">
249
- <FormLabel>Completed</FormLabel>
250
- <FormDescription>
251
- Mark this task as completed
252
- </FormDescription>
253
- </div>
254
- </FormItem>
255
- )}
256
- />
257
- ```
258
-
259
- ## Form Validation
260
-
261
- ### Zod Schema Definition
262
-
263
- Define validation schemas using Zod:
264
-
265
- ```typescript
266
- const schema = z.object({
267
- id: z.string().optional(),
268
- title: z.string().min(3, 'Title must be at least 3 characters'),
269
- description: z.string().optional(),
270
- dueDate: z.date().transform(value => value.toISOString()).optional(),
271
- priority: z.enum(['low', 'medium', 'high']).default('medium'),
272
- isCompleted: z.boolean().default(false),
273
- })
274
-
275
- type FormValues = z.infer<typeof schema>
276
- ```
277
-
278
- ### Common Validation Patterns
279
-
280
- #### Required Fields
281
-
282
- ```typescript
283
- z.string().min(1, 'This field is required')
284
- ```
285
-
286
- #### Email Validation
287
-
288
- ```typescript
289
- z.string().email('Please enter a valid email address')
290
- ```
291
-
292
- #### Number Validation
293
-
294
- ```typescript
295
- z.number().min(0, 'Value must be positive').max(100, 'Value must be at most 100')
296
- ```
297
-
298
- #### Date Validation
299
-
300
- ```typescript
301
- z.date()
302
- .min(new Date(), 'Date must be in the future')
303
- .transform(value => value.toISOString())
304
- ```
305
-
306
- #### Conditional Validation
307
-
308
- ```typescript
309
- z.object({
310
- hasDeadline: z.boolean(),
311
- deadline: z.date().optional().superRefine((val, ctx) => {
312
- if (ctx.parent.hasDeadline && !val) {
313
- ctx.addIssue({
314
- code: z.ZodIssueCode.custom,
315
- message: 'Deadline is required when "Has Deadline" is checked',
316
- });
317
- }
318
- }),
319
- })
320
- ```
321
-
322
- ## Form Submission
323
-
324
- ### Using Custom Hook
325
-
326
- The `useFormWithZod` custom hook simplifies form creation and submission:
327
-
328
- ```typescript
329
- const form = useFormWithZod({
330
- schema: schema,
331
- defaultValues: defaultValues || { title: '', description: '' },
332
- onSubmit: async (values) => {
333
- // Handle form submission
334
- const result = await tryCatch(mutation.mutate({ body: values }))
335
-
336
- if (result.error) {
337
- toast.error('Error submitting form. Please try again.')
338
- return
339
- }
340
-
341
- toast.success('Form submitted successfully!')
342
- // Additional success handling
343
- }
344
- })
345
- ```
346
-
347
- ### Using Igniter.js Mutations
348
-
349
- ```typescript
350
- const upsertMutation = api.task.upsert.useMutation()
351
-
352
- // In form submission handler
353
- const result = await tryCatch(upsertMutation.mutate({
354
- body: formValues
355
- }))
356
- ```
357
-
358
- ### Form Submission States
359
-
360
- Handle different form submission states:
361
-
362
- ```typescript
363
- <Button
364
- type="submit"
365
- disabled={form.formState.isSubmitting || !form.formState.isValid}
366
- >
367
- {form.formState.isSubmitting ? (
368
- <>
369
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
370
- Submitting...
371
- </>
372
- ) : (
373
- <>
374
- Submit
375
- <ArrowRight className="ml-2 h-4 w-4" />
376
- </>
377
- )}
378
- </Button>
379
- ```
380
-
381
- ## Form State Management
382
-
383
- ### Using useFormWithZod
384
-
385
- ```typescript
386
- import { useFormWithZod } from '@/hooks/use-form-with-zod'
387
-
388
- const form = useFormWithZod({
389
- schema: schema,
390
- defaultValues: {
391
- title: '',
392
- description: '',
393
- },
394
- onSubmit: (values) => {
395
- // Form submission logic
396
- }
397
- })
398
-
399
- // Access form state
400
- const { isDirty, isValid, isSubmitting } = form.formState
401
- ```
402
-
403
- ### Form Reset
404
-
405
- ```typescript
406
- // Reset form to initial values
407
- form.reset()
408
-
409
- // Reset form to specific values
410
- form.reset({
411
- title: 'New Title',
412
- description: 'New Description'
413
- })
414
- ```
415
-
416
- ### Form Dialog Integration
417
-
418
- When using forms inside dialogs, make sure to reset the form when the dialog closes:
419
-
420
- ```typescript
421
- <Dialog onOpenChange={(open) => {
422
- if (!open) {
423
- form.reset()
424
- }
425
- }}>
426
- {/* Dialog content and form */}
427
- </Dialog>
428
- ```
429
-
430
- ## Error Handling
431
-
432
- ### Try-Catch Pattern
433
-
434
- Use the `tryCatch` utility to handle form submission errors:
435
-
436
- ```typescript
437
- import { tryCatch } from '@/utils/try-catch'
438
-
439
- // In form submission handler
440
- const result = await tryCatch(upsertMutation.mutate({ body: values }))
441
-
442
- if (result.error) {
443
- toast.error('Error saving task. Please try again.')
444
- return
445
- }
446
-
447
- toast.success('Task created successfully!')
448
- ```
449
-
450
- ### Field-Level Error Handling
451
-
452
- Errors are automatically displayed below each field using `FormMessage`:
453
-
454
- ```tsx
455
- <FormField
456
- control={form.control}
457
- name="title"
458
- render={({ field }) => (
459
- <FormItem>
460
- <FormLabel>Title</FormLabel>
461
- <FormControl>
462
- <Input {...field} />
463
- </FormControl>
464
- <FormMessage />
465
- </FormItem>
466
- )}
467
- />
468
- ```
469
-
470
- ### Form-Level Error Handling
471
-
472
- Display form-level errors:
473
-
474
- ```tsx
475
- {form.formState.errors.root && (
476
- <Alert variant="destructive">
477
- <AlertCircle className="h-4 w-4" />
478
- <AlertTitle>Error</AlertTitle>
479
- <AlertDescription>
480
- {form.formState.errors.root.message}
481
- </AlertDescription>
482
- </Alert>
483
- )}
484
- ```
485
-
486
- ## Advanced Form Patterns
487
-
488
- ### Dynamic Fields
489
-
490
- Using React Hook Form's `useFieldArray`:
491
-
492
- ```tsx
493
- import { useFieldArray } from "react-hook-form"
494
-
495
- // Inside component
496
- const { fields, append, remove } = useFieldArray({
497
- control: form.control,
498
- name: "tasks",
499
- })
500
-
501
- // In JSX
502
- {fields.map((field, index) => (
503
- <div key={field.id} className="flex items-center gap-2">
504
- <FormField
505
- control={form.control}
506
- name={`tasks.${index}.title`}
507
- render={({ field }) => (
508
- <FormItem className="flex-1">
509
- <FormControl>
510
- <Input {...field} />
511
- </FormControl>
512
- <FormMessage />
513
- </FormItem>
514
- )}
515
- />
516
- <Button
517
- type="button"
518
- variant="outline"
519
- size="icon"
520
- onClick={() => remove(index)}
521
- >
522
- <Trash2 className="h-4 w-4" />
523
- </Button>
524
- </div>
525
- ))}
526
-
527
- <Button
528
- type="button"
529
- variant="outline"
530
- size="sm"
531
- onClick={() => append({ title: '' })}
532
- >
533
- <Plus className="mr-2 h-4 w-4" />
534
- Add Task
535
- </Button>
536
- ```
537
-
538
- ### Multi-Step Forms
539
-
540
- ```tsx
541
- function MultiStepForm() {
542
- const [step, setStep] = useState(0)
543
- const form = useFormWithZod({
544
- schema: schema,
545
- defaultValues: { /* ... */ },
546
- onSubmit: async (values) => {
547
- // Submit final form data
548
- }
549
- })
550
-
551
- const steps = [
552
- // Step 1: Basic Info
553
- <div key="basic" className="space-y-4">
554
- <FormField
555
- control={form.control}
556
- name="title"
557
- render={({ field }) => (/* ... */)}
558
- />
559
- {/* More fields */}
560
- </div>,
561
-
562
- // Step 2: Additional Details
563
- <div key="details" className="space-y-4">
564
- <FormField
565
- control={form.control}
566
- name="description"
567
- render={({ field }) => (/* ... */)}
568
- />
569
- {/* More fields */}
570
- </div>,
571
-
572
- // Step 3: Review
573
- <div key="review" className="space-y-4">
574
- {/* Review UI */}
575
- </div>
576
- ]
577
-
578
- return (
579
- <Form {...form}>
580
- <form onSubmit={form.onSubmit} className="space-y-8">
581
- {steps[step]}
582
-
583
- <div className="flex justify-between">
584
- <Button
585
- type="button"
586
- variant="outline"
587
- onClick={() => setStep(prev => Math.max(0, prev - 1))}
588
- disabled={step === 0}
589
- >
590
- Previous
591
- </Button>
592
-
593
- {step < steps.length - 1 ? (
594
- <Button
595
- type="button"
596
- onClick={() => setStep(prev => Math.min(steps.length - 1, prev + 1))}
597
- >
598
- Next
599
- </Button>
600
- ) : (
601
- <Button type="submit">Submit</Button>
602
- )}
603
- </div>
604
- </form>
605
- </Form>
606
- )
607
- }
608
- ```
609
-
610
- ### Form with File Upload
611
-
612
- ```tsx
613
- // Zod schema
614
- const schema = z.object({
615
- name: z.string(),
616
- avatar: z.instanceof(File).optional(),
617
- })
618
-
619
- // Component
620
- function FileUploadForm() {
621
- const form = useFormWithZod({
622
- schema,
623
- defaultValues: { name: '' },
624
- onSubmit: async (values) => {
625
- // Create FormData for submission
626
- const formData = new FormData()
627
- formData.append('name', values.name)
628
- if (values.avatar) {
629
- formData.append('avatar', values.avatar)
630
- }
631
-
632
- // Submit formData to API
633
- await uploadMutation.mutate({ formData })
634
- }
635
- })
636
-
637
- return (
638
- <Form {...form}>
639
- <form onSubmit={form.onSubmit} className="space-y-4">
640
- <FormField
641
- control={form.control}
642
- name="name"
643
- render={({ field }) => (/* ... */)}
644
- />
645
-
646
- <FormField
647
- control={form.control}
648
- name="avatar"
649
- render={({ field: { value, onChange, ...field } }) => (
650
- <FormItem>
651
- <FormLabel>Profile Picture</FormLabel>
652
- <FormControl>
653
- <Input
654
- type="file"
655
- accept="image/*"
656
- onChange={(e) => {
657
- const file = e.target.files?.[0]
658
- if (file) onChange(file)
659
- }}
660
- {...field}
661
- />
662
- </FormControl>
663
- <FormMessage />
664
- </FormItem>
665
- )}
666
- />
667
-
668
- <Button type="submit">Upload</Button>
669
- </form>
670
- </Form>
671
- )
672
- }
673
- ```
674
-
675
- ## Cache Invalidation After Form Submission
676
-
677
- ```typescript
678
- const queryClient = useQueryClient()
679
-
680
- // In form submission handler
681
- const handleSubmit = async (values) => {
682
- const result = await tryCatch(upsertMutation.mutate({ body: values }))
683
-
684
- if (result.error) {
685
- toast.error('Error saving data')
686
- return
687
- }
688
-
689
- toast.success('Data saved successfully!')
690
-
691
- // Invalidate relevant queries to refetch data
692
- queryClient.invalidate(['task.list'])
693
-
694
- // Close modal/dialog
695
- onClose()
696
- }
697
- ```
698
-
699
- ## Best Practices
700
-
701
- ### 1. Form Organization
702
-
703
- * Keep form components focused on a single purpose
704
- * Extract complex form logic into custom hooks
705
- * Group related fields together
706
- * Use consistent spacing and layout for all forms
707
-
708
- ### 2. Performance Optimization
709
-
710
- * Use form validation modes appropriately:
711
- - `onChange`: Validates as user types (best for simple forms)
712
- - `onBlur`: Validates when field loses focus (better UX for most forms)
713
- - `onSubmit`: Validates only on submit (best for complex forms)
714
-
715
- ```typescript
716
- const form = useFormWithZod({
717
- schema: schema,
718
- defaultValues: { /* ... */ },
719
- mode: 'onBlur', // or 'onChange', 'onSubmit'
720
- })
721
- ```
722
-
723
- * Debounce validation for text inputs:
724
-
725
- ```typescript
726
- <Input
727
- {...field}
728
- onChange={(e) => {
729
- clearTimeout(timeout.current)
730
- timeout.current = setTimeout(() => {
731
- field.onChange(e)
732
- }, 300)
733
- }}
734
- />
735
- ```
736
-
737
- ### 3. Accessibility
738
-
739
- * Always use `FormLabel` for form inputs
740
- * Ensure form controls have appropriate ARIA attributes
741
- * Provide clear error messages
742
- * Make forms keyboard navigable
743
- * Use `fieldset` and `legend` for groups of related inputs
744
-
745
- ```tsx
746
- <fieldset className="border rounded-md p-4">
747
- <legend className="text-sm font-medium px-2">Contact Information</legend>
748
- {/* Form fields */}
749
- }
750
- </fieldset>
751
- ```
752
-
753
- ### 4. Error Prevention
754
-
755
- * Provide clear validation messages
756
- * Use placeholder text to guide users
757
- * Implement input masks for formatted fields
758
- * Show validation feedback as users type
759
- * Confirm destructive actions
760
-
761
- ### 5. Reusability
762
-
763
- Create custom form field components for common patterns:
764
-
765
- ```tsx
766
- function FormTextField({ name, label, placeholder, ...props }) {
767
- return (
768
- <FormField
769
- name={name}
770
- render={({ field }) => (
771
- <FormItem>
772
- <FormLabel>{label}</FormLabel>
773
- <FormControl>
774
- <Input placeholder={placeholder} {...field} {...props} />
775
- </FormControl>
776
- <FormMessage />
777
- </FormItem>
778
- )}
779
- />
780
- )
781
- }
782
-
783
- // Usage
784
- <FormTextField name="title" label="Title" placeholder="Enter a title" />
785
- ```
786
-
787
- ### 6. Testing
788
-
789
- * Test form validation with valid and invalid inputs
790
- * Test form submission with mock API calls
791
- * Test form reset functionality
792
- * Test form accessibility using jest-axe or similar tools
793
-
794
- ## Complete Example: Task Form
795
-
796
- Here's a complete example of a task creation/editing form:
797
-
798
- ```tsx
799
- 'use client'
800
-
801
- import * as z from 'zod'
802
- import { useRef } from 'react'
803
- import { format } from 'date-fns'
804
- import { toast } from 'sonner'
805
- import { api, useQueryClient } from '@/igniter.client'
806
- import { useFormWithZod } from '@/hooks/use-form-with-zod'
807
- import { tryCatch } from '@/utils/try-catch'
808
- import { Task } from '../../task.interface'
809
-
810
- import { cn } from '@/lib/utils'
811
- import { Button } from '@/components/ui/button'
812
- import { Calendar } from '@/components/ui/calendar'
813
- import {
814
- Dialog,
815
- DialogContent,
816
- DialogHeader,
817
- DialogTitle,
818
- DialogFooter,
819
- DialogTrigger,
820
- } from '@/components/ui/dialog'
821
- import {
822
- Form,
823
- FormControl,
824
- FormField,
825
- FormItem,
826
- FormLabel,
827
- FormMessage,
828
- } from '@/components/ui/form'
829
- import { Input } from '@/components/ui/input'
830
- import { Textarea } from '@/components/ui/textarea'
831
- import {
832
- Popover,
833
- PopoverContent,
834
- PopoverTrigger,
835
- } from '@/components/ui/popover'
836
- import { ArrowRight, CalendarIcon, Trash2 } from 'lucide-react'
837
-
838
- // 1. Define form schema
839
- const schema = z.object({
840
- id: z.string().optional(),
841
- title: z.string().min(3, 'Title must be at least 3 characters'),
842
- description: z.string().optional(),
843
- dueDate: z.date().transform(value => value.toISOString()).optional(),
844
- })
845
-
846
- type TaskDialogProps = {
847
- defaultValues?: Task;
848
- children: React.ReactNode;
849
- }
850
-
851
- export function TaskDialog({ defaultValues, children }: TaskDialogProps) {
852
- // 2. Setup references and API
853
- const triggerRef = useRef<HTMLDivElement>(null)
854
- const queryClient = useQueryClient()
855
- const upsertMutation = api.task.upsert.useMutation()
856
- const deleteMutation = api.task.delete.useMutation()
857
-
858
- // 3. Initialize form with Zod
859
- const form = useFormWithZod({
860
- schema: schema,
861
- defaultValues: defaultValues || { title: '', description: '' },
862
- onSubmit: async (values) => {
863
- const result = await tryCatch(upsertMutation.mutate({ body: values }))
864
-
865
- if (result.error) {
866
- toast.error('Error saving task. Please try again.')
867
- return
868
- }
869
-
870
- if (values.id) toast.success('Task updated successfully!')
871
- if (!values.id) toast.success('Task created successfully!')
872
-
873
- // 4. Invalidate queries to refetch data
874
- queryClient.invalidate(['task.list'])
875
- form.reset()
876
- triggerRef.current?.click() // Close dialog
877
- }
878
- })
879
-
880
- // 5. Handle delete action
881
- const handleDelete = async (task: Task) => {
882
- const result = await tryCatch(deleteMutation.mutate({ params: { id: task.id } }))
883
-
884
- if (result.error) {
885
- toast.error('Error deleting task. Please try again.')
886
- return
887
- }
888
-
889
- toast.success('Task deleted successfully!')
890
- queryClient.invalidate(['task.list'])
891
- triggerRef.current?.click() // Close dialog
892
- }
893
-
894
- // 6. Render form
895
- return (
896
- <Dialog onOpenChange={() => form.reset()}>
897
- <DialogTrigger asChild>
898
- <div ref={triggerRef}>
899
- {children}
900
- </div>
901
- </DialogTrigger>
902
- <DialogContent className="sm:max-w-[425px]">
903
- <DialogHeader>
904
- <DialogTitle>
905
- {defaultValues ? 'Edit Task' : 'Create Task'}
906
- </DialogTitle>
907
- </DialogHeader>
908
-
909
- <Form {...form}>
910
- <form onSubmit={form.onSubmit} className="space-y-4 py-4">
911
- {/* Title field */}
912
- <FormField
913
- control={form.control}
914
- name="title"
915
- render={({ field }) => (
916
- <FormItem>
917
- <FormLabel>Title</FormLabel>
918
- <FormControl>
919
- <Input placeholder="Task title..." {...field} />
920
- </FormControl>
921
- <FormMessage />
922
- </FormItem>
923
- )}
924
- />
925
-
926
- {/* Description field */}
927
- <FormField
928
- control={form.control}
929
- name="description"
930
- render={({ field }) => (
931
- <FormItem>
932
- <FormLabel>Description</FormLabel>
933
- <FormControl>
934
- <Textarea
935
- placeholder="Task description..."
936
- {...field}
937
- />
938
- </FormControl>
939
- <FormMessage />
940
- </FormItem>
941
- )}
942
- />
943
-
944
- {/* Due Date field */}
945
- <FormField
946
- control={form.control}
947
- name="dueDate"
948
- render={({ field }) => (
949
- <FormItem className="flex flex-col">
950
- <FormLabel>Due Date</FormLabel>
951
- <Popover>
952
- <PopoverTrigger asChild>
953
- <FormControl>
954
- <Button
955
- variant="outline"
956
- className={cn(
957
- 'w-full pl-3 text-left font-normal',
958
- !field.value && 'text-muted-foreground'
959
- )}
960
- >
961
- {field.value ? (
962
- format(field.value, 'PPP')
963
- ) : (
964
- <span>Pick a date</span>
965
- )}
966
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
967
- </Button>
968
- </FormControl>
969
- </PopoverTrigger>
970
- <PopoverContent className="w-auto p-0" align="start">
971
- <Calendar
972
- mode="single"
973
- selected={field.value ? new Date(field.value) : undefined}
974
- onSelect={field.onChange}
975
- disabled={(date) =>
976
- date < new Date(new Date().setHours(0, 0, 0, 0))
977
- }
978
- initialFocus
979
- />
980
- </PopoverContent>
981
- </Popover>
982
- <FormMessage />
983
- </FormItem>
984
- )}
985
- />
986
- </form>
987
- </Form>
988
-
989
- {/* Action buttons */}
990
- <DialogFooter className="sm:justify-between">
991
- <Button type="submit" onClick={form.onSubmit}>
992
- {defaultValues ? 'Update' : 'Create'}
993
- <ArrowRight className="ml-2" />
994
- </Button>
995
- {defaultValues && (
996
- <Button variant="destructive" onClick={() => handleDelete(defaultValues)}>
997
- <Trash2 className="mr-2 h-4 w-4" />
998
- Delete
999
- </Button>
1000
- )}
1001
- </DialogFooter>
1002
- </DialogContent>
1003
- </Dialog>
1004
- )
1005
- }
1006
- ```
1007
-
1008
- This example demonstrates:
1009
- - Schema definition with Zod
1010
- - Form state management with useFormWithZod
1011
- - Form field rendering with shadcn/ui components
1012
- - Form submission with error handling
1013
- - Cache invalidation after successful submission
1014
- - Delete functionality with confirmation
1015
- - Dialog integration with proper state management
1016
-
1017
- ## Conclusion
1018
-
1019
- By following these guidelines and patterns, you can build robust, type-safe, and user-friendly forms in your Igniter.js applications. Proper form implementation not only improves the developer experience but also significantly enhances the user experience by providing clear validation feedback and smooth interactions.
1020
-
1021
- Remember that forms are often the primary way users interact with your application, so investing time in creating high-quality form experiences pays significant dividends in user satisfaction and engagement.