Last active
November 16, 2021 20:47
-
-
Save bartoszgolebiowski/ed7ce444a0fe1c2acf13a7b2dcec7463 to your computer and use it in GitHub Desktop.
Formik multistep form + validation
This file contains hidden or 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
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
https://codesandbox.io/s/practical-payne-0fkkp?file=/src/App.tsx