Skip to content

Commit

Permalink
Add Formik Example to React Sample App (#5414)
Browse files Browse the repository at this point in the history
  • Loading branch information
deleonio authored Oct 17, 2023
2 parents a4d7be4 + 2bab3b5 commit a775b0a
Show file tree
Hide file tree
Showing 23 changed files with 1,096 additions and 60 deletions.
2 changes: 1 addition & 1 deletion packages/components/src/dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</head>
<body class="demo" data-theme="demo" v-scope="{ on: console.log }">
<main class="container" style="padding-top: 100px">
<kol-abbr _title="Abkürzung" _tooltip-align="right" class="hydrated" style="">z.B.</kol-abbr>
<kol-select _label="q test" _options="[ { 'value': 'a', 'label': 'a' }, { 'value': 'b', 'label': 'b' } ]" _value="b"></kol-select>
</main>
</body>
</html>
3 changes: 2 additions & 1 deletion packages/samples/react/.eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
**/assets/**
**/assets/**
**/complex-form/**
2 changes: 1 addition & 1 deletion packages/samples/react/.ts-prunerc.js
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',
};
4 changes: 3 additions & 1 deletion packages/samples/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"cpy-cli": "5.0.0",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.33.2",
"formik": "2.4.5",
"nightwatch-axe-verbose": "2.2.2",
"npm-run-all": "4.1.5",
"react": "18.2.0",
Expand All @@ -43,7 +44,8 @@
"rimraf": "3.0.2",
"ts-prune": "0.10.3",
"typescript": "5.2.2",
"world_countries_lists": "2.8.2"
"world_countries_lists": "2.8.2",
"yup": "1.3.1"
},
"files": [
".eslintignore",
Expand Down
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>
);
}
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>
);
}
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>
);
}
Loading

0 comments on commit a775b0a

Please sign in to comment.