feat: Implement dynamic form field rendering and replace SubmitButton with Actions component
parent
076924bbd6
commit
734c62998f
@ -0,0 +1,39 @@
|
|||||||
|
import { useStore } from '@tanstack/react-form'
|
||||||
|
import type { FormType } from '../..'
|
||||||
|
import { useFormContext } from '../..'
|
||||||
|
import Button from '../../../button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type ActionsProps = {
|
||||||
|
CustomActions?: (form: FormType) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Actions = ({
|
||||||
|
CustomActions,
|
||||||
|
}: ActionsProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const form = useFormContext()
|
||||||
|
|
||||||
|
const [isSubmitting, canSubmit] = useStore(form.store, state => [
|
||||||
|
state.isSubmitting,
|
||||||
|
state.canSubmit,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (CustomActions)
|
||||||
|
return CustomActions(form)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-end p-4 pt-2'>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
disabled = {isSubmitting || !canSubmit}
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={() => form.handleSubmit()}
|
||||||
|
>
|
||||||
|
{t('common.operation.submit')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Actions
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { useStore } from '@tanstack/react-form'
|
|
||||||
import { useFormContext } from '../..'
|
|
||||||
import Button, { type ButtonProps } from '../../../button'
|
|
||||||
|
|
||||||
type SubmitButtonProps = Omit<ButtonProps, 'disabled' | 'loading' | 'onClick'>
|
|
||||||
|
|
||||||
const SubmitButton = ({ ...buttonProps }: SubmitButtonProps) => {
|
|
||||||
const form = useFormContext()
|
|
||||||
|
|
||||||
const [isSubmitting, canSubmit] = useStore(form.store, state => [
|
|
||||||
state.isSubmitting,
|
|
||||||
state.canSubmit,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting || !canSubmit}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={() => form.handleSubmit()}
|
|
||||||
{...buttonProps}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubmitButton
|
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import { type BaseConfiguration, BaseVarType } from './types'
|
||||||
|
import { withForm } from '../..'
|
||||||
|
import { useStore } from '@tanstack/react-form'
|
||||||
|
|
||||||
|
type FieldProps<T> = {
|
||||||
|
initialData?: T
|
||||||
|
config: BaseConfiguration<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Field = <T,>({
|
||||||
|
initialData,
|
||||||
|
config,
|
||||||
|
}: FieldProps<T>) => withForm({
|
||||||
|
defaultValues: initialData,
|
||||||
|
props: {
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
render: function Render({
|
||||||
|
form,
|
||||||
|
config,
|
||||||
|
}) {
|
||||||
|
const { type, label, placeholder, variable, tooltip, showConditions, max, min, options } = config
|
||||||
|
|
||||||
|
const fieldValues = useStore(form.store, state => state.values)
|
||||||
|
|
||||||
|
const isAllConditionsMet = useMemo(() => {
|
||||||
|
if (!showConditions.length) return true
|
||||||
|
return showConditions.every((condition) => {
|
||||||
|
const { variable, value } = condition
|
||||||
|
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
|
||||||
|
return fieldValue === value
|
||||||
|
})
|
||||||
|
}, [fieldValues, showConditions])
|
||||||
|
|
||||||
|
if (!isAllConditionsMet)
|
||||||
|
return <></>
|
||||||
|
|
||||||
|
if ([BaseVarType.textInput].includes(type)) {
|
||||||
|
return (
|
||||||
|
<form.AppField
|
||||||
|
name={variable}
|
||||||
|
children={field => (
|
||||||
|
<field.TextField
|
||||||
|
label={label}
|
||||||
|
tooltip={tooltip}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([BaseVarType.numberInput].includes(type)) {
|
||||||
|
return (
|
||||||
|
<form.AppField
|
||||||
|
name={variable}
|
||||||
|
children={field => (
|
||||||
|
<field.NumberInputField
|
||||||
|
label={label}
|
||||||
|
tooltip={tooltip}
|
||||||
|
placeholder={placeholder}
|
||||||
|
max={max}
|
||||||
|
min={min}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([BaseVarType.checkbox].includes(type)) {
|
||||||
|
return (
|
||||||
|
<form.AppField
|
||||||
|
name={variable}
|
||||||
|
children={field => (
|
||||||
|
<field.CheckboxField
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([BaseVarType.select].includes(type)) {
|
||||||
|
return (
|
||||||
|
<form.AppField
|
||||||
|
name={variable}
|
||||||
|
children={field => (
|
||||||
|
<field.SelectField
|
||||||
|
options={options!}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Field
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useAppForm } from '../..'
|
||||||
|
import Field from './field'
|
||||||
|
import type { BaseFormProps } from './types'
|
||||||
|
|
||||||
|
const BaseForm = <T,>({
|
||||||
|
initialData,
|
||||||
|
configurations,
|
||||||
|
onSubmit,
|
||||||
|
CustomActions,
|
||||||
|
}: BaseFormProps<T>) => {
|
||||||
|
const baseForm = useAppForm({
|
||||||
|
defaultValues: initialData,
|
||||||
|
validators: {
|
||||||
|
onSubmit: ({ value }) => {
|
||||||
|
console.log('onSubmit', value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSubmit: ({ value }) => {
|
||||||
|
onSubmit(value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className='w-full'
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
baseForm.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col gap-4 px-4 py-2'>
|
||||||
|
{configurations.map((config, index) => {
|
||||||
|
const FieldComponent = Field<T>({
|
||||||
|
initialData,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
return <FieldComponent key={index} form={baseForm} config={config} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<baseForm.AppForm>
|
||||||
|
<baseForm.Actions
|
||||||
|
CustomActions={CustomActions}
|
||||||
|
/>
|
||||||
|
</baseForm.AppForm>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(BaseForm)
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import type { DeepKeys } from '@tanstack/react-form'
|
||||||
|
import type { FormType } from '../..'
|
||||||
|
import type { Option } from '../../../select/pure'
|
||||||
|
|
||||||
|
export enum BaseVarType {
|
||||||
|
textInput = 'textInput',
|
||||||
|
numberInput = 'numberInput',
|
||||||
|
checkbox = 'checkbox',
|
||||||
|
select = 'select',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShowCondition<T> = {
|
||||||
|
variable: DeepKeys<T>
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NumberConfiguration = {
|
||||||
|
max?: number
|
||||||
|
min?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectConfiguration = {
|
||||||
|
options?: Option[] // Options for select field
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseConfiguration<T> = {
|
||||||
|
label: string
|
||||||
|
variable: DeepKeys<T> // Variable name
|
||||||
|
maxLength?: number // Max length for text input
|
||||||
|
placeholder?: string
|
||||||
|
required: boolean
|
||||||
|
showConditions: ShowCondition<T>[] // Show this field only when the all conditions are met
|
||||||
|
type: BaseVarType
|
||||||
|
tooltip?: string // Tooltip for this field
|
||||||
|
} & NumberConfiguration & SelectConfiguration
|
||||||
|
|
||||||
|
export type BaseFormProps<T> = {
|
||||||
|
initialData?: T
|
||||||
|
configurations: BaseConfiguration<T>[]
|
||||||
|
CustomActions?: (form: FormType) => React.ReactNode
|
||||||
|
onSubmit: (value: T) => void
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue