-
Notifications
You must be signed in to change notification settings - Fork 35
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
Add Formik Example to React Sample App #5414
Changes from 18 commits
d50c818
f2b4242
df51f3a
9af7752
9d95d44
2a55093
26de4b3
9141547
deb71fe
e11087f
fa240bd
b419e54
d3ef5ac
4d247b2
d7c71fd
d70003e
b543bcf
17fa9e6
f8c2cc7
86a37fb
aac1073
1da16fd
2bab3b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { useState } from 'react'; | ||
import { KolTabs } from '@public-ui/react'; | ||
import { DistrictForm } from './DistrictForm'; | ||
import { Summary } from './Summary'; | ||
import { PersonalInformationForm } from './PersonalInformationForm'; | ||
import { Formik, FormikHelpers } from 'formik'; | ||
import * as Yup from 'yup'; | ||
import { AvailableAppointmentsForm } from './AvailableAppointmentsForm'; | ||
import { Iso8601 } from '@public-ui/components'; | ||
import { checkAppointmentAvailability } from './appointmentService'; | ||
|
||
export interface FormProps {} | ||
export interface FormValues { | ||
district: string; | ||
date: Iso8601; | ||
time: Iso8601; | ||
salutation: string; | ||
name: string; | ||
company: string; | ||
email: string; | ||
phone: string; | ||
} | ||
|
||
enum FormSection { | ||
DISTRICT, | ||
AVAILABLE_APPOINTMENTS, | ||
PERSONAL_INFORMATION, | ||
SUMMARY, | ||
} | ||
|
||
const formSectionSequence = [FormSection.DISTRICT, FormSection.AVAILABLE_APPOINTMENTS, FormSection.PERSONAL_INFORMATION, FormSection.SUMMARY] as const; | ||
|
||
const initialValues: FormValues = { | ||
district: '', | ||
date: '' as Iso8601, | ||
time: '' as Iso8601, | ||
salutation: '', | ||
name: '', | ||
company: '', | ||
email: '', | ||
phone: '', | ||
}; | ||
|
||
const districtSchema = { | ||
district: Yup.string().required('Bitte Stadtteil wählen.'), | ||
}; | ||
const personalInformationSchema = { | ||
salutation: Yup.string().required('Bitte Anrede auswählen.'), | ||
name: Yup.string().required('Bitte Name eingeben.'), | ||
company: Yup.string().when('salutation', { | ||
is: (salutation: string) => salutation === 'Firma', | ||
then: (schema) => schema.required('Bitte Firmenname angeben.'), | ||
}), | ||
email: Yup.string().required('Bitte E-Mail-Adresse eingeben.'), | ||
}; | ||
const availableAppointmentsSchema = { | ||
date: Yup.string().required('Bitte Datum eingeben.'), | ||
time: Yup.string().when('date', { | ||
is: (date: string) => Boolean(date), // only validate time when date is already set | ||
then: (schema) => schema.test('checkTimeAvailability', 'Termin leider nicht mehr verfügbar.', checkAppointmentAvailability), | ||
}), | ||
}; | ||
|
||
export function AppointmentForm() { | ||
const [activeFormSection, setActiveFormSection] = useState(FormSection.DISTRICT); | ||
|
||
const validationSchema = Yup.object().shape({ | ||
...(activeFormSection === FormSection.DISTRICT ? districtSchema : {}), | ||
...(activeFormSection === FormSection.AVAILABLE_APPOINTMENTS ? availableAppointmentsSchema : {}), | ||
...(activeFormSection === FormSection.PERSONAL_INFORMATION ? personalInformationSchema : {}), | ||
}); | ||
|
||
const handleSubmit = async (_values: FormValues, formik: FormikHelpers<FormValues>) => { | ||
const currentSectionIndex = formSectionSequence.indexOf(activeFormSection); | ||
const nextSection = formSectionSequence[currentSectionIndex + 1]; | ||
if (nextSection !== undefined) { | ||
await formik.setTouched({}); | ||
setActiveFormSection(nextSection); | ||
} | ||
}; | ||
|
||
return ( | ||
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}> | ||
<KolTabs | ||
_tabs={[ | ||
{ | ||
_label: '1. Einwohnermeldeamt wählen', | ||
}, | ||
{ | ||
_label: '2. Freie Termine', | ||
}, | ||
{ | ||
_label: '3. Persönliche Daten', | ||
}, | ||
{ | ||
_label: 'Zusammenfassung', | ||
}, | ||
]} | ||
_label="Formular-Navigation" | ||
_selected={activeFormSection} | ||
_on={{ onSelect: (_event, selectedTab) => setActiveFormSection(selectedTab) }} | ||
> | ||
<div> | ||
<DistrictForm /> | ||
</div> | ||
<div> | ||
<AvailableAppointmentsForm /> | ||
</div> | ||
<div> | ||
<PersonalInformationForm /> | ||
</div> | ||
<div> | ||
<Summary /> | ||
</div> | ||
</KolTabs> | ||
</Formik> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import { KolButton, KolForm, KolHeading, KolInputDate, KolInputRadio, KolSpin } from '@public-ui/react'; | ||
import { FormValues } from './AppointmentForm'; | ||
import { ErrorList } from './ErrorList'; | ||
import { Field, FieldProps, useFormikContext } from 'formik'; | ||
import { fetchAvailableTimes } from './appointmentService'; | ||
import { Option } from '@public-ui/components/src'; | ||
|
||
export function AvailableAppointmentsForm() { | ||
const form = useFormikContext<FormValues>(); | ||
|
||
const [sectionSubmitted, setSectionSubmitted] = useState(false); | ||
const [availableTimes, setAvailableTimes] = useState<Option<string>[] | null>(null); | ||
|
||
useEffect(() => { | ||
let ignoreResponse = false; | ||
setAvailableTimes(null); | ||
|
||
if (form.values.date) { | ||
fetchAvailableTimes().then( | ||
(times) => { | ||
if (!ignoreResponse) { | ||
setAvailableTimes(times); | ||
void form.setFieldValue('time', times[0].value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hier war ich praktisch gezwungen, einen Wert vorauszuwählen: KoliBri markiert per default den ersten radio button als |
||
void form.setFieldTouched('time'); | ||
} | ||
}, | ||
() => {}, | ||
); // ignore errors | ||
} | ||
return () => { | ||
ignoreResponse = true; | ||
}; | ||
}, [form.values.date]); | ||
|
||
return ( | ||
<div className="p-2"> | ||
<KolHeading _level={2} _label="Wählen Sie einen Termin aus"></KolHeading> | ||
|
||
{sectionSubmitted && Object.keys(form.errors).length ? ( | ||
<div className="mt-2"> | ||
<ErrorList errors={form.errors} /> | ||
</div> | ||
) : null} | ||
|
||
<KolForm | ||
_on={{ | ||
onSubmit: () => { | ||
void form.submitForm(); | ||
setSectionSubmitted(true); | ||
}, | ||
}} | ||
> | ||
<Field name="date"> | ||
{({ field }: FieldProps<FormValues['date']>) => ( | ||
<KolInputDate | ||
id="field-date" | ||
_label="Datum" | ||
_value={field.value} | ||
_error={form.errors.date || ''} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Relevante Stelle: |
||
_touched={form.touched.date} | ||
_required | ||
_on={{ | ||
onChange: (event: Event, value: unknown): void => { | ||
if (event.target) { | ||
void form.setFieldValue('date', value, true); | ||
} | ||
}, | ||
onBlur: () => { | ||
void form.setFieldTouched('date', true); | ||
}, | ||
}} | ||
/> | ||
)} | ||
</Field> | ||
|
||
{form.values.date && ( | ||
<div className="grid gap-4 mt-4"> | ||
{availableTimes ? ( | ||
<> | ||
<Field name="time"> | ||
{({ field }: FieldProps<FormValues['time']>) => ( | ||
<KolInputRadio | ||
id="field-date" | ||
_label="Zeit" | ||
_orientation="horizontal" | ||
_options={availableTimes} | ||
_value={field.value} | ||
_error={form.errors.time || ''} | ||
_touched={form.touched.time} | ||
_required | ||
_on={{ | ||
onChange: (event: Event, value: unknown): void => { | ||
if (event.target) { | ||
void form.setFieldTouched('time', true); | ||
void form.setFieldValue('time', value, true); | ||
} | ||
}, | ||
}} | ||
/> | ||
)} | ||
</Field> | ||
<p> | ||
<em>Aus Testzwecken sind nur die Termine zu jeder halben Stunde verfügbar.</em> | ||
</p> | ||
</> | ||
) : ( | ||
<KolSpin _show className="block" aria-label="Termine werden geladen." _variant="cycle" /> | ||
)} | ||
</div> | ||
)} | ||
|
||
<KolButton _label="Weiter" _type="submit" className="mt-2" _disabled={form.isValidating} /> | ||
{form.values.date && form.isValidating ? <KolSpin _show aria-label="Termin wird geprüft." /> : ''} | ||
</KolForm> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import React, { useState } from 'react'; | ||
import { KolButton, KolForm, KolHeading, KolSelect } from '@public-ui/react'; | ||
import { Field, FieldProps, useFormikContext } from 'formik'; | ||
import { FormValues } from './AppointmentForm'; | ||
import { ErrorList } from './ErrorList'; | ||
|
||
const LOCATION_OPTIONS = [ | ||
{ | ||
value: 'Aplerbeck', | ||
label: 'Aplerbeck', | ||
}, | ||
{ | ||
value: 'Brackel', | ||
label: 'Brackel', | ||
}, | ||
{ | ||
value: 'Dorstfeld', | ||
label: 'Dorstfeld', | ||
}, | ||
{ | ||
value: 'Innenstadt Ost', | ||
label: 'Innenstadt Ost', | ||
}, | ||
{ | ||
value: 'Innenstadt West', | ||
label: 'Innenstadt West', | ||
}, | ||
]; | ||
|
||
export function DistrictForm() { | ||
const form = useFormikContext<FormValues>(); | ||
const [sectionSubmitted, setSectionSubmitted] = useState(false); | ||
|
||
return ( | ||
<div className="p-2"> | ||
<KolHeading _level={2} _label="Wählen Sie einen Stadtteil aus"></KolHeading> | ||
|
||
{sectionSubmitted && Object.keys(form.errors).length ? ( | ||
<div className="mt-2"> | ||
<ErrorList errors={form.errors} /> | ||
</div> | ||
) : null} | ||
|
||
<KolForm | ||
_on={{ | ||
onSubmit: () => { | ||
void form.submitForm(); | ||
setSectionSubmitted(true); | ||
}, | ||
}} | ||
> | ||
<Field name="district"> | ||
{({ field }: FieldProps<FormValues['district']>) => ( | ||
<KolSelect | ||
id="field-district" | ||
_label="Stadtteil" | ||
_options={[{ label: 'Bitte wählen…', value: '' }, ...LOCATION_OPTIONS]} | ||
_value={[field.value]} | ||
_error={form.errors.district || ''} | ||
_touched={form.touched.district} | ||
_required | ||
_on={{ | ||
onChange: (event, values: unknown) => { | ||
if (event.target) { | ||
const [value] = values as [FormValues['district']]; | ||
void form.setFieldTouched('district', true); | ||
void form.setFieldValue('district', value, true); | ||
} | ||
}, | ||
}} | ||
/> | ||
)} | ||
</Field> | ||
|
||
<KolButton _label="Weiter" _type="submit" className="mt-2" /> | ||
</KolForm> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import { KolAlert, KolLink } from '@public-ui/react'; | ||
|
||
export type ErrorListPropType = { | ||
errors: Record<string, string>; | ||
}; | ||
|
||
export function ErrorList({ errors }: ErrorListPropType) { | ||
const handleLinkClick = (event: Event) => { | ||
const href = (event.target as HTMLAnchorElement | undefined)?.href; | ||
if (href) { | ||
const hrefUrl = new URL(href); | ||
|
||
const targetElement = document.querySelector<HTMLElement>(hrefUrl.hash); | ||
if (targetElement && typeof targetElement.focus === 'function') { | ||
targetElement.focus(); | ||
} | ||
} | ||
}; | ||
|
||
return ( | ||
<KolAlert _type="error" _variant="msg"> | ||
Bitte korrigieren Sie folgende Fehler: | ||
<nav aria-label="Fehlerliste"> | ||
<ul> | ||
{Object.entries(errors).map(([field, error]) => ( | ||
<li key={field}> | ||
<KolLink _href={`#field-${field}`} _label={error} _on={{ onClick: handleLinkClick }} /> | ||
</li> | ||
))} | ||
</ul> | ||
</nav> | ||
</KolAlert> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ich würde hier gerne jeweils noch eine
_disabled
-Property hinzufügen, wenn die vorherige Sektion nicht valide ist. Mir ist bloß noch nicht klar, wie wir das mit Formik erreichen können, weil auf dieser Ebene keinen Zugriff auf die Form-Instanz bzw. die Values haben.Ich lasse den Punkt erst einmal offen und komme ggf. später darauf zurück.