Created
February 15, 2018 13:59
-
-
Save mrowa44/dbe22360f767384566b84e3149d88507 to your computer and use it in GitHub Desktop.
React final form wizard better example
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
import React from 'react'; | |
import PropTypes from 'prop-types'; | |
import moment from 'moment'; | |
import { connect } from 'react-redux'; | |
import * as actions from 'actions/cards'; | |
import { activateCardStep1Validate, activateCardStep2Validate } from 'utils/validate'; | |
import { API_BIRTHDATE_FORMAT } from 'utils/constants'; | |
import Modal, { ModalContent } from 'components/Modal'; | |
import Wizard from 'components/Wizard'; | |
import activateIcon from 'shared/icons/activate.svg'; | |
import styles from './ActivateCardModal.scss'; | |
import Step1 from './Step1'; | |
import Step2 from './Step2'; | |
class ActivateCardModal extends React.Component { | |
handleFormSubmit = ({ birthDate, ...values }) => { | |
const { activateCard, onClose } = this.props; | |
const birthDateFormatted = moment(birthDate).format(API_BIRTHDATE_FORMAT); | |
return activateCard({ | |
...values, | |
birthDate: birthDateFormatted, | |
}) | |
.then(onClose) | |
.catch(({ formError }) => formError); | |
} | |
render() { | |
const { onClose, isOpen } = this.props; | |
return ( | |
<Modal | |
label="Activate card" | |
onClose={onClose} | |
className={styles.modal} | |
isOpen={isOpen} | |
{...this.props} | |
> | |
<ModalContent title="Activate card" icon={activateIcon}> | |
<Wizard | |
onSubmit={this.handleFormSubmit} | |
className={styles.form} | |
submitText="Activate" | |
onClose={onClose} | |
> | |
<Wizard.Page validate={activateCardStep1Validate}> | |
<Step1 onClose={onClose} /> | |
</Wizard.Page> | |
<Wizard.Page validate={activateCardStep2Validate}> | |
<Step2 /> | |
</Wizard.Page> | |
</Wizard> | |
</ModalContent> | |
</Modal> | |
); | |
} | |
} | |
ActivateCardModal.propTypes = { | |
activateCard: PropTypes.func.isRequired, | |
isOpen: PropTypes.bool.isRequired, | |
onClose: PropTypes.func.isRequired, | |
}; | |
const mapDispatchToProps = { activateCard: actions.activateCard }; | |
export default connect(null, mapDispatchToProps)(ActivateCardModal); | |
export { ActivateCardModal as ActivateCardModalUnwrapped }; |
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
import React from 'react'; | |
import cx from 'classnames'; | |
import { Field } from 'react-final-form'; | |
import Input from 'components/Input'; | |
import Dropdown from 'components/Dropdown'; | |
import { GENDERS } from 'utils/constants'; | |
import activateStyles from './ActivateCardModal.scss'; | |
import styles from './Step1.scss'; | |
function Step1() { | |
return ( | |
<div className={styles.wrapper}> | |
<div className={activateStyles.formRow}> | |
<Field | |
name="company_name" | |
component={Input} | |
label="company-name" | |
title="Company name" | |
className={activateStyles.inputFullWidth} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
title="gender" | |
name="gender" | |
component={Dropdown} | |
options={GENDERS} | |
className={styles.genderDropdown} | |
placeholder="Select" | |
clearable={false} | |
/> | |
<Field | |
type="email" | |
label="email" | |
title="E-mail" | |
name="email" | |
component={Input} | |
className={cx(activateStyles.inputFullWidth, activateStyles.inputRight)} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<div className={styles.phoneWrapper}> | |
<div className={styles.phoneInputTitle}>Phone number</div> | |
<div className={styles.phoneWrapperInputs}> | |
<Field | |
label="countryCode" | |
name="countryCode" | |
component={Input} | |
className={styles.countryCode} | |
inputClass={styles.countryCodeInput} | |
/> | |
<Field | |
label="phone" | |
name="phone" | |
component={Input} | |
className={styles.phone} | |
inputClass={styles.phoneInput} | |
/> | |
</div> | |
</div> | |
<Field | |
type="date" | |
label="birth-date" | |
title="Date of birth" | |
name="birthDate" | |
component={Input} | |
className={cx(activateStyles.inputRight, styles.dateInput)} | |
inputClass={styles.dateInputInner} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
label="country" | |
title="Country/state" | |
name="country" | |
component={Input} | |
/> | |
<Field | |
label="city" | |
title="City" | |
name="city" | |
component={Input} | |
className={activateStyles.inputRight} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
label="street" | |
title="Street name" | |
name="streetName" | |
component={Input} | |
/> | |
<Field | |
label="zip-code" | |
name="zip" | |
title="ZIP/Postal code" | |
component={Input} | |
className={activateStyles.inputRight} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
label="id-number" | |
title="id number" | |
name="idNumber" | |
type="text" | |
component={Input} | |
/> | |
<Field | |
label="occupation" | |
title="Occupation" | |
name="occupation" | |
component={Input} | |
className={activateStyles.inputRight} | |
/> | |
</div> | |
</div> | |
); | |
} | |
export default Step1; |
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
import React from 'react'; | |
import cx from 'classnames'; | |
import { Field } from 'react-final-form'; | |
import Input from 'components/Input'; | |
import PINInput from 'components/PINInput'; | |
import MonthInput from 'components/MonthInput'; | |
import activateStyles from './ActivateCardModal.scss'; | |
import styles from './Step2.scss'; | |
function Step2() { | |
return ( | |
<div> | |
<div className={activateStyles.formRow}> | |
<Field | |
name="cardNumber" | |
component={Input} | |
label="card-number" | |
title="Card number" | |
type="text" | |
className={activateStyles.inputFullWidth} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
name="expiryDate" | |
component={MonthInput} | |
label="expiry-date" | |
title="Expiry date" | |
className={cx(activateStyles.inputFullWidth, styles.dateInput)} | |
inputClass={styles.dateInputInner} | |
/> | |
<Field | |
name="cvv" | |
component={Input} | |
label="cvv" | |
title="CVV" | |
type="text" | |
className={cx(styles.cvv, activateStyles.inputRight)} | |
inputClass={styles.cvvInner} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
justified | |
name="pin" | |
component={PINInput} | |
title="PIN Code" | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
justified | |
title="Confirm PIN Code" | |
name="cpin" | |
component={PINInput} | |
/> | |
</div> | |
<div className={activateStyles.formRow}> | |
<Field | |
component={Input} | |
name="password" | |
type="password" | |
label="password" | |
title="Password" | |
className={activateStyles.inputFullWidth} | |
/> | |
</div> | |
</div> | |
); | |
} | |
export default Step2; |
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
import React from 'react'; | |
import PropTypes from 'prop-types'; | |
import { Form } from 'react-final-form'; | |
import Button from 'components/Button'; | |
import spinnerIcon from 'shared/icons/spinner.svg'; | |
import checkMarkIcon from 'shared/icons/checkmark.svg'; | |
import angleArrowRight from 'shared/icons/angle-arrow-right.svg'; | |
import angleArrowLeft from 'shared/icons/angle-arrow-left.svg'; | |
import crossIcon from 'shared/icons/cross.svg'; | |
import styles from './Wizard.scss'; | |
class Wizard extends React.Component { | |
static Page = ({ children }) => children | |
state = { | |
currentPage: 0, | |
} | |
get isLastPage() { | |
const children = this.props.children; | |
const currentPage = this.state.currentPage; | |
return currentPage === React.Children.count(children) - 1; | |
} | |
get activePage() { | |
const currentPage = this.state.currentPage; | |
const children = this.props.children; | |
const activePage = React.Children.toArray(children)[currentPage]; | |
return activePage; | |
} | |
handleNext = (values) => { | |
const children = this.props.children; | |
this.setState(prevState => ({ | |
currentPage: Math.min(prevState.currentPage + 1, children.length - 1), | |
values, | |
})); | |
} | |
handlePrevious = () => { | |
this.setState(prevState => ({ | |
currentPage: Math.max(prevState.currentPage - 1, 0), | |
})); | |
} | |
// Both validate and handleSubmit switching are implemented | |
// here because Redux Final Form does not accept changes to those | |
// functions once the form has been defined. | |
validate = (values) => { | |
const { props } = this.activePage; | |
return props.validate ? props.validate(values) : {}; | |
} | |
handleSubmit = (values) => { | |
if (this.isLastPage) { | |
return this.props.onSubmit(values); | |
} | |
return this.handleNext(values); | |
} | |
render() { | |
const { currentPage, values: prevValues } = this.state; | |
return ( | |
<Form | |
initialValues={prevValues} | |
validate={this.validate} | |
onSubmit={this.handleSubmit} | |
> | |
{({ handleSubmit, submitting }) => ( | |
<form onSubmit={handleSubmit}> | |
{this.activePage} | |
<div> | |
<div className={styles.buttons}> | |
{currentPage > 0 && ( | |
<Button | |
primary | |
iconOnLeft | |
type="button" | |
onClick={this.handlePrevious} | |
icon={angleArrowLeft} | |
> | |
Previous | |
</Button> | |
)} | |
{this.isLastPage ? ( | |
<Button | |
primary | |
type="submit" | |
disabled={submitting} | |
icon={submitting ? spinnerIcon : checkMarkIcon} | |
> | |
{this.props.submitText} | |
</Button> | |
) : ( | |
<React.Fragment> | |
<Button danger icon={crossIcon} onClick={this.props.onClose}> | |
Cancel | |
</Button> | |
<Button primary type="submit" icon={angleArrowRight}> | |
Continue | |
</Button> | |
</React.Fragment> | |
)} | |
</div> | |
</div> | |
</form> | |
)} | |
</Form> | |
); | |
} | |
} | |
Wizard.propTypes = { | |
children: PropTypes.node.isRequired, | |
onClose: PropTypes.func.isRequired, | |
onSubmit: PropTypes.func.isRequired, | |
submitText: PropTypes.string, | |
}; | |
Wizard.defaultProps = { | |
submitText: 'Submit', | |
}; | |
export default Wizard; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any pro tip on how to pass state between the pages so we can have conditional logic in pages based on previous values (in previous pages)? @mrowa44