Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce code verbosity of forms #683

Merged
merged 15 commits into from
Jan 9, 2023
4 changes: 2 additions & 2 deletions administration/src/application/FormType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SetState } from './useUpdateStateCallback'

export type Form<State, Options extends {}, ValidatedInput, AdditionalProps extends {}> = {
initialState: State
getValidatedInput: GetValidatedInput<State, Options, ValidatedInput>
validate: Validate<State, Options, ValidatedInput>
getArrayBufferKeys: (state: State) => number[]
Component: (props: Props<State, AdditionalProps, Options>) => ReactElement | null
}
Expand All @@ -13,7 +13,7 @@ export type ValidationError = { type: 'error'; message?: string }
export type ValidationResult<I> = ValidationError | ValidationSuccess<I>

// Do not require an `options` parameter, if Options is an empty object.
export type GetValidatedInput<State, Options, ValidatedInput> = {} extends Options
export type Validate<State, Options, ValidatedInput> = {} extends Options
? (state: State, options?: Options) => ValidationResult<ValidatedInput>
: (state: State, options: Options) => ValidationResult<ValidatedInput>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ApplyController = () => {
}

const submit = () => {
const validationResult = ApplicationForm.getValidatedInput(state)
const validationResult = ApplicationForm.validate(state)
if (validationResult.type === 'error') {
enqueueSnackbar('Ungültige bzw. fehlende Eingaben entdeckt. Bitte prüfen Sie die rot markierten Felder.', {
variant: 'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const useFormAsStep = <
): { label: string; validate: () => ValidationResult<unknown>; element: ReactNode } => {
const state = parentState[keyInParent]
const setState = useUpdateStateCallback(setParentState, keyInParent)
const validate = useCallback(() => form.getValidatedInput(state, options), [state, form, options])
const validate = useCallback(() => form.validate(state, options), [state, form, options])
const formProps = { ...additionalProps, options, state, setState }
const element = <form.Component {...formProps} />
return { label, validate, element }
Expand Down
6 changes: 3 additions & 3 deletions administration/src/application/components/SwitchComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'

const SwitchComponent = ({
const SwitchComponent = <T extends string>({
children,
value,
}: {
children: { [key: string]: React.ReactElement | null }
value: string | null
children: { [key in T]: React.ReactElement | null }
value: T | null
}) => {
if (value === null) return null
return children[value]
Expand Down
60 changes: 19 additions & 41 deletions administration/src/application/components/forms/AddressForm.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
import { AddressInput } from '../../../generated/graphql'
import ShortTextForm, { ShortTextFormState } from '../primitive-inputs/ShortTextForm'
import ShortTextForm from '../primitive-inputs/ShortTextForm'
import { useUpdateStateCallback } from '../../useUpdateStateCallback'
import { Form } from '../../FormType'
import {
CompoundState,
createCompoundGetArrayBufferKeys,
createCompoundValidate,
createCompoundInitialState,
} from '../../compoundFormUtils'

export type AddressFormState = {
street: ShortTextFormState
houseNumber: ShortTextFormState
location: ShortTextFormState
postalCode: ShortTextFormState
const SubForms = {
street: ShortTextForm,
houseNumber: ShortTextForm,
location: ShortTextForm,
postalCode: ShortTextForm,
}

type State = CompoundState<typeof SubForms>
type ValidatedInput = AddressInput
type Options = {}
type AdditionalProps = {}
const AddressForm: Form<AddressFormState, Options, ValidatedInput, AdditionalProps> = {
initialState: {
street: ShortTextForm.initialState,
houseNumber: ShortTextForm.initialState,
location: ShortTextForm.initialState,
postalCode: ShortTextForm.initialState,
},
getArrayBufferKeys: state => [
...ShortTextForm.getArrayBufferKeys(state.street),
...ShortTextForm.getArrayBufferKeys(state.houseNumber),
...ShortTextForm.getArrayBufferKeys(state.location),
...ShortTextForm.getArrayBufferKeys(state.postalCode),
],
getValidatedInput: state => {
const street = ShortTextForm.getValidatedInput(state.street)
const houseNumber = ShortTextForm.getValidatedInput(state.houseNumber)
const location = ShortTextForm.getValidatedInput(state.location)
const postalCode = ShortTextForm.getValidatedInput(state.postalCode)
if (
street.type === 'error' ||
houseNumber.type === 'error' ||
location.type === 'error' ||
postalCode.type === 'error'
)
return { type: 'error' }
return {
type: 'valid',
value: {
street: street.value,
houseNumber: houseNumber.value,
location: location.value,
postalCode: postalCode.value,
},
}
},

const AddressForm: Form<State, Options, ValidatedInput, AdditionalProps> = {
initialState: createCompoundInitialState(SubForms),
getArrayBufferKeys: createCompoundGetArrayBufferKeys(SubForms),
validate: createCompoundValidate(SubForms, {}),
Component: ({ state, setState }) => (
<>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
Expand Down
48 changes: 21 additions & 27 deletions administration/src/application/components/forms/ApplicationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
import { ApplicationInput, CardType } from '../../../generated/graphql'
import { Form } from '../../FormType'
import PersonalDataForm, { PersonalDataFormState } from './PersonalDataForm'
import StepCardTypeForm, { StepCardTypeFormState } from './StepCardTypeForm'
import StepRequirementsForm, { StepRequirementsFormState } from './StepRequirementsForm'
import StepSendForm, { StepSendFormState } from './StepSendForm'
import PersonalDataForm from './PersonalDataForm'
import StepCardTypeForm from './StepCardTypeForm'
import StepRequirementsForm from './StepRequirementsForm'
import StepSendForm from './StepSendForm'
import SteppedSubForms, { useFormAsStep } from '../SteppedSubForms'
import { useUpdateStateCallback } from '../../useUpdateStateCallback'
import { CompoundState, createCompoundGetArrayBufferKeys, createCompoundInitialState } from '../../compoundFormUtils'

type RegionId = number

export type ApplicationFormState = {
activeStep: number
stepPersonalData: PersonalDataFormState
stepCardType: StepCardTypeFormState
stepRequirements: StepRequirementsFormState
stepSend: StepSendFormState
const SubForms = {
stepPersonalData: PersonalDataForm,
stepCardType: StepCardTypeForm,
stepRequirements: StepRequirementsForm,
stepSend: StepSendForm,
}

type State = { activeStep: number } & CompoundState<typeof SubForms>
type ValidatedInput = [RegionId, ApplicationInput]
type Options = {}
type AdditionalProps = { onSubmit: () => void; loading: boolean; privacyPolicy: string }
const ApplicationForm: Form<ApplicationFormState, Options, ValidatedInput, AdditionalProps> = {
const ApplicationForm: Form<State, Options, ValidatedInput, AdditionalProps> = {
initialState: {
...createCompoundInitialState(SubForms),
activeStep: 0,
stepPersonalData: PersonalDataForm.initialState,
stepCardType: StepCardTypeForm.initialState,
stepRequirements: StepRequirementsForm.initialState,
stepSend: StepSendForm.initialState,
},
getArrayBufferKeys: state => [
...PersonalDataForm.getArrayBufferKeys(state.stepPersonalData),
...StepCardTypeForm.getArrayBufferKeys(state.stepCardType),
...StepRequirementsForm.getArrayBufferKeys(state.stepRequirements),
...StepSendForm.getArrayBufferKeys(state.stepSend),
],
getValidatedInput: state => {
const personalData = PersonalDataForm.getValidatedInput(state.stepPersonalData)
const stepCardType = StepCardTypeForm.getValidatedInput(state.stepCardType)
getArrayBufferKeys: createCompoundGetArrayBufferKeys(SubForms),
validate: state => {
const personalData = PersonalDataForm.validate(state.stepPersonalData)
const stepCardType = StepCardTypeForm.validate(state.stepCardType)
if (personalData.type === 'error' || stepCardType.type === 'error') return { type: 'error' }

const stepRequirements = StepRequirementsForm.getValidatedInput(state.stepRequirements, {
const stepRequirements = StepRequirementsForm.validate(state.stepRequirements, {
cardType: stepCardType.value.cardType,
})
const stepSend = StepSendForm.getValidatedInput(state.stepSend)
const stepSend = StepSendForm.validate(state.stepSend)
if (stepRequirements.type === 'error' || stepSend.type === 'error') return { type: 'error' }

return {
Expand Down Expand Up @@ -78,7 +72,7 @@ const ApplicationForm: Form<ApplicationFormState, Options, ValidatedInput, Addit
state,
setState,
'stepRequirements',
{ cardType: state.stepCardType.cardType },
{ cardType: state.stepCardType.cardType.selectedValue },
{}
)
const sendStep = useFormAsStep('Antrag Senden', StepSendForm, state, setState, 'stepSend', {}, { privacyPolicy })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { BlueCardEntitlementInput, BlueCardEntitlementType } from '../../../gene
import { useUpdateStateCallback } from '../../useUpdateStateCallback'
import { Form } from '../../FormType'
import SwitchComponent from '../SwitchComponent'
import WorkAtOrganizationsEntitlementForm, {
WorkAtOrganizationsEntitlementFormState,
} from './WorkAtOrganizationsEntitlementForm'
import RadioGroupForm from '../primitive-inputs/RadioGroupForm'
import radioGroupForm, { RadioGroupFormState } from '../primitive-inputs/RadioGroupForm'
import JuleicaEntitlementForm, { JuleicaEntitlementFormState } from './JuleicaEntitlementForm'
import WorkAtDepartmentEntitlementForm, {
WorkAtDepartmentEntitlementFormState,
} from './WorkAtDepartmentEntitlementForm'
import MilitaryReserveEntitlementForm, { MilitaryReserveEntitlementFormState } from './MilitaryReserveEntitlementForm'
import VolunteerServiceEntitlementForm, {
VolunteerServiceEntitlementFormState,
} from './VolunteerServiceEntitlementForm'
import WorkAtOrganizationsEntitlementForm from './WorkAtOrganizationsEntitlementForm'
import { createRadioGroupForm } from '../primitive-inputs/RadioGroupForm'
import JuleicaEntitlementForm from './JuleicaEntitlementForm'
import WorkAtDepartmentEntitlementForm from './WorkAtDepartmentEntitlementForm'
import MilitaryReserveEntitlementForm from './MilitaryReserveEntitlementForm'
import VolunteerServiceEntitlementForm from './VolunteerServiceEntitlementForm'
import {
CompoundState,
createCompoundGetArrayBufferKeys,
createCompoundInitialState,
createSwitchValidate,
} from '../../compoundFormUtils'

const EntitlementTypeRadioGroupForm = createRadioGroupForm<BlueCardEntitlementType>()
const entitlementTypeOptions: { labelByValue: { [K in BlueCardEntitlementType]: string } } = {
labelByValue: {
[BlueCardEntitlementType.WorkAtOrganizations]:
Expand All @@ -30,104 +30,32 @@ const entitlementTypeOptions: { labelByValue: { [K in BlueCardEntitlementType]:
},
}

export type BlueCardEntitlementFormState = {
entitlementType: RadioGroupFormState
workAtOrganizationsEntitlement: WorkAtOrganizationsEntitlementFormState
juleicaEntitlement: JuleicaEntitlementFormState
workAtDepartmentEntitlement: WorkAtDepartmentEntitlementFormState
militaryReserveEntitlement: MilitaryReserveEntitlementFormState
volunteerServiceEntitlement: VolunteerServiceEntitlementFormState
const SubForms = {
entitlementType: EntitlementTypeRadioGroupForm,
workAtOrganizationsEntitlement: WorkAtOrganizationsEntitlementForm,
juleicaEntitlement: JuleicaEntitlementForm,
workAtDepartmentEntitlement: WorkAtDepartmentEntitlementForm,
militaryReserveEntitlement: MilitaryReserveEntitlementForm,
volunteerServiceEntitlement: VolunteerServiceEntitlementForm,
}

type State = CompoundState<typeof SubForms>
type ValidatedInput = BlueCardEntitlementInput
type Options = {}
type AdditionalProps = {}
const BlueCardEntitlementForm: Form<BlueCardEntitlementFormState, Options, ValidatedInput, AdditionalProps> = {
initialState: {
entitlementType: RadioGroupForm.initialState,
workAtOrganizationsEntitlement: WorkAtOrganizationsEntitlementForm.initialState,
juleicaEntitlement: JuleicaEntitlementForm.initialState,
workAtDepartmentEntitlement: WorkAtDepartmentEntitlementForm.initialState,
militaryReserveEntitlement: MilitaryReserveEntitlementForm.initialState,
volunteerServiceEntitlement: VolunteerServiceEntitlementForm.initialState,
},
getArrayBufferKeys: state => [
...RadioGroupForm.getArrayBufferKeys(state.entitlementType),
...WorkAtOrganizationsEntitlementForm.getArrayBufferKeys(state.workAtOrganizationsEntitlement),
...JuleicaEntitlementForm.getArrayBufferKeys(state.juleicaEntitlement),
...WorkAtDepartmentEntitlementForm.getArrayBufferKeys(state.workAtDepartmentEntitlement),
...MilitaryReserveEntitlementForm.getArrayBufferKeys(state.militaryReserveEntitlement),
...VolunteerServiceEntitlementForm.getArrayBufferKeys(state.volunteerServiceEntitlement),
],
getValidatedInput: state => {
const entitlementTypeResult = radioGroupForm.getValidatedInput(state.entitlementType, entitlementTypeOptions)
if (entitlementTypeResult.type === 'error') return { type: 'error' }
const entitlementType = entitlementTypeResult.value.value as BlueCardEntitlementType
switch (entitlementType) {
case BlueCardEntitlementType.WorkAtOrganizations: {
const workAtOrganizationsEntitlement = WorkAtOrganizationsEntitlementForm.getValidatedInput(
state.workAtOrganizationsEntitlement
)
if (workAtOrganizationsEntitlement.type === 'error') return { type: 'error' }
return {
type: 'valid',
value: {
entitlementType,
workAtOrganizationsEntitlement: workAtOrganizationsEntitlement.value,
},
}
}
case BlueCardEntitlementType.Juleica: {
const juleicaEntitlement = JuleicaEntitlementForm.getValidatedInput(state.juleicaEntitlement)
if (juleicaEntitlement.type === 'error') return { type: 'error' }
return {
type: 'valid',
value: {
entitlementType,
juleicaEntitlement: juleicaEntitlement.value,
},
}
}
case BlueCardEntitlementType.WorkAtDepartment:
const workAtDepartmentEntitlement = WorkAtDepartmentEntitlementForm.getValidatedInput(
state.workAtDepartmentEntitlement
)
if (workAtDepartmentEntitlement.type === 'error') return { type: 'error' }
return {
type: 'valid',
value: {
entitlementType,
workAtDepartmentEntitlement: workAtDepartmentEntitlement.value,
},
}
case BlueCardEntitlementType.MilitaryReserve:
const militaryReserveEntitlement = MilitaryReserveEntitlementForm.getValidatedInput(
state.militaryReserveEntitlement
)
if (militaryReserveEntitlement.type === 'error') return { type: 'error' }
return {
type: 'valid',
value: {
entitlementType,
militaryReserveEntitlement: militaryReserveEntitlement.value,
},
}
case BlueCardEntitlementType.VolunteerService:
const volunteerServiceEntitlement = VolunteerServiceEntitlementForm.getValidatedInput(
state.volunteerServiceEntitlement
)
if (volunteerServiceEntitlement.type === 'error') return { type: 'error' }
return {
type: 'valid',
value: {
entitlementType,
volunteerServiceEntitlement: volunteerServiceEntitlement.value,
},
}
}
},
const BlueCardEntitlementForm: Form<State, Options, ValidatedInput, AdditionalProps> = {
initialState: createCompoundInitialState(SubForms),
getArrayBufferKeys: createCompoundGetArrayBufferKeys(SubForms),
validate: createSwitchValidate(SubForms, { entitlementType: entitlementTypeOptions }, 'entitlementType', {
JULEICA: 'juleicaEntitlement',
MILITARY_RESERVE: 'militaryReserveEntitlement',
VOLUNTEER_SERVICE: 'volunteerServiceEntitlement',
WORK_AT_DEPARTMENT: 'workAtDepartmentEntitlement',
WORK_AT_ORGANIZATIONS: 'workAtOrganizationsEntitlement',
}),
Component: ({ state, setState }) => (
<>
<RadioGroupForm.Component
<EntitlementTypeRadioGroupForm.Component
state={state.entitlementType}
divideItems
title='Ich erfülle folgende Voraussetzung für die Beantragung einer blauen Ehrenamtskarte:'
Expand Down
Loading