@panenco/formik-wizard-form
v0.0.3
Published
Formik wizard form wrapper
Keywords
Readme
Wizard Form 🧙♂️
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
Installation
To install formik-wizard-form please follow next steps:
- Check
formik@^2.0.0to be also installed as apeerDependency - Run
npm i @panenco/formik-wizard-form
Usage
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.
WizardProps
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
Create your step containers
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>
);
}
}Init WizardForm
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()).
WizardBag
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;
}Rendering
There are couple ways to render current step.
1. Do nothing will just render current step container and pass MagicalBag + FormikBag to it 🙃
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'));2. Use function in children prop that receives 'ready to render' argument.
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'));3. Use component prop with same signature.
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'));Usage with connect from react-redux
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);Steps Track
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>
);
};To do List:
- React Native (with React-Navigation) usage
- Validation on 'fast travel' with
toStep - Fiels error to step return
- Add
isValidtostepsMetaTBD
