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