-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Formik Example to React Sample App (#5414)
- Loading branch information
Showing
23 changed files
with
1,096 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
**/assets/** | ||
**/assets/** | ||
**/complex-form/** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// https://github.com/nadeesha/ts-prune | ||
https: module.exports = { | ||
ignore: 'node_modules', | ||
ignore: 'node_modules|complex-form', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
packages/samples/react/src/scenarios/appointment-form/AppointmentForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React, { useEffect, 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 [selectedTab, setSelectedTab] = useState(activeFormSection); | ||
|
||
const validationSchema = Yup.object().shape({ | ||
...(activeFormSection === FormSection.DISTRICT ? districtSchema : {}), | ||
...(activeFormSection === FormSection.AVAILABLE_APPOINTMENTS ? availableAppointmentsSchema : {}), | ||
...(activeFormSection === FormSection.PERSONAL_INFORMATION ? personalInformationSchema : {}), | ||
}); | ||
|
||
useEffect(() => { | ||
setSelectedTab(activeFormSection); | ||
}, [activeFormSection]); | ||
|
||
const handleSubmit = async (_values: FormValues, formik: FormikHelpers<FormValues>) => { | ||
console.log(_values, formik); | ||
const currentSectionIndex = formSectionSequence.indexOf(activeFormSection); | ||
const nextSection = formSectionSequence[currentSectionIndex + 1]; | ||
if (nextSection !== undefined) { | ||
await formik.setTouched({}); | ||
setTimeout(() => setActiveFormSection(nextSection), 1000); | ||
} | ||
}; | ||
|
||
return ( | ||
<Formik<FormValues> initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}> | ||
<KolTabs | ||
_tabs={[ | ||
{ | ||
_label: '1. Einwohnermeldeamt wählen', | ||
}, | ||
{ | ||
_label: '2. Freie Termine', | ||
_disabled: activeFormSection < FormSection.AVAILABLE_APPOINTMENTS, | ||
}, | ||
{ | ||
_label: '3. Persönliche Daten', | ||
_disabled: activeFormSection < FormSection.PERSONAL_INFORMATION, | ||
}, | ||
{ | ||
_label: 'Zusammenfassung', | ||
_disabled: activeFormSection < FormSection.SUMMARY, | ||
}, | ||
]} | ||
_label="Formular-Navigation" | ||
_selected={selectedTab} | ||
_on={{ onSelect: (_event, selectedTab) => setActiveFormSection(selectedTab) }} | ||
> | ||
<div> | ||
<DistrictForm /> | ||
</div> | ||
<div> | ||
<AvailableAppointmentsForm /> | ||
</div> | ||
<div> | ||
<PersonalInformationForm /> | ||
</div> | ||
<div> | ||
<Summary /> | ||
</div> | ||
</KolTabs> | ||
</Formik> | ||
); | ||
} |
118 changes: 118 additions & 0 deletions
118
packages/samples/react/src/scenarios/appointment-form/AvailableAppointmentsForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
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 || ''} | ||
_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> | ||
); | ||
} |
80 changes: 80 additions & 0 deletions
80
packages/samples/react/src/scenarios/appointment-form/DistrictForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
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) => { | ||
// Select und Radio setzen den Wert immer initial. | ||
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> | ||
); | ||
} |
Oops, something went wrong.