Skip to content

Instantly share code, notes, and snippets.

@bartoszgolebiowski
Last active November 16, 2021 20:47
Show Gist options
  • Save bartoszgolebiowski/ed7ce444a0fe1c2acf13a7b2dcec7463 to your computer and use it in GitHub Desktop.
Save bartoszgolebiowski/ed7ce444a0fe1c2acf13a7b2dcec7463 to your computer and use it in GitHub Desktop.
Formik multistep form + validation
import React from "react";
import {
Box,
Label,
Input,
InputProps,
Text,
Button,
Flex
} from "@theme-ui/components";
import { Theme, ThemeProvider } from "theme-ui";
import { Form, Formik, FormikConfig, FormikHelpers, useField } from "formik";
import * as Yup from "yup";
const theme: Theme = {
buttons: {
primary: {
color: "background",
bg: "primary",
"&:hover": {
bg: "text"
}
},
secondary: {
color: "background",
bg: "secondary"
}
}
};
type Values = {
firstName: string;
lastName: string;
code: string;
email: string;
phone: string;
cardNumber: string;
cardExpiry: string;
cardCVC: string;
};
const initialValues: Values = {
firstName: "",
lastName: "",
code: "",
email: "",
phone: "",
cardNumber: "",
cardExpiry: "",
cardCVC: ""
};
const Layout: React.FC = (props) => {
return <Box sx={{ maxWidth: "20rem", margin: "10rem auto" }} {...props} />;
};
const MultistepForm: React.FC<FormikConfig<Values>> = (props) => {
const [snap, setSnap] = React.useState<Values>(props.initialValues);
const [step, setStep] = React.useState(0);
const steps = React.Children.toArray(props.children) as React.ReactElement<
SingleStep
>[];
const currentStep = steps[step];
const nextPage = (value: Values) => {
setSnap(value);
setStep(step + 1);
};
const prevPage = (value: Values) => {
setSnap(value);
setStep(step - 1);
};
const hasPrev = step !== 0;
const hasNext = step !== steps.length - 1;
const handleSubmit = (value: Values, helper: FormikHelpers<Values>) => {
if (currentStep.props.onSubmit) {
currentStep.props.onSubmit(value, helper);
}
if (!hasNext) {
props.onSubmit(value, helper);
} else {
nextPage(value);
}
};
return (
<Formik
initialValues={snap}
onSubmit={handleSubmit}
validationSchema={currentStep.props.validationSchema}
>
{(formik) => (
<Form autoComplete="off">
<Text sx={{ fontSize: 5, fontWeight: "bold" }}>
{currentStep.props.label}
</Text>
{currentStep}
<Flex
pt={2}
sx={{
justifyContent: "flex-end",
"& > button": {
ml: 2
}
}}
>
{hasPrev && (
<Button variant="primary" onClick={() => prevPage(formik.values)}>
Previous
</Button>
)}
<Button type="submit" variant="secondary">
{hasNext ? "Next" : "Submit"}
</Button>
</Flex>
</Form>
)}
</Formik>
);
};
interface Field extends InputProps {
label: string;
name: string;
}
const FieldInput = ({ label, ...props }: Field) => {
const [field, meta] = useField(props);
return (
<Flex>
<Label
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start"
}}
>
{label}
<Input {...field} {...props} />
{meta.touched && meta.error ? (
<Text sx={{ color: "red" }}>{meta.error}</Text>
) : null}
</Label>
</Flex>
);
};
type SingleStep = {
validationSchema: FormikConfig<Values>["validationSchema"];
onSubmit: FormikConfig<Values>["onSubmit"];
label: string;
};
const SingleStep: React.FC<SingleStep> = (props) => {
return <>{props.children}</>;
};
const App = () => {
return (
<ThemeProvider theme={theme}>
<Layout>
<MultistepForm
initialValues={initialValues}
onSubmit={(value, helper) => {
alert(JSON.stringify(value, null, 2));
}}
>
<SingleStep
label="Person details"
onSubmit={(values, helper) =>
console.log("completed step number 1")
}
validationSchema={() => {
return Yup.object().shape({
firstName: Yup.string().required("Required"),
lastName: Yup.string().required("Required")
});
}}
>
<FieldInput
placeholder="First name"
label="First name"
name="firstName"
/>
<FieldInput
placeholder="Last name"
label="Last name"
name="lastName"
/>
</SingleStep>
<SingleStep
label="Location details"
onSubmit={(values, helper) =>
console.log("completed step number 2")
}
validationSchema={() => {
return Yup.object().shape({
code: Yup.string().required("Required"),
email: Yup.string()
.email("Invalid email address")
.required("Required"),
phone: Yup.string().required("Required")
});
}}
>
<FieldInput placeholder="Code" label="Code" name="code" />
<FieldInput
type="email"
placeholder="Email"
label="Email"
name="email"
/>
<FieldInput
type="tel"
placeholder="Phone"
label="Phone"
name="phone"
/>
</SingleStep>
<SingleStep
label="Card details"
onSubmit={(values, helper) =>
console.log("completed step number 3")
}
validationSchema={() => {
return Yup.object().shape({
cardCode: Yup.string().matches(/^4[0-9]{12}(?:[0-9]{3})?$/),
cardExpiry: Yup.string().matches(
/^(0[1-9]|1[0-2])\/?([0-9]{4}|[0-9]{2})$/
),
country: Yup.number().max(999).min(0)
});
}}
>
<FieldInput
type="tel"
placeholder="1234 1234 1234 1234"
label="Card Number"
name="cardNumber"
autoComplete="off"
/>
<FieldInput
type="text"
placeholder="01/01"
label="Card Expiry"
name="cardExpiry"
autoComplete="off"
/>
<FieldInput
type="password"
placeholder="CVC"
label="Card CVC"
name="cardCVC"
autoComplete="off"
/>
</SingleStep>
</MultistepForm>
</Layout>
</ThemeProvider>
);
};
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment