├── .gitignore ├── .prettierrc ├── .size-snapshot.json ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── example ├── index.js ├── steps.js └── views │ ├── CompanyInfo.js │ ├── PersonalInfo.js │ └── Summary.js ├── package.json ├── src ├── index.tsx └── types.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts* 5 | dist 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "semi": false, 5 | "arrowParens": "always", 6 | "jsxSingleQuote": false, 7 | "jsxBracketSameLine": false, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "bracketSpacing": true 11 | } 12 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "/Users/zaguini/Development/formik-wizard/dist/formik-wizard.cjs.development.js": { 3 | "bundled": 4395, 4 | "minified": 2348, 5 | "gzipped": 956 6 | }, 7 | "/Users/zaguini/Development/formik-wizard/dist/formik-wizard.cjs.production.js": { 8 | "bundled": 4395, 9 | "minified": 2348, 10 | "gzipped": 956 11 | }, 12 | "/Users/zaguini/Development/formik-wizard/dist/formik-wizard.umd.development.js": { 13 | "bundled": 4971, 14 | "minified": 2245, 15 | "gzipped": 1020 16 | }, 17 | "/Users/zaguini/Development/formik-wizard/dist/formik-wizard.umd.production.js": { 18 | "bundled": 4971, 19 | "minified": 2245, 20 | "gzipped": 1020 21 | }, 22 | "/Users/zaguini/Development/formik-wizard/dist/formik-wizard.es.production.js": { 23 | "bundled": 4186, 24 | "minified": 2169, 25 | "gzipped": 896, 26 | "treeshaked": { 27 | "rollup": { 28 | "code": 92, 29 | "import_statements": 70 30 | }, 31 | "webpack": { 32 | "code": 1185 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | zaguiini 2 | remiroyc 3 | michelts 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luis Felipe Zaguini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS LIBRARY IS LOOKING FOR MAINTAINERS. I (LUIS FELIPE ZAGUINI) DON'T HAVE ENOUGH BANDWIDTH ANYMORE TO WORK ON OSS. IF YOU ARE INTERESTED, PLEASE CONTACT ME: `luisfelipezaguini [at] gmail [dot] com`** 2 | 3 | Also, we're in the process of making this library way more flexible, allowing people to use whatever form and wizard library they like. [Please read this issue](https://github.com/zaguiini/formik-wizard/issues/39) and comment if you will to contribute. 4 | 5 | --- 6 | 7 | # formik-wizard 8 | 9 | A multi-step form component powered by [`formik`](https://github.com/jaredpalmer/formik) and [`react-albus`](https://github.com/americanexpress/react-albus). 10 | 11 | ## Why? 12 | 13 | Large forms are generally bad for User Experience: it becomes both tiresome to fill and, in most of the cases, it gets slow. 14 | I've built this lib to tackle this problem: dividing one big form in multiple smaller forms, it gets much easier to reason about, 15 | both as a developer and as a user. 16 | 17 | All the smaller forms may include validation (powered by [`yup`](https://github.com/jquense/yup)) and default values. 18 | 19 | You can check the demo [here](http://formik-wizard.surge.sh/), with the corresponding source code [here](./example). 20 | 21 | ## Installation 22 | 23 | You need to have `formik` and `react-albus` installed -- they are peer dependencies. 24 | After that, just `yarn add formik-wizard` and you're good to go! 25 | 26 | **If you plan to validate the sections, you need to install `yup` as well!** 27 | 28 | ## Usage 29 | 30 | Check out the [example](./example) source code and the [typings](./src/types.ts). 31 | There's a hook called `useFormikWizard` that you can use to read and write sections values and form statuses. 32 | I recommend using [`immer`](https://github.com/mweststrate/immer) because you're modifying the steps data directly! 33 | 34 | ## Usage with `react-native` 35 | 36 | It's pretty straightforward: just use the `Form` prop component as a `children` forwarder. Example: 37 | 38 | ```js 39 | children} 42 | /> 43 | ``` 44 | 45 | That's needed because there's no `form` web component on React Native and `formik-wizard` (and `formik`) fallbacks to it. 46 | 47 | Also, React Native doesn't have a submit button/input. To achieve a similar result, grab [**formik**'s context](3how-do-you-access-the-formik-context-inside-the-step-form-eg-for-conditional-rendering) and fire its submit handler. 48 | 49 | **REMEMBER: IT'S NOT FORMIK-WIZARD'S CONTEXT. IT'S FORMIK'S!!!** 50 | 51 | ## Troubleshooting 52 | 53 | ### I can't use it as the default export 54 | 55 | That's a known issue. Jared palmer's tsdx [doesn't handle default exports very well](https://github.com/palmerhq/tsdx/issues/165). Two options: 56 | 57 | #### Use it as `FormikWizard.default` 58 | 59 | ```js 60 | import FormikWizard from 'formik-wizard' 61 | 62 | function App() { 63 | return 64 | } 65 | ``` 66 | 67 | or... 68 | 69 | #### Use the named export 70 | 71 | ```js 72 | import { FormikWizard } from 'formik-wizard' 73 | 74 | function App() { 75 | return 76 | } 77 | ``` 78 | 79 | ### How do you use setStatus, setSubmitting inside `handleSubmit` function? 80 | 81 | The `onSubmit` function expects a `Promise`. Whatever you return from that `Promise` will be set as the status. For example: 82 | 83 | ```js 84 | import { useCallback } from 'react' 85 | 86 | const handleSubmit = useCallback((values) => { 87 | return new Promise((resolve) => { 88 | setTimeout(() => { 89 | resolve({ 90 | message: "success" 91 | }) 92 | }, 5000) 93 | }) 94 | }, []) 95 | ``` 96 | 97 | While that `Promise` is pending, the `isSubmitting` flag is set to `true`. The status is set automatically from the return of that `Promise`. 98 | 99 | ### How do you access the Formik context inside the step form (e.g. for conditional rendering)? 100 | 101 | The step form is wrapped inside a [`Formik 102 | component`](https://jaredpalmer.com/formik/docs/api/formik) but its props 103 | aren't propagated to the form component. Anyway, you'll still have access to the 104 | Formik context through one of these methods: 105 | 106 | * by using the [`connect 107 | HOC`](https://jaredpalmer.com/formik/docs/api/connect). 108 | * by using the [`Field 109 | component`](https://jaredpalmer.com/formik/docs/api/field) with a render prop 110 | or a callback function as children. 111 | * by using the `useFormikContext` hook (available in Formik's v2). 112 | 113 | ## License 114 | 115 | MIT 116 | 117 | ## Credits 118 | 119 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). 120 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import FormikWizard from 'formik-wizard' 2 | import React from 'react' 3 | 4 | import steps from './steps' 5 | 6 | function FormWrapper({ 7 | children, 8 | isLastStep, 9 | status, 10 | goToPreviousStep, 11 | canGoBack, 12 | actionLabel, 13 | }) { 14 | return ( 15 |
16 | {status && ( 17 |
18 | {status.message} 19 |
20 |
21 | )} 22 |
23 | 26 | 29 |
30 |
31 | {children} 32 |
33 | ) 34 | } 35 | 36 | function App() { 37 | const handleSubmit = React.useCallback((values) => { 38 | console.log('full values:', values) 39 | 40 | return { 41 | message: 'Thanks for submitting!', 42 | } 43 | }, []) 44 | 45 | return ( 46 | 47 | ) 48 | } 49 | 50 | export default App 51 | -------------------------------------------------------------------------------- /example/steps.js: -------------------------------------------------------------------------------- 1 | import { object, string } from 'yup' 2 | 3 | import CompanyInfo from './views/CompanyInfo' 4 | import PersonalInfo from './views/PersonalInfo' 5 | import Summary from './views/Summary' 6 | 7 | export default [ 8 | { 9 | id: 'personal', 10 | component: PersonalInfo, 11 | initialValues: { 12 | userName: '', 13 | }, 14 | validationSchema: object().shape({ 15 | userName: string().required(), 16 | }), 17 | }, 18 | { 19 | id: 'company', 20 | component: CompanyInfo, 21 | initialValues: { 22 | companyName: '', 23 | }, 24 | validationSchema: object().shape({ 25 | companyName: string().required(), 26 | }), 27 | actionLabel: 'Proceed', 28 | onAction: (sectionValues, formValues) => { 29 | if (sectionValues.companyName === 'argh!') { 30 | throw new Error('Please, choose a better name!') 31 | } 32 | }, 33 | }, 34 | { 35 | id: 'summary', 36 | component: Summary, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /example/views/CompanyInfo.js: -------------------------------------------------------------------------------- 1 | import { FastField, useFormikContext } from 'formik' 2 | import React from 'react' 3 | 4 | function CompanyInfo() { 5 | const { errors, touched } = useFormikContext() 6 | 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 | 14 | {touched.companyName && errors.companyName} 15 | 16 |
(try typing "argh!" once)
17 |
18 | ) 19 | } 20 | 21 | export default CompanyInfo 22 | -------------------------------------------------------------------------------- /example/views/PersonalInfo.js: -------------------------------------------------------------------------------- 1 | import { FastField, useFormikContext } from 'formik' 2 | import React from 'react' 3 | 4 | function PersonalInfo() { 5 | const { errors, touched } = useFormikContext() 6 | 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 | 14 | {touched.userName && errors.userName} 15 | 16 |
17 | ) 18 | } 19 | 20 | export default PersonalInfo 21 | -------------------------------------------------------------------------------- /example/views/Summary.js: -------------------------------------------------------------------------------- 1 | import { useFormikWizard } from 'formik-wizard' 2 | import React from 'react' 3 | 4 | function Summary() { 5 | const { values } = useFormikWizard() 6 | 7 | return ( 8 |
9 |

Is this information correct?

10 |

User name: {values.personal.userName}

11 |

Company name: {values.company.companyName}

12 |
13 | ) 14 | } 15 | 16 | export default Summary 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formik-wizard", 3 | "version": "3.1.1", 4 | "main": "dist/index.js", 5 | "description": "A multi-step form component powered by formik and react-albus", 6 | "umd:main": "dist/formik-wizard.umd.production.js", 7 | "module": "dist/formik-wizard.es.production.js", 8 | "typings": "dist/index.d.ts", 9 | "repository": "https://github.com/zaguiini/formik-wizard", 10 | "author": "Luis Felipe Zaguini", 11 | "license": "MIT", 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "start": "tsdx watch", 17 | "prebuild": "rimraf dist/", 18 | "build": "tsdx build", 19 | "test": "tsdx test --env=jsdom", 20 | "prepublish": "yarn build" 21 | }, 22 | "peerDependencies": { 23 | "formik": "^2.0.1-rc.12", 24 | "react": "^16.8.6", 25 | "react-albus": "^2.0.0", 26 | "yup": "^0.27.0" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^24.0.15", 30 | "@types/react": "^16.8.23", 31 | "@types/react-albus": "^2.0.5", 32 | "@types/react-dom": "^16.8.5", 33 | "@types/yup": "^0.26.22", 34 | "formik": "^2.0.1-rc.12", 35 | "prettier": "^1.18.2", 36 | "react": "^16.8.6", 37 | "react-albus": "^2.0.0", 38 | "rimraf": "^2.6.3", 39 | "tsdx": "^0.7.2", 40 | "typescript": "^3.5.3", 41 | "yup": "^0.27.0" 42 | }, 43 | "dependencies": { 44 | "immer": "^3.1.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form as DefaultForm, Formik, FormikProps } from 'formik' 2 | import produce from 'immer' 3 | import React from 'react' 4 | import { Step as AlbusStep, Steps as AlbusSteps, Wizard as AlbusWizard, WizardContext } from 'react-albus' 5 | 6 | import { 7 | FormikWizardBaseValues, 8 | FormikWizardContextValue, 9 | FormikWizardProps, 10 | FormikWizardStepType, 11 | FormikWizardWrapperProps, 12 | } from './types' 13 | 14 | function getInitialValues(steps: FormikWizardStepType[]) { 15 | return steps.reduce((curr, next) => { 16 | curr[next.id] = next.initialValues 17 | return curr 18 | }, {}) 19 | } 20 | 21 | const FormikWizardContext = React.createContext( 22 | null 23 | ) 24 | 25 | interface FormikWizardStepProps 26 | extends FormikWizardContextValue { 27 | step: FormikWizardStepType 28 | Form?: any 29 | steps: string[] 30 | FormWrapper: React.SFC> 31 | wizard: WizardContext 32 | formikProps?: Partial> 33 | onSubmit: FormikWizardProps['onSubmit'] 34 | } 35 | 36 | function FormikWizardStep({ 37 | step, 38 | Form = DefaultForm, 39 | FormWrapper, 40 | steps, 41 | wizard, 42 | formikProps, 43 | onSubmit, 44 | setStatus, 45 | status, 46 | values, 47 | setValues, 48 | }: FormikWizardStepProps) { 49 | const info = React.useMemo(() => { 50 | return { 51 | canGoBack: steps[0] !== step.id, 52 | currentStep: step.id, 53 | isLastStep: steps[steps.length - 1] === step.id, 54 | } 55 | }, [steps, step]) 56 | 57 | const handleSubmit = React.useCallback( 58 | async (sectionValues) => { 59 | setStatus(undefined) 60 | 61 | let status 62 | 63 | try { 64 | if (info.isLastStep) { 65 | const newValues = produce(values, (draft: any) => { 66 | draft[info.currentStep] = sectionValues 67 | }) 68 | 69 | status = await onSubmit(newValues) 70 | setValues(newValues) 71 | } else { 72 | status = step.onAction 73 | ? await step.onAction(sectionValues, values) 74 | : undefined 75 | 76 | setValues((values: any) => { 77 | return produce(values, (draft: any) => { 78 | draft[info.currentStep] = sectionValues 79 | }) 80 | }) 81 | 82 | setImmediate(wizard.next) 83 | } 84 | } catch (e) { 85 | status = e 86 | } 87 | 88 | setStatus(status) 89 | }, 90 | [ 91 | info.currentStep, 92 | info.isLastStep, 93 | onSubmit, 94 | setStatus, 95 | setValues, 96 | step, 97 | values, 98 | wizard.next, 99 | ] 100 | ) 101 | 102 | return ( 103 | 111 | {(props) => ( 112 |
113 | { 120 | setStatus(undefined) 121 | 122 | if (step.keepValuesOnPrevious) { 123 | setValues((values: any) => 124 | produce(values, (draft: any) => { 125 | draft[step.id] = props.values 126 | }) 127 | ) 128 | } 129 | 130 | wizard.previous() 131 | }} 132 | status={status} 133 | values={values} 134 | setStatus={setStatus} 135 | setValues={setValues} 136 | > 137 | {React.createElement(step.component)} 138 | 139 |
140 | )} 141 |
142 | ) 143 | } 144 | 145 | export function FormikWizard({ 146 | formikProps, 147 | albusProps, 148 | onSubmit, 149 | steps, 150 | Form, 151 | render, 152 | }: FormikWizardProps) { 153 | const [status, setStatus] = React.useState(undefined) 154 | const [values, setValues] = React.useState(() => getInitialValues(steps)) 155 | 156 | React.useEffect(() => { 157 | setValues(getInitialValues(steps)) 158 | setStatus(undefined) 159 | }, [steps]) 160 | 161 | const stepIds = React.useMemo(() => steps.map((step) => step.id), [steps]) 162 | 163 | return ( 164 | 165 | 173 | 174 | {steps.map((step) => ( 175 | ( 179 | 195 | )} 196 | /> 197 | ))} 198 | 199 | 200 | 201 | ) 202 | } 203 | 204 | export default FormikWizard 205 | 206 | export function useFormikWizard() { 207 | return React.useContext(FormikWizardContext) as FormikWizardContextValue 208 | } 209 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { FormikProps, FormikErrors } from 'formik' 2 | import { WizardContext, WizardProps } from 'react-albus' 3 | import { Schema } from 'yup' 4 | 5 | export type FormikWizardBaseValues = any 6 | 7 | export interface FormikWizardContextValue { 8 | status: S 9 | setStatus: React.Dispatch> 10 | values: V 11 | setValues: React.Dispatch> 12 | } 13 | 14 | export interface FormikWizardStepType { 15 | id: string 16 | component: React.SFC<{}> 17 | validationSchema?: Schema 18 | validate?: (values: any) => void | object | Promise>, 19 | initialValues?: FormikWizardBaseValues 20 | actionLabel?: string 21 | onAction?: ( 22 | sectionValues: FormikWizardBaseValues, 23 | formValues: FormikWizardBaseValues 24 | ) => Promise 25 | keepValuesOnPrevious?: boolean 26 | } 27 | 28 | export interface FormikWizardWrapperProps 29 | extends FormikWizardContextValue { 30 | canGoBack: boolean 31 | goToPreviousStep: () => void 32 | currentStep: string 33 | actionLabel?: string 34 | isLastStep: boolean 35 | steps: string[] 36 | wizard: WizardContext 37 | children: React.ReactNode 38 | isSubmitting: boolean 39 | } 40 | 41 | export interface FormikWizardProps { 42 | steps: FormikWizardStepType[] 43 | render: React.SFC> 44 | onSubmit: (values: Values) => void | Promise 45 | formikProps?: Partial> 46 | albusProps?: Partial 47 | Form?: any 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "ESNext", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "rootDir": "./", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "strictPropertyInitialization": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "moduleResolution": "node", 21 | "baseUrl": "./", 22 | "paths": { 23 | "*": ["src/*", "node_modules/*"] 24 | }, 25 | "jsx": "react", 26 | "esModuleInterop": true 27 | }, 28 | "include": ["src", "types"] 29 | } 30 | --------------------------------------------------------------------------------