Skip to content

Commit 5476b80

Browse files
committed
Add support for ApiState context in forms accessible from child components
1 parent c7cfb75 commit 5476b80

26 files changed

+20078
-257
lines changed

src/components/AutoCreateForm.tsx

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { form } from './css'
77
import { getTypeName, transition as doTransition } from '@/use/utils'
88
import { Sole } from '@/use/config'
99
import { ApiResult, HttpMethods, humanize, map } from '@servicestack/client'
10-
import { ModalProviderContext } from '@/use/context'
10+
import { ModalProviderContext, ApiStateContext } from '@/use/context'
1111
import AutoFormFields from './AutoFormFields'
1212
import PrimaryButton from './PrimaryButton'
1313
import SecondaryButton from './SecondaryButton'
@@ -31,6 +31,32 @@ interface AutoCreateFormSlots {
3131
children?: ReactNode
3232
}
3333

34+
/**
35+
* AutoCreateForm component for creating new entities with ServiceStack AutoQuery CRUD APIs.
36+
*
37+
* The form provides ApiStateContext to all child components, allowing them to access
38+
* the form's loading and error state using the `useApiState()` hook.
39+
*
40+
* @example
41+
* ```tsx
42+
* // In a child component within AutoCreateForm:
43+
* import { useApiState } from '@servicestack/react'
44+
*
45+
* function CustomFormField() {
46+
* const apiState = useApiState()
47+
*
48+
* if (apiState?.loading) {
49+
* return <div>Saving...</div>
50+
* }
51+
*
52+
* if (apiState?.error) {
53+
* return <div>Error: {apiState.error.message}</div>
54+
* }
55+
*
56+
* return <div>Form field content</div>
57+
* }
58+
* ```
59+
*/
3460
const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoCreateFormSlots>((props, ref) => {
3561
const {
3662
type,
@@ -307,32 +333,34 @@ const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoC
307333
)
308334

309335
return (
310-
<ModalProviderContext.Provider value={modalProvider}>
311-
<div>
312-
{formStyle === 'card' ? (
313-
<div className={panelClass}>
314-
{formContent(false)}
315-
</div>
316-
) : (
317-
<div className="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
318-
<div className="fixed inset-0"></div>
319-
<div className="fixed inset-0 overflow-hidden">
320-
<div onMouseDown={close} className="absolute inset-0 overflow-hidden">
321-
<div onMouseDown={(e) => e.stopPropagation()} className="pointer-events-none fixed inset-y-0 right-0 flex pl-10">
322-
<div className={`pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${transition1}`}>
323-
{formContent(true)}
336+
<ApiStateContext.Provider value={client}>
337+
<ModalProviderContext.Provider value={modalProvider}>
338+
<div>
339+
{formStyle === 'card' ? (
340+
<div className={panelClass}>
341+
{formContent(false)}
342+
</div>
343+
) : (
344+
<div className="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
345+
<div className="fixed inset-0"></div>
346+
<div className="fixed inset-0 overflow-hidden">
347+
<div onMouseDown={close} className="absolute inset-0 overflow-hidden">
348+
<div onMouseDown={(e) => e.stopPropagation()} className="pointer-events-none fixed inset-y-0 right-0 flex pl-10">
349+
<div className={`pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${transition1}`}>
350+
{formContent(true)}
351+
</div>
324352
</div>
325353
</div>
326354
</div>
327355
</div>
328-
</div>
329-
)}
356+
)}
330357

331-
{modal?.name === 'ModalLookup' && modal.ref && (
332-
<ModalLookup refInfo={modal.ref} onDone={openModalDone} configureField={configureField} />
333-
)}
334-
</div>
335-
</ModalProviderContext.Provider>
358+
{modal?.name === 'ModalLookup' && modal.ref && (
359+
<ModalLookup refInfo={modal.ref} onDone={openModalDone} configureField={configureField} />
360+
)}
361+
</div>
362+
</ModalProviderContext.Provider>
363+
</ApiStateContext.Provider>
336364
)
337365
})
338366

src/components/AutoEditForm.tsx

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { form } from './css'
77
import { getTypeName, transition as doTransition } from '@/use/utils'
88
import { Sole } from '@/use/config'
99
import { ApiResult, HttpMethods, humanize, map, mapGet } from '@servicestack/client'
10-
import { ModalProviderContext } from '@/use/context'
10+
import { ModalProviderContext, ApiStateContext } from '@/use/context'
1111
import AutoFormFields from './AutoFormFields'
1212
import PrimaryButton from './PrimaryButton'
1313
import SecondaryButton from './SecondaryButton'
@@ -32,6 +32,32 @@ interface AutoEditFormSlots {
3232
children?: ReactNode
3333
}
3434

