Skip to content

Instantly share code, notes, and snippets.

@jesster2k10
Last active September 1, 2021 13:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jesster2k10/fc61c4f7defc15f3271ce88b648a29ce to your computer and use it in GitHub Desktop.
Save jesster2k10/fc61c4f7defc15f3271ce88b648a29ce to your computer and use it in GitHub Desktop.
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}
/>
)
}
}
/* 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