feat: Implement dynamic form field rendering and replace SubmitButton with Actions component

This commit is contained in:
twwu
2025-04-25 18:13:52 +08:00
parent 076924bbd6
commit 734c62998f
10 changed files with 294 additions and 47 deletions

View File

@@ -1,18 +1,13 @@
import cn from '@/utils/classnames'
import { useFieldContext } from '../..'
import type { PureSelectProps } from '../../../select/pure'
import type { Option, PureSelectProps } from '../../../select/pure'
import PureSelect from '../../../select/pure'
import Label from '../label'
import { useCallback } from 'react'
type SelectOption = {
value: string
label: string
}
type SelectFieldProps = {
label: string
options: SelectOption[]
options: Option[]
onChange?: (value: string) => void
isRequired?: boolean
showOptional?: boolean

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -318,9 +318,7 @@ const InputFieldForm = ({
{t('common.operation.cancel')}
</Button>
<form.AppForm>
<form.SubmitButton variant='primary'>
{t('common.operation.save')}
</form.SubmitButton>
<form.Actions />
</form.AppForm>
</div>
</form>

View File

@@ -5,7 +5,9 @@ import CheckboxField from './components/field/checkbox'
import SelectField from './components/field/select'
import CustomSelectField from './components/field/custom-select'
import OptionsField from './components/field/options'
import SubmitButton from './components/form/submit-button'
import Actions from './components/form/actions'
export type FormType = ReturnType<typeof useFormContext>
export const { fieldContext, useFieldContext, formContext, useFormContext }
= createFormHookContexts()
@@ -20,7 +22,7 @@ export const { useAppForm, withForm } = createFormHook({
OptionsField,
},
formComponents: {
SubmitButton,
Actions,
},
fieldContext,
formContext,

View File

@@ -17,7 +17,7 @@ import type {
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
type Option = {
export type Option = {
label: string
value: string
}

View File

@@ -1,19 +1,62 @@
'use client'
import InputFieldForm from '../components/base/form/form-scenarios/input-field'
// import DemoForm from '../components/base/form/form-scenarios/demo'
import InputFieldForm from '../components/base/form/form-scenarios/base'
import { BaseVarType } from '../components/base/form/form-scenarios/base/types'
export default function Page() {
return (
<div className='flex h-screen w-full items-center justify-center p-20'>
<div className='w-[400px] rounded-lg border border-gray-800 bg-components-panel-bg'>
<div className='w-[400px] rounded-lg border border-components-panel-border bg-components-panel-bg'>
<InputFieldForm
initialData={undefined}
supportFile
onCancel={() => { console.log('cancel') }}
onSubmit={value => console.log('submit', value)}
initialData={{
type: 'option_1',
variable: 'test',
label: 'Test',
required: true,
}}
configurations={[
{
type: BaseVarType.textInput,
variable: 'variable',
label: 'Variable',
required: true,
showConditions: [{
variable: 'type',
value: 'option_1',
}],
},
{
type: BaseVarType.numberInput,
variable: 'max_length',
label: 'Max Length',
required: true,
showConditions: [],
max: 100,
min: 1,
},
{
type: BaseVarType.checkbox,
variable: 'required',
label: 'Required',
required: true,
showConditions: [],
},
{
type: BaseVarType.select,
variable: 'type',
label: 'Type',
required: true,
showConditions: [],
options: [
{ label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' },
{ label: 'Option 3', value: 'option_3' },
],
},
]}
onSubmit={(value) => {
console.log('onSubmit', value)
}}
/>
{/* <DemoForm /> */}
</div>
</div>
)