Last active
September 1, 2021 13:28
-
-
Save jesster2k10/fc61c4f7defc15f3271ce88b648a29ce to your computer and use it in GitHub Desktop.
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
const routes: any[] = [ | |
{ | |
path: 'account', | |
component: <AccountStep />, | |
requiredFields: ['email', 'first_name', 'last_name', 'password'] | |
}, | |
{ | |
path: 'profile', | |
component: <ProfileStep />, | |
requiredFields: ['user.occupation'] | |
}, | |
{ | |
path: 'preferences', | |
component: <TenantPreferencesStep /> | |
} | |
] | |
class SampleComponent extends Component { | |
private cachedPage = Number(localStorage.getItem('tenant_registration_page')) | |
public state = { | |
page: this.cachedPage || 0, | |
loading: false, | |
errors: [] | |
} | |
private onNext = async ( | |
page: number, | |
values: TenantRegistrationFormValues | |
) => { | |
if (page === 1) { | |
this.checkForEmail(get(values, 'email'), () => this.goToPage(page)) | |
} else { | |
this.goToPage(page) | |
} | |
} | |
private checkForEmail = async (identity: string, callback: () => void) => { | |
this.setState({ loading: true, errors: [] }) | |
try { | |
const { exists = false } = await api.get('identities', { | |
params: { | |
identity | |
} | |
}) | |
if (!exists) { | |
this.setState( | |
{ | |
loading: false | |
}, | |
callback | |
) | |
} else { | |
this.setState({ | |
errors: ['This email has been taken'] | |
}) | |
} | |
} catch (_) { | |
this.setState({ | |
errors: ['This email has been taken'] | |
}) | |
} finally { | |
this.setState({ loading: false }) | |
} | |
} | |
private goToPage = (page: number) => { | |
this.setState( | |
{ | |
page | |
}, | |
() => { | |
localStorage.setItem('tenant_registration_page', String(page)) | |
} | |
) | |
} | |
public render() { | |
const { page, loading, errors } = this.state | |
return ( | |
<MultiStep | |
title="Tenant Registration" | |
persisted | |
steps={routes} | |
page={page} | |
PageComponent={RegistrationPage} | |
onChangePage={this.onNext} | |
validationSchema={commonRegistrationSchema} | |
initialValues={tenantInitialValues} | |
loading={loading} | |
errors={errors} | |
path="/tenants/sign_up" | |
onSubmit={console.log} | |
/> | |
) | |
} | |
} |
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
/* eslint-disable react/jsx-wrap-multilines */ | |
import React, { Component, Fragment } from 'react' | |
import { Alert, Icon } from 'antd' | |
import { PrimaryButton } from 'components/buttons' | |
import Helmet from 'react-helmet' | |
import styled from 'styled-components' | |
import { Formik, FormikConfig, FormikProps } from 'formik' | |
import { Page, PageProps } from 'components/layout' | |
import { RegistrationPageProps } from 'components/registrations/RegistrationPage' | |
import { Redirect, withRouter, RouteComponentProps, Route } from 'react-router' | |
import { FiArrowLeft, FiArrowRight, FiCheck } from 'react-icons/fi' | |
import { Persist } from 'formik-persist' | |
import get from 'lodash/get' | |
import styles from './multistep.module.scss' | |
export interface Step<T> { | |
path: string | |
title?: string | |
component: React.ReactNode | |
requiredFields?: (keyof Partial<T>)[] | |
} | |
interface MultiStepProps<T> { | |
onChangePage: (page: number, value?: T | any) => void | |
onSubmit: (values: T) => void | |
PageComponent?: any | |
steps: Step<T>[] | |
page: number | |
title: string | |
path: string | |
errors?: string[] | |
persisted?: boolean | |
loading?: boolean | |
loadingDisplay?: 'cover-screen' | 'discrete' | |
} | |
type MultiStepComponentProps<T> = MultiStepProps<T> & | |
Omit<Partial<FormikConfig<T>>, 'render'> & | |
RouteComponentProps & | |
Partial<Omit<PageProps, 'children'>> & | |
Partial<Omit<RegistrationPageProps, 'children'>> & { | |
validationSchema?: any | |
} | |
class MultiStepComponent<T = any> extends Component< | |
MultiStepComponentProps<T> | |
> { | |
public static defaultProps = { | |
PageComponent: Page, | |
steps: [], | |
persisted: false | |
} | |
public componentDidMount = () => { | |
const { history } = this.props | |
this.goToPage() | |
history.listen(location => { | |
if (location.pathname !== this.currentPathname) { | |
const { onChangePage, steps, path } = this.props | |
const route = location.pathname.replace(`${path}/`, '') | |
const page = steps.findIndex(item => item.path === route) | |
onChangePage(page) | |
} | |
}) | |
} | |
public componentDidUpdate = ({ page: oldPage }: any) => { | |
const { page } = this.props | |
if (page !== oldPage) { | |
this.goToPage() | |
} | |
} | |
private get isLastPage() { | |
const { page, steps } = this.props | |
return page === steps.length - 1 | |
} | |
private get currentPathname() { | |
const { page, steps, path } = this.props | |
const step = steps[page] || {} | |
return `${path}/${step.path}` | |
} | |
private goToPage = () => { | |
const { history, steps, page } = this.props | |
const { path } = this.props | |
const step = steps[page] | |
if (step) { | |
history.push(`${path}/${step.path}`) | |
} | |
} | |
private isValid = ({ errors, values }: any) => { | |
const { page, steps } = this.props | |
const step = steps[page] | |
if (step && step.requiredFields) { | |
return step.requiredFields.every( | |
field => !get(errors, field) && get(values, field) | |
) | |
} | |
return true | |
} | |
private onSubmit = (values: T) => { | |
const { onSubmit, onChangePage, page } = this.props | |
if (!this.isLastPage) { | |
onChangePage(page + 1, values) | |
} else { | |
onSubmit(values) | |
} | |
} | |
private onBack = () => { | |
const { page, onChangePage } = this.props | |
if (page > 0) { | |
onChangePage(page - 1) | |
} | |
} | |
private renderStep = (step: Step<T>, formikProps: FormikProps<T>) => { | |
const { | |
PageComponent, | |
title, | |
loading, | |
loadingDisplay = 'discrete', | |
page, | |
errors, | |
...props | |
} = this.props | |
const disabled = !this.isValid(formikProps) | |
return ( | |
<PageComponent {...(props as any)} step={page}> | |
<PageContainer> | |
{title && step.title && ( | |
<Helmet> | |
<title>{`${title} - ${step.title}`}</title> | |
</Helmet> | |
)} | |
<Content> | |
{errors && errors.length > 0 && ( | |
<AlertContainer> | |
<Alert | |
message={ | |
this.isLastPage | |
? 'Failed to submit form' | |
: 'Failed to continue' | |
} | |
description={ | |
<ul> | |
{errors.map(error => ( | |
<li key={error}>{error}</li> | |
))} | |
</ul> | |
} | |
type="error" | |
closable | |
/> | |
</AlertContainer> | |
)} | |
{React.cloneElement(step.component as any, formikProps)} | |
</Content> | |
<Footer> | |
{page > 0 && ( | |
<PrimaryButton | |
onClick={this.onBack} | |
className={styles.back} | |
disabled={loading} | |
> | |
<FiArrowLeft /> | |
</PrimaryButton> | |
)} | |
<FormButton | |
element="button" | |
type="submit" | |
loading={loading} | |
onClick={formikProps.handleSubmit} | |
disabled={disabled} | |
> | |
{!loading ? ( | |
<> | |
{this.isLastPage ? 'Submit' : 'Next'} | |
{this.isLastPage ? <FiCheck /> : <FiArrowRight />} | |
</> | |
) : ( | |
loadingDisplay === 'discrete' && <Icon type="loading" /> | |
)} | |
</FormButton> | |
</Footer> | |
</PageContainer> | |
</PageComponent> | |
) | |
} | |
public render() { | |
const { steps, path: route, persisted, ...props } = this.props | |
return ( | |
<div> | |
<Formik<T> {...(props as any)} onSubmit={this.onSubmit}> | |
{formikProps => ( | |
<div> | |
{persisted && <Persist name={route} />} | |
{steps.map((step, index) => ( | |
<Fragment key={step.path}> | |
{index === 0 && ( | |
<Redirect from={route} exact to={`${route}/${step.path}`} /> | |
)} | |
<Route | |
path={`${route}/${step.path}`} | |
exact | |
render={routeProps => | |
this.renderStep(step, { ...formikProps, ...routeProps }) | |
} | |
/> | |
</Fragment> | |
))} | |
</div> | |
)} | |
</Formik> | |
</div> | |
) | |
} | |
} | |
export const MultiStep = withRouter(MultiStepComponent) | |
const PageContainer = styled.div` | |
width: 100%; | |
display: flex; | |
flex-direction: column; | |
` | |
const Content = styled.div` | |
max-width: 980px; | |
` | |
const Footer = styled.div` | |
display: flex; | |
flex-direction: row; | |
bottom: 1em; | |
background-color: white; | |
` | |
const FormButton = styled(PrimaryButton)` | |
svg { | |
margin-left: 0.5em; | |
} | |
i { | |
svg { | |
margin: 0; | |
} | |
} | |
` | |
const AlertContainer = styled.div` | |
margin-bottom: 1em; | |
ul { | |
margin-top: 1em; | |
margin: 0; | |
padding: 0; | |
margin-left: 1em; | |
} | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment