Skip to content

Instantly share code, notes, and snippets.

@mrowa44
Created February 15, 2018 13:59
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrowa44/dbe22360f767384566b84e3149d88507 to your computer and use it in GitHub Desktop.
Save mrowa44/dbe22360f767384566b84e3149d88507 to your computer and use it in GitHub Desktop.
React final form wizard better example
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 };
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;
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;
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;
@jetobe95
Copy link

Thaanskss i use it in react native correctly

@jleach
Copy link

jleach commented Jun 10, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment