1- import React , { useState , useMemo , useEffect , useRef , useCallback , useImperativeHandle , forwardRef , ReactNode } from 'react'
2- import type { MetadataType , ApiRequest , ResponseStatus , ModalProvider } from '@/types'
1+ import React , { useState , useMemo , useEffect , useRef , useCallback , useImperativeHandle , forwardRef , ReactNode , createContext } from 'react'
2+ import type { MetadataType , ApiRequest , ResponseStatus , ModalProvider , ApiState } from '@/types'
33import type { AutoFormProps } from '@/components/types'
44import { ApiResult , HttpMethods , humanize , map , omitEmpty } from '@servicestack/client'
55import { useClient } from '@/use/client'
66import { getTypeName , transition as doTransition } from '@/use/utils'
77import { useMetadata } from '@/use/metadata'
88import { form , card , slideOver } from './css'
9- import { ModalProviderContext } from '@/use/context'
9+ import { ModalProviderContext , ApiStateContext } from '@/use/context'
1010import AutoFormFields from './AutoFormFields'
1111import PrimaryButton from './PrimaryButton'
1212import SecondaryButton from './SecondaryButton'
@@ -34,6 +34,32 @@ interface AutoFormSlots {
3434 rightbuttons ?: ( props : { instance : AutoFormRef | null , model : any } ) => ReactNode
3535}
3636
37+ /**
38+ * AutoForm component that automatically generates a form from a ServiceStack DTO type.
39+ *
40+ * The form provides ApiStateContext to all child components, allowing them to access
41+ * the form's loading and error state using the `useApiState()` hook.
42+ *
43+ * @example
44+ * ```tsx
45+ * // In a child component within AutoForm:
46+ * import { useApiState } from '@servicestack/react'
47+ *
48+ * function CustomFormField() {
49+ * const apiState = useApiState()
50+ *
51+ * if (apiState?.loading) {
52+ * return <div>Loading...</div>
53+ * }
54+ *
55+ * if (apiState?.error) {
56+ * return <div>Error: {apiState.error.message}</div>
57+ * }
58+ *
59+ * return <div>Form field content</div>
60+ * }
61+ * ```
62+ */
3763const AutoForm = forwardRef < AutoFormRef , AutoFormProps & AutoFormSlots > ( ( props , ref ) => {
3864 const {
3965 type,
@@ -56,10 +82,12 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
5682 subHeadingClass : subHeadingClassProp ,
5783 submitLabel = 'Submit' ,
5884 allowSubmit,
85+ onSubmit,
5986 onSuccess,
6087 onError,
6188 onDone,
6289 onChange,
90+ children,
6391 // Slots
6492 heading : headingSlot ,
6593 subheading : subheadingSlot ,
@@ -82,7 +110,7 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
82110 const [ api , setApi ] = useState ( new ApiResult ( ) )
83111
84112 const panelClass = useMemo ( ( ) => panelClassProp || form . panelClass ( formStyle ) , [ panelClassProp , formStyle ] )
85- const formClass = useMemo ( ( ) => formClassProp || ( formStyle === "card" ? 'shadow sm:rounded-md' : slideOver . formClass ) , [ formClassProp , formStyle ] )
113+ const formClass = useMemo ( ( ) => formClassProp || form . formClass ( formStyle ) , [ formClassProp , formStyle ] )
86114 const headingClass = useMemo ( ( ) => headingClassProp || form . headingClass ( formStyle ) , [ headingClassProp , formStyle ] )
87115 const subHeadingClass = useMemo ( ( ) => subHeadingClassProp || form . subHeadingClass ( formStyle ) , [ subHeadingClassProp , formStyle ] )
88116 const buttonsClass = useMemo ( ( ) => typeof buttonsClassProp === 'string' ? buttonsClassProp : form . buttonsClass , [ buttonsClassProp ] )
@@ -141,7 +169,11 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
141169
142170 let apiResult : ApiResult < any >
143171
144- if ( HttpMethods . hasRequestBody ( method ) ) {
172+ if ( onSubmit != null ) {
173+ let requestDto = new dto . constructor ( omitEmpty ( model ) )
174+ apiResult = await onSubmit ( requestDto )
175+ }
176+ else if ( HttpMethods . hasRequestBody ( method ) ) {
145177 let requestDto = new dto . constructor ( )
146178 let formData = new FormData ( form )
147179 if ( ! returnsVoid ) {
@@ -192,7 +224,7 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
192224 }
193225
194226 useEffect ( ( ) => {
195- doTransition ( rule1 , { value : transition1 } as any , show )
227+ doTransition ( rule1 , setTransition1 , show )
196228 if ( ! show ) {
197229 const timer = setTimeout ( done , 700 )
198230 return ( ) => clearTimeout ( timer )
@@ -285,16 +317,18 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
285317
286318 { headerSlot ?.( { instance : instanceRef , model } ) }
287319 < input type = "submit" className = "hidden" />
288- < AutoFormFields
289- ref = { formFieldsRef }
290- key = { formFieldsKey }
291- type = { type as string }
292- value = { model }
293- onChange = { update }
294- api = { api }
295- configureField = { configureField }
296- configureFormLayout = { configureFormLayout }
297- />
320+ { children || (
321+ < AutoFormFields
322+ ref = { formFieldsRef }
323+ key = { formFieldsKey }
324+ type = { type }
325+ value = { model }
326+ onChange = { update }
327+ api = { api }
328+ configureField = { configureField }
329+ configureFormLayout = { configureFormLayout }
330+ />
331+ ) }
298332 { footerSlot ?.( { instance : instanceRef , model } ) }
299333 </ div >
300334 </ div >
@@ -324,31 +358,33 @@ const AutoForm = forwardRef<AutoFormRef, AutoFormProps & AutoFormSlots>((props,
324358 )
325359
326360 return (
327- < ModalProviderContext . Provider value = { modalProvider } >
328- < div >
329- { formStyle === 'card' ? (
330- < div className = { panelClass } >
331- { formContent ( false ) }
332- </ div >
333- ) : (
334- < div className = "relative z-10" aria-labelledby = "slide-over-title" role = "dialog" aria-modal = "true" >
335- < div className = "fixed inset-0" > </ div >
336- < div className = "fixed inset-0 overflow-hidden" >
337- < div onMouseDown = { close } className = "absolute inset-0 overflow-hidden" >
338- < div onMouseDown = { ( e ) => e . stopPropagation ( ) } className = "pointer-events-none fixed inset-y-0 right-0 flex pl-10" >
339- < div className = { `pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${ transition1 } ` } >
340- { formContent ( true ) }
361+ < ApiStateContext . Provider value = { client } >
362+ < ModalProviderContext . Provider value = { modalProvider } >
363+ < div >
364+ { formStyle === 'card' ? (
365+ < div className = { panelClass } >
366+ { formContent ( false ) }
367+ </ div >
368+ ) : (
369+ < div className = "relative z-10" aria-labelledby = "slide-over-title" role = "dialog" aria-modal = "true" >
370+ < div className = "fixed inset-0" > </ div >
371+ < div className = "fixed inset-0 overflow-hidden" >
372+ < div onMouseDown = { close } className = "absolute inset-0 overflow-hidden" >
373+ < div onMouseDown = { ( e ) => e . stopPropagation ( ) } className = "pointer-events-none fixed inset-y-0 right-0 flex pl-10" >
374+ < div className = { `pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${ transition1 } ` } >
375+ { formContent ( true ) }
376+ </ div >
341377 </ div >
342378 </ div >
343379 </ div >
344380 </ div >
345- </ div >
346- ) }
347- { modal ?. name === 'ModalLookup' && modal . ref && (
348- < ModalLookup refInfo = { modal . ref } onDone = { openModalDone } configureField = { configureField } />
349- ) }
350- </ div >
351- </ ModalProviderContext . Provider >
381+ ) }
382+ { modal ?. name === 'ModalLookup' && modal . ref && (
383+ < ModalLookup refInfo = { modal . ref } onDone = { openModalDone } configureField = { configureField } />
384+ ) }
385+ </ div >
386+ </ ModalProviderContext . Provider >
387+ </ ApiStateContext . Provider >
352388 )
353389} )
354390
0 commit comments