This package was created to make step-by-step forms easier... I hope so
- Installation
- Usage
- WizardProps
- WizardBag
- Rendering
- Usage with redux container
- Creating steps track
- Usage with routers
- Todo
To install formik-wizard-form please follow next steps:
- Check
formik@^2.0.0
to be also installed as apeerDependency
- Run
npm i @panenco/formik-wizard-form
The main phylosopy of this wizard form is 'divide and conquer'. Therefore, step containers contain all needed properties for working. Idea is to keep all fields and step related data in the same place like: Title
, Validation
, onSubmit
, NoReturn
.
Here is the interface that represents WizardForm props. They are obviously extend FormikConfig
.
interface WizardStepContainer<Values> {
onSubmit?: (values?: Values, formikHelpers?: FormikHelpers<Values>) => void | Promise<any>;
NoReturn?: boolean;
Title?: React.ReactNode;
Validation?: any | (() => any);
}
interface WizardProps<Values> extends FormikConfig<Values> {
steps: (React.ComponentType<any> & WizardStepContainer<Values>)[];
children?: (
current: {
step: React.ReactNode,
} & WizardStepMeta,
) => React.ReactNode | React.ReactNode;
component?: React.ComponentType<any>;
navigator?: INavigatorConstructor;
initialStep?: number;
}
About INavigator
you can read in navigator README.md
There are couple different steps containers definitions examples below. For steps that send data independtly you can define class component method onSubmit
or in functional components use hook useImperativeHandler
in combination with forwardRef
for exposing methods.
import React from 'react';
import { Form } from 'formik';
import Field from '@panenco/formik-form-field';
import { PrimaryButton, TextInput, SelectInput } from '@panenco/pui';
export const PhilosophersStone = () => {
return (
<Form>
<Field name="firstName" placeholder="First name" component={TextInput} />
<Field name="lastName" placeholder="Last name" component={TextInput} />
<div>
<PrimaryButton type="submit">Next</PrimaryButton>
</div>
</Form>
);
};
const faculties = [
{ label: 'Gryffindor', value: 'Gryffindor' },
{ label: 'Hufflepuff', value: 'Hufflepuff' },
{ label: 'Ravenclaw', value: 'Ravenclaw' },
{ label: 'Slytherin', value: 'Slytherin' },
];
export const TheChamberOfSecrets = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
onSubmit: async (values, formikBag) => {
await sayToHat(values, formikBag);
},
}));
return (
<Form>
<Field name="faculty" placeholder="Faculty" component={SelectInput} options={faculties} />
<div>
<PrimaryButton type="submit">Make choice</PrimaryButton>
</div>
</Form>
);
});
TheChamberOfSecrets.Title = 'The Chamber of Secrets';
TheChamberOfSecrets.Validation = Yup.object().required('You need to choose faculty');
TheChamberOfSecrets.NoReturn = true;
export class ThePrisonerOfAzkaban extends React.Component {
static Title = 'The Prisoner of Azkaban';
onSubmit = (...args) => {
console.log(...args);
};
render() {
return (
<Form>
<Field name="email" placeholder="Email" component={TextInput} />
<div>
<PrimaryButton type="submit">Submit</PrimaryButton>
</div>
</Form>
);
}
}
WizardForm
is a wrapper around Formik
, so you need to pass props that are used to init formik form. For instance, initialValues
is still required, but onSubmit
- not (in case you have defined one as step container method, otherwise it will just do on step validation and do next()
).
There are couple props passed in MagicalContext
and to all steps as props
export interface WizardStepMeta {
stepIndex: number;
title: React.ReactNode;
noReturn: boolean;
touched: boolean;
}
export interface MagicalContext {
currentStepIndex: number;
stepsMeta: Array<WizardStepMeta>;
next: () => void;
back: () => void;
toStep: (step: number) => void;
toFirstStep: () => void;
toLastStep: () => void;
setWizardState: (state: any | Function) => void;
readonly wizardState: any;
}
There are couple ways to render current step.
import React from 'react';
import { WizardForm } from '@panenco/formik-wizard-form';
import * as HarryPotterAnd from './steps';
const App = () => {
return (
<WizardForm
initialValues={{
firstName: '',
lastName: '',
faculty: null,
email: '',
}}
steps={[
HarryPotterAnd.PhilosophersStone,
HarryPotterAnd.TheChamberOfSecrets,
HarryPotterAnd.ThePrisonerOfAzkaban,
]}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
children: (current: { step: React.ReactNode } & WizardStepMeta) => React.ReactNode
import React from 'react';
import { WizardForm } from '@panenco/formik-wizard-form';
import * as HarryPotterAnd from './steps';
const App = () => {
return (
<WizardForm
initialValues={{
firstName: '',
lastName: '',
faculty: null,
email: '',
}}
steps={[
HarryPotterAnd.PhilosophersStone,
HarryPotterAnd.TheChamberOfSecrets,
HarryPotterAnd.ThePrisonerOfAzkaban,
]}
>
{({ step }) => (
<div>
<h1>My Wizard Form</h1>
<div>{step}</div>
</div>
)}
</WizardForm>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
import React from 'react';
import { WizardForm, useWizardContext } from '@panenco/formik-wizard-form';
import { SecondaryButton } from '@panenco/pui';
import * as HarryPotterAnd from './steps';
const Layout = ({ step }) => {
const { toFirstStep } = useWizardContext();
const handleClick = () => toFirstStep();
return (
<div>
<h1>My Wizard Form</h1>
<div>{step}</div>
<div>
<SecondaryButton type="button" onClick={handleClick}>
Go to start
</SecondaryButton>
</div>
</div>
);
};
const App = () => {
return (
<WizardForm
initialValues={{
firstName: '',
lastName: '',
faculty: null,
email: '',
}}
steps={[
HarryPotterAnd.PhilosophersStone,
HarryPotterAnd.TheChamberOfSecrets,
HarryPotterAnd.ThePrisonerOfAzkaban,
]}
component={Layout}
/>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
When you are connecting your redux store to one of the step containers, then form's ref won't be forwarded by connect
HOC. To make it work again you need to add { forwardRef: true }
option as the 4-th argument of connect
.
connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(StepContainer);
If you need to do a steps 'trackline' this can be customly done using Wizard's MagicalContext
.
import React from 'react';
import { WizardForm, useWizardContext } from '@panenco/formik-wizard-form';
import * as HarryPotterAnd from './steps';
const WizardTrack = () => {
const { currentStepIndex, stepsMeta, toStep } = useWizardContext();
const handleStepClick = step => () => toStep(step);
return (
<div style={{ display: 'flex' }}>
{stepsMeta.map((step, index) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
{Boolean(index) && <div style={{ color: index <= currentStepIndex ? 'black' : 'grey' }}> - </div>}
<button
type="button"
onClick={handleStepClick(index)}
style={{ color: index <= currentStepIndex ? 'black' : 'grey' }}
>
[{step.title || `Step ${index}`}]
</button>
</div>
);
})}
</div>
);
};
const App = () => {
return (
<WizardForm
initialValues={{
firstName: '',
lastName: '',
faculty: null,
email: '',
}}
steps={[
HarryPotterAnd.PhilosophersStone,
HarryPotterAnd.TheChamberOfSecrets,
HarryPotterAnd.ThePrisonerOfAzkaban,
]}
>
{({ step }) => (
<div>
<h1>My Wizard Form</h1>
<WizardTrack />
<div>{step}</div>
</div>
)}
</WizardForm>
);
};
- React Native (with React-Navigation) usage
- Validation on 'fast travel' with
toStep
- Fiels error to step return
- Add
isValid
tostepsMeta
TBD