35+
/**
36+
* AutoEditForm component for editing existing entities with ServiceStack AutoQuery CRUD APIs.
37+
*
38+
* The form provides ApiStateContext to all child components, allowing them to access
39+
* the form's loading and error state using the `useApiState()` hook.
40+
*
41+
* @example
42+
* ```tsx
43+
* // In a child component within AutoEditForm:
44+
* import { useApiState } from '@servicestack/react'
45+
*
46+
* function CustomFormField() {
47+
* const apiState = useApiState()
48+
*
49+
* if (apiState?.loading) {
50+
* return <div>Updating...</div>
51+
* }
52+
*
53+
* if (apiState?.error) {
54+
* return <div>Error: {apiState.error.message}</div>
55+
* }
56+
*
57+
* return <div>Form field content</div>
58+
* }
59+
* ```
60+
*/
3561
const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFormSlots>((props, ref) => {
3662
const {
3763
type,
@@ -417,32 +443,34 @@ const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFor
417443
)
418444

419445
return (
420-
<ModalProviderContext.Provider value={modalProvider}>
421-
<div>
422-
{formStyle === 'card' ? (
423-
<div className={panelClass}>
424-
{formContent(false)}
425-
</div>
426-
) : (
427-
<div className="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
428-
<div className="fixed inset-0"></div>
429-
<div className="fixed inset-0 overflow-hidden">
430-
<div onMouseDown={close} className="absolute inset-0 overflow-hidden">
431-
<div onMouseDown={(e) => e.stopPropagation()} className="pointer-events-none fixed inset-y-0 right-0 flex pl-10">
432-
<div className={`pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${transition1}`}>
433-
{formContent(true)}
446+
<ApiStateContext.Provider value={client}>
447+
<ModalProviderContext.Provider value={modalProvider}>
448+
<div>
449+
{formStyle === 'card' ? (
450+
<div className={panelClass}>
451+
{formContent(false)}
452+
</div>
453+
) : (
454+
<div className="relative z-10" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
455+
<div className="fixed inset-0"></div>
456+
<div className="fixed inset-0 overflow-hidden">
457+
<div onMouseDown={close} className="absolute inset-0 overflow-hidden">
458+
<div onMouseDown={(e) => e.stopPropagation()} className="pointer-events-none fixed inset-y-0 right-0 flex pl-10">
459+
<div className={`pointer-events-auto w-screen xl:max-w-3xl md:max-w-xl max-w-lg ${transition1}`}>
460+
{formContent(true)}
461+
</div>
434462
</div>
435463
</div>
436464
</div>
437465
</div>
438-
</div>
439-
)}
466+
)}
440467

441-
{modal?.name === 'ModalLookup' && modal.ref && (
442-
<ModalLookup refInfo={modal.ref} onDone={openModalDone} configureField={configureField} />
443-
)}
444-
</div>
445-
</ModalProviderContext.Provider>
468+
{modal?.name === 'ModalLookup' && modal.ref && (
469+
<ModalLookup refInfo={modal.ref} onDone={openModalDone} configureField={configureField} />
470+
)}
471+
</div>
472+
</ModalProviderContext.Provider>
473+
</ApiStateContext.Provider>
446474
)
447475
})
448476

src/components/AutoForm.tsx

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
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'
33
import type { AutoFormProps } from '@/components/types'
44
import { ApiResult, HttpMethods, humanize, map, omitEmpty } from '@servicestack/client'
55
import { useClient } from '@/use/client'
66
import { getTypeName, transition as doTransition } from '@/use/utils'
77
import { useMetadata } from '@/use/metadata'
88
import { form, card, slideOver } from './css'
9-
import { ModalProviderContext } from '@/use/context'
9+
import { ModalProviderContext, ApiStateContext } from '@/use/context'
1010
import AutoFormFields from './AutoFormFields'
1111
import PrimaryButton from './PrimaryButton'
1212
import 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+
*/
3763
const 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

src/components/AutoFormFields.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const AutoFormFields = forwardRef<AutoFormFieldsRef, AutoFormFieldsProps>(({
3232

3333
const { metadataApi, apiOf, typeOf, typeOfRef, createFormLayout, Crud } = useMetadata()
3434

35-
const typeName = useMemo(() => typeProp || getTypeName(modelValue), [typeProp, modelValue])
35+
const typeName = useMemo(() => getTypeName(typeProp || modelValue), [typeProp, modelValue])
3636

3737
const type = useMemo(() => metaType ?? typeOf(typeName), [metaType, typeName, typeOf])
3838

0 commit comments

Comments
 (0)