@@ -15,8 +15,48 @@ import {
1515import { renderChildren } from '../../lib/utils' ;
1616import { Alert , AlertDescription } from '@/ui/alert' ;
1717import { AlertCircle , Loader2 } from 'lucide-react' ;
18+ import { cn } from '@/lib/utils' ;
1819import React from 'react' ;
1920
21+ // TypeScript interfaces for type safety
22+ interface SelectOption {
23+ label : string ;
24+ value : string ;
25+ }
26+
27+ interface FieldValidation {
28+ required ?: string | boolean ;
29+ minLength ?: { value : number ; message : string } ;
30+ maxLength ?: { value : number ; message : string } ;
31+ min ?: { value : number ; message : string } ;
32+ max ?: { value : number ; message : string } ;
33+ pattern ?: { value : string | RegExp ; message : string } ;
34+ validate ?: ( value : any ) => boolean | string ;
35+ }
36+
37+ interface FieldCondition {
38+ field : string ;
39+ equals ?: any ;
40+ notEquals ?: any ;
41+ in ?: any [ ] ;
42+ }
43+
44+ interface FormFieldConfig {
45+ id ?: string ;
46+ name : string ;
47+ label ?: string ;
48+ description ?: string ;
49+ type ?: string ;
50+ inputType ?: string ;
51+ required ?: boolean ;
52+ disabled ?: boolean ;
53+ placeholder ?: string ;
54+ options ?: SelectOption [ ] ;
55+ validation ?: FieldValidation ;
56+ condition ?: FieldCondition ;
57+ [ key : string ] : any ;
58+ }
59+
2060// Form renderer component - Airtable-style feature-complete form
2161ComponentRegistry . register ( 'form' ,
2262 ( { schema, className, onAction, ...props } ) => {
@@ -44,9 +84,9 @@ ComponentRegistry.register('form',
4484 const [ isSubmitting , setIsSubmitting ] = React . useState ( false ) ;
4585 const [ submitError , setSubmitError ] = React . useState < string | null > ( null ) ;
4686
47- // Watch for form changes
87+ // Watch for form changes - only track changes when onAction is available
4888 React . useEffect ( ( ) => {
49- if ( onChangeProp && onAction ) {
89+ if ( onAction ) {
5090 const subscription = form . watch ( ( data ) => {
5191 onAction ( {
5292 type : 'form_change' ,
@@ -56,7 +96,7 @@ ComponentRegistry.register('form',
5696 } ) ;
5797 return ( ) => subscription . unsubscribe ( ) ;
5898 }
59- } , [ form , onAction , onChangeProp ] ) ;
99+ } , [ form , onAction ] ) ;
60100
61101 // Handle form submission
62102 const handleSubmit = form . handleSubmit ( async ( data ) => {
@@ -85,9 +125,19 @@ ComponentRegistry.register('form',
85125 if ( resetOnSubmit ) {
86126 form . reset ( ) ;
87127 }
88- } catch ( error : any ) {
89- setSubmitError ( error ?. message || 'An error occurred during submission' ) ;
90- console . error ( 'Form submission error:' , error ) ;
128+ } catch ( error ) {
129+ // Handle different error types safely
130+ const errorMessage = error instanceof Error
131+ ? error . message
132+ : typeof error === 'string'
133+ ? error
134+ : 'An error occurred during submission' ;
135+ setSubmitError ( errorMessage ) ;
136+
137+ // Only log errors in development
138+ if ( process . env . NODE_ENV === 'development' ) {
139+ console . error ( 'Form submission error:' , error ) ;
140+ }
91141 } finally {
92142 setIsSubmitting ( false ) ;
93143 }
@@ -104,9 +154,15 @@ ComponentRegistry.register('form',
104154 }
105155 } ;
106156
107- // Determine grid classes based on columns
157+ // Determine grid classes based on columns (explicit classes for Tailwind JIT)
158+ const gridColsClass =
159+ columns === 1 ? '' :
160+ columns === 2 ? 'md:grid-cols-2' :
161+ columns === 3 ? 'md:grid-cols-3' :
162+ 'md:grid-cols-4' ;
163+
108164 const gridClass = columns > 1
109- ? ` grid gap-4 md:grid-cols- ${ Math . min ( columns , 4 ) } `
165+ ? cn ( ' grid gap-4' , gridColsClass )
110166 : 'space-y-4' ;
111167
112168 return (
@@ -129,7 +185,7 @@ ComponentRegistry.register('form',
129185 ) : (
130186 // Otherwise render fields from schema
131187 < div className = { schema . fieldContainerClass || gridClass } >
132- { fields . map ( ( field : any , index : number ) => {
188+ { fields . map ( ( field : FormFieldConfig , index : number ) => {
133189 const {
134190 name,
135191 label,
@@ -142,18 +198,21 @@ ComponentRegistry.register('form',
142198 ...fieldProps
143199 } = field ;
144200
145- // Handle conditional rendering
201+ // Handle conditional rendering with null/undefined safety
146202 if ( condition ) {
147203 const watchField = condition . field ;
148204 const watchValue = form . watch ( watchField ) ;
149205
150- if ( condition . equals && watchValue !== condition . equals ) {
206+ // Check for null/undefined before evaluating conditions
207+ const hasValue = watchValue !== undefined && watchValue !== null ;
208+
209+ if ( condition . equals !== undefined && watchValue !== condition . equals ) {
151210 return null ;
152211 }
153- if ( condition . notEquals && watchValue === condition . notEquals ) {
212+ if ( condition . notEquals !== undefined && watchValue === condition . notEquals ) {
154213 return null ;
155214 }
156- if ( condition . in && ! condition . in . includes ( watchValue ) ) {
215+ if ( condition . in && ( ! hasValue || ! condition . in . includes ( watchValue ) ) ) {
157216 return null ;
158217 }
159218 }
@@ -164,12 +223,17 @@ ComponentRegistry.register('form',
164223 } ;
165224
166225 if ( required ) {
167- rules . required = validation . required || `${ label || name } is required` ;
226+ rules . required = typeof validation . required === 'string'
227+ ? validation . required
228+ : `${ label || name } is required` ;
168229 }
169230
231+ // Use field.id or field.name for stable keys (never use index alone)
232+ const fieldKey = field . id ?? name ;
233+
170234 return (
171235 < FormField
172- key = { field . id || name || index }
236+ key = { fieldKey }
173237 control = { form . control }
174238 name = { name }
175239 rules = { rules }
@@ -178,7 +242,11 @@ ComponentRegistry.register('form',
178242 { label && (
179243 < FormLabel >
180244 { label }
181- { required && < span className = "text-destructive ml-1" > *</ span > }
245+ { required && (
246+ < span className = "text-destructive ml-1" aria-label = "required" >
247+ *
248+ </ span >
249+ ) }
182250 </ FormLabel >
183251 ) }
184252 < FormControl >
@@ -188,6 +256,7 @@ ComponentRegistry.register('form',
188256 ...formField ,
189257 inputType : fieldProps . inputType ,
190258 options : fieldProps . options ,
259+ placeholder : fieldProps . placeholder ,
191260 disabled : disabled || fieldDisabled || isSubmitting ,
192261 } ) }
193262 </ FormControl >
@@ -303,16 +372,26 @@ ComponentRegistry.register('form',
303372 }
304373) ;
305374
306- // Helper function to render field components
307- function renderFieldComponent ( type : string , props : any ) {
308- const { schema, inputType, options, ...fieldProps } = props ;
375+ // Helper function to render field components with proper typing
376+ interface RenderFieldProps {
377+ inputType ?: string ;
378+ options ?: SelectOption [ ] ;
379+ placeholder ?: string ;
380+ value ?: any ;
381+ onChange ?: ( value : any ) => void ;
382+ disabled ?: boolean ;
383+ [ key : string ] : any ;
384+ }
385+
386+ function renderFieldComponent ( type : string , props : RenderFieldProps ) {
387+ const { inputType, options = [ ] , placeholder, ...fieldProps } = props ;
309388
310389 switch ( type ) {
311390 case 'input' :
312- return < Input type = { inputType || 'text' } { ...fieldProps } /> ;
391+ return < Input type = { inputType || 'text' } placeholder = { placeholder } { ...fieldProps } /> ;
313392
314393 case 'textarea' :
315- return < Textarea { ...fieldProps } /> ;
394+ return < Textarea placeholder = { placeholder } { ...fieldProps } /> ;
316395
317396 case 'checkbox' :
318397 // For checkbox, we need to handle the value differently
@@ -330,13 +409,19 @@ function renderFieldComponent(type: string, props: any) {
330409 case 'select' :
331410 // For select with react-hook-form, we need to handle the onChange
332411 const { value : selectValue , onChange : selectOnChange , ...selectProps } = fieldProps ;
412+
413+ // Safety check for options
414+ if ( ! options || options . length === 0 ) {
415+ return < div className = "text-sm text-muted-foreground" > No options available</ div > ;
416+ }
417+
333418 return (
334419 < Select value = { selectValue } onValueChange = { selectOnChange } { ...selectProps } >
335420 < SelectTrigger >
336- < SelectValue placeholder = " Select an option" />
421+ < SelectValue placeholder = { placeholder ?? ' Select an option' } />
337422 </ SelectTrigger >
338423 < SelectContent >
339- { options ? .map ( ( opt : any ) => (
424+ { options . map ( ( opt : SelectOption ) => (
340425 < SelectItem key = { opt . value } value = { opt . value } >
341426 { opt . label }
342427 </ SelectItem >
@@ -346,6 +431,6 @@ function renderFieldComponent(type: string, props: any) {
346431 ) ;
347432
348433 default :
349- return < Input type = { inputType || 'text' } { ...fieldProps } /> ;
434+ return < Input type = { inputType || 'text' } placeholder = { placeholder } { ...fieldProps } /> ;
350435 }
351436}
0 commit comments