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

Add Formik Example to React Sample App #5414

Merged
merged 23 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 84 additions & 70 deletions packages/components/src/components.d.ts

Large diffs are not rendered by default.

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>
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,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',
Copy link
Member Author

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.

},
{
_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);
Copy link
Member Author

Choose a reason for hiding this comment

The 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 checked, auch wenn der value mit keiner der Optionen übereinstimmt (z.B. leerer value). Das ist vermutlich ein Bug, der in KoliBri behoben werden sollte, weil so keine "leere Auswahl" von Radio-Buttons möglich ist. Getestet mit '', undefined und null.

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 || ''}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Der Workaround ( || '') ist unbedingt notwendig, weil wenn der error von einem String zurück zu undefined geändert wird, dies nicht im internen State übernommen wird. Der Fehler bleibt also immer sichtbar.
Ich habe hier einiges ausprobiert, aber keine Lösung gefunden, die nicht gleichzeitig ein Breaking Change wäre. Ich würde vorschlagen, dass wir uns das Thema einmal zusammen anschauen.

Relevante Stelle: packages/components/src/utils/prop.validators.ts:102

_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>
);
}
Loading
Loading