├── .envrc ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ ├── docs.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .tool-versions ├── CNAME ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── blog │ ├── 2022-05-06-welcome-back │ │ └── index.md │ └── authors.yml ├── docs │ └── intro.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── form-icon.svg │ │ ├── robo-wizard.png │ │ └── xstate-fsm-logo.png └── tsconfig.json ├── examples ├── alpine │ ├── index.html │ ├── index.js │ └── package.json ├── history │ ├── .gitignore │ ├── favicon.svg │ ├── index.html │ ├── package.json │ ├── src │ │ ├── main.ts │ │ └── vite-env.d.ts │ └── tsconfig.json ├── html │ ├── app.ts │ ├── index.html │ └── package.json ├── react-context │ ├── app.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── vite.config.js ├── react-router │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── favicon.svg │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── react │ ├── app.tsx │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── vite.config.js ├── svelte │ ├── .parcelrc │ ├── App.svelte │ ├── index.html │ ├── index.js │ ├── package.json │ └── vite.config.js └── vue │ ├── App.vue │ ├── index.html │ ├── index.ts │ ├── package.json │ └── vite.config.js ├── package.json ├── packages ├── README.md ├── core │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ └── index.test.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react-router │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Step.tsx │ │ │ └── index.ts │ │ ├── context │ │ │ └── index.tsx │ │ └── index.ts │ ├── test │ │ ├── index.test.tsx │ │ └── setup.ts │ ├── tsconfig.json │ └── vite.config.ts └── react │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── contexts │ │ └── index.tsx │ ├── hooks │ │ ├── index.ts │ │ └── use-wizard.ts │ └── index.ts │ ├── test │ ├── hooks │ │ └── use-wizard.test.ts │ └── setup.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | use asdf 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "airbnb-base", 4 | "plugin:prettier/recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | parser: "@typescript-eslint/parser", 8 | parserOptions: { 9 | tsconfigRootDir: ".", 10 | project: ["./tsconfig.json"] 11 | }, 12 | "settings": { 13 | "import/parsers": { 14 | "@typescript-eslint/parser": [".ts", "tsx"] 15 | }, 16 | "import/resolver": { 17 | typescript: {} 18 | }, 19 | "import/extensions": [".ts", ".tsx"] 20 | }, 21 | rules: { 22 | "no-underscore-dangle": "off", 23 | "import/prefer-default-export": "off", 24 | "import/extensions": [ 25 | "error", 26 | "ignorePackages", 27 | { 28 | ts: "never", 29 | tsx: "never" 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: push 3 | 4 | jobs: 5 | run-tests: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | 11 | - name: pnpm setup 12 | uses: pnpm/action-setup@v2.2.4 13 | with: 14 | version: 7.17.1 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | cache: 'pnpm' 21 | 22 | - name: Build 23 | run: | 24 | pnpm install 25 | NODE_ENV=production pnpm build 26 | 27 | - name: Test 28 | run: | 29 | pnpm lint 30 | pnpm test 31 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy-docs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | 16 | - name: pnpm setup 17 | uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: 7.17.1 20 | 21 | - name: Use Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | cache: 'pnpm' 26 | 27 | - name: Install and Build 🔧 28 | run: | 29 | pnpm install 30 | pnpm build:docs 31 | 32 | - name: Deploy 🚀 33 | uses: JamesIves/github-pages-deploy-action@releases/v3 34 | with: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | BRANCH: gh-pages # The branch the action should deploy to. 37 | FOLDER: _site # The folder the action should deploy. 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | if: "!contains(github.event.head_commit.message, 'skip ci')" 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: pnpm setup 17 | uses: pnpm/action-setup@v2 18 | with: 19 | version: 7.x.x 20 | 21 | - name: Use Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: Release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | run: pnpm run semantic-release 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | examples/**/package-lock.json 6 | examples/**/pnpm-lock.yaml 7 | examples/**/.cache 8 | examples/**/.parcel-cache 9 | docs/build/ 10 | docs/docs/api/ 11 | _site/ 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | workspaces-update=false 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.0.0 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | robo-wizard.js.org 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 HipsterBrown 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robo Wizard 2 | 3 | ![](./docs/static/img/robo-wizard.png) 4 | 5 | Robo Wizard is a library and collection of packages for building multi-step workflows backed by a state machine using [@xstate/fsm](https://xstate.js.org/docs/packages/xstate-fsm/). 6 | 7 | 👀 [Read the docs to find out more](http://robo-wizard.js.org) 8 | 9 | Check out the [examples](https://github.com/HipsterBrown/robo-wizard/tree/main/examples) directory to see a sample of usage with HTML and a few framework integrations. 10 | 11 | ## Local Development 12 | 13 | This project is built with [Vite](https://vitejs.dev/) and uses [pnpm](https://pnpm.io/) for package management. 14 | 15 | Below is a list of commands you will probably find useful. 16 | 17 | ### `pnpm start` 18 | 19 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. 20 | 21 | ### `pnpm run build` 22 | 23 | Bundles the package to the `dist` folder. 24 | The package is optimized and bundled with Rollup into multiple formats (UMD and ES Module). 25 | 26 | ### `pnpm test` 27 | 28 | Runs the test watcher (Vitest) in an interactive mode. 29 | By default, runs tests related to files changed since the last commit. 30 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ pnpm i 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ pnpm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ pnpm build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | See the `docs.yml` GitHub Actions workflow 30 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/blog/2022-05-06-welcome-back/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome-back 3 | title: Welcome (Back) 4 | authors: [hipsterbrown] 5 | tags: [intro] 6 | --- 7 | 8 | It's been a little over 2 years since the version 1.0.0 release of `robo-wizard`: 9 | 10 |

Related to this, I published v1.0.0 of a new project called robo-wizard, a library for building multi-step forms backed by a state machine. 🤖🧙‍♂️

👏 Shoutout to @matthewcp for robot3, which makes this all possible.https://t.co/7kujZPy0uj

— Nick Hehr (@hipsterbrown) May 11, 2020
11 | 12 | Quite a bit has changed since that original announcement, although the focus of the library has remained the same. 13 | 14 | While I'm still grateful to [robot3](https://thisrobot.life/) for inspiring the minimal API and creating a smooth on-ramp to learning about state machines in UI development, that dependency has been replaced by [`@xstate/fsm`](https://xstate.js.org/docs/packages/xstate-fsm/). XState FSM also provides a minimal API to power the internal logic of `robo-wizard`, with consistent updates and dedicated team behind it. This refactor was purely internal and did not affect the public API of the package. 15 | 16 | Another advantage of adopting XState is the ability to fire "entry" actions that can be configured as options when creating the machine, separate from the schema. This feature lead to the `createWizard` function from `robo-wizard` accepting its first action parameter for navigating [routed steps](https://github.com/HipsterBrown/robo-wizard/pull/17). 17 | 18 | ```typescript 19 | import { createWizard } from 'robo-wizard'; 20 | const wizard = createWizard( 21 | ['first', 'second', 'third'], 22 | { firstName: '', lastName: '' } 23 | { 24 | navigate: () => history.pushState({}, '', `/${wizard.currentStep}`) 25 | } 26 | ); 27 | window.addEventListener('popstate', () => { 28 | const stepFromPath = window.location.pathname.split('/').pop(); 29 | if (stepFromPath !== wizard.currentStep) { 30 | wizard.sync({ step: stepFromPath }) 31 | } 32 | }) 33 | ``` 34 | 35 | --- 36 | 37 | While these updates are exciting enough for a single blog post, there's _one more thing_. 38 | 39 | The core `robo-wizard` package is no longer alone in the [repo](https://github.com/HipsterBrown/robo-wizard); the first official framework integration packages have been published: [`@robo-wizard/react`](/docs/api/modules/robo_wizard_react) & [`@robo-wizard/react-router`](/docs/api/modules/robo_wizard_react_router) 40 | 41 | These packages make getting started with Robo Wizard and React much easier than before, while the [other framework examples still exist](https://github.com/HipsterBrown/robo-wizard/tree/main/examples) until more demand motivates their own official integrations. The repo is now in a better place to support adding more packages through the help of [pnpm workspaces](https://pnpm.io/workspaces). 42 | 43 | It feels good to get this project started again. There's plenty of work ahead; from documentation and educational content to feature development to expand the functionality to as many environments as possible. If any of this sounds interesting to you, please drop a line on [Twitter](https://twitter.com/hipsterbrown) or start a [discussion](https://github.com/HipsterBrown/robo-wizard/discussions) in the project repo. 44 | 45 | Cheers and thanks for making the web a bit more magical! 46 | -------------------------------------------------------------------------------- /docs/blog/authors.yml: -------------------------------------------------------------------------------- 1 | hipsterbrown: 2 | name: HipsterBrown (Nick Hehr) 3 | title: Maintainer of Robo Wizard 4 | url: https://github.com/hipsterbrown 5 | image_url: https://github.com/hipsterbrown.png 6 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Overview 6 | 7 | Robo Wizard is a library and collection of packages for building multi-step workflows backed by a state machine using [@xstate/fsm](https://xstate.js.org/docs/packages/xstate-fsm/). 8 | 9 | ## Installation 10 | 11 | This package is written in TypeScript so type definitions are included by default: 12 | 13 | ``` 14 | npm install robo-wizard 15 | ``` 16 | 17 | ``` 18 | pnpm install robo-wizard 19 | ``` 20 | 21 | ``` 22 | yarn add robo-wizard 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Basics 28 | 29 | ```typescript 30 | import { createWizard } from 'robo-wizard'; 31 | 32 | const wizard = createWizard(['first', 'second', 'third']); 33 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep) }); 34 | 35 | console.log(wizard.currentStep); // first 36 | 37 | wizard.goToNextStep(); 38 | 39 | console.log(wizard.currentStep); // second 40 | 41 | wizard.goToNextStep(); 42 | 43 | console.log(wizard.currentStep); // third 44 | 45 | wizard.goToPreviousStep(); 46 | 47 | console.log(wizard.currentStep); // second 48 | ``` 49 | 50 | ### Gathering Values 51 | 52 | ```typescript 53 | import { createWizard } from 'robo-wizard'; 54 | 55 | const wizard = createWizard(['first', 'second', 'third'], { firstName: '', lastName: '' }); 56 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep), updatedWizard.currentValues }); 57 | 58 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 59 | 60 | wizard.goToNextStep({ values: { firstName: 'Jane' } }); 61 | 62 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: '' } 63 | 64 | wizard.goToNextStep({ values: { lastName: 'Doe' } }); 65 | 66 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: 'Doe' } 67 | 68 | wizard.goToPreviousStep({ values: { firstName: '', lastName: '' } }); 69 | 70 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 71 | ``` 72 | 73 | ### Navigation 74 | 75 | In order to act as a good web citizen, robo-wizard provides a way to integrate with client-side routing APIs for steps that map to real URL paths. 76 | 77 | ```typescript 78 | import { createWizard } from 'robo-wizard'; 79 | 80 | const wizard = createWizard( 81 | ['first', 'second', 'third'], 82 | { firstName: '', lastName: '' } 83 | { 84 | navigate: () => history.pushState({}, '', `/${wizard.currentStep}`) 85 | } 86 | ); 87 | 88 | window.addEventListener('popstate', () => { 89 | const stepFromPath = window.location.pathname.split('/').pop(); 90 | if (stepFromPath && stepFromPath !== wizard.currentStep) wizard.sync({ step: stepFromPath }) 91 | }) 92 | 93 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep), updatedWizard.currentValues }); 94 | 95 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 96 | 97 | wizard.goToNextStep({ values: { firstName: 'Jane' } }); 98 | 99 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: '' } 100 | 101 | wizard.goToNextStep({ values: { lastName: 'Doe' } }); 102 | 103 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: 'Doe' } 104 | 105 | wizard.goToPreviousStep({ values: { firstName: '', lastName: '' } }); 106 | 107 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 108 | ``` 109 | 110 | While the above example demonstrates using the [History API](http://developer.mozilla.org/en-US/docs/Web/API/History_API), see the examples directory for how the [`history`](https://www.npmjs.com/package/history) and [`react-router`](https://www.npmjs.com/package/react-router) packages can be integrated. 111 | 112 | ## Examples 113 | 114 | Check out the [examples](https://github.com/HipsterBrown/robo-wizard/tree/main/examples) directory to see a sample of usage with HTML and a few framework integrations. 115 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | const path = require('path') 4 | 5 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 6 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 7 | 8 | /** @type {import('@docusaurus/types').Config} */ 9 | const config = { 10 | title: 'Robo Wizard', 11 | tagline: 'Intuitive, multi-step workflows backed by a state machine', 12 | url: 'http://robo-wizard.js.org', 13 | baseUrl: '/', 14 | onBrokenLinks: 'warn', 15 | onBrokenMarkdownLinks: 'warn', 16 | favicon: 'img/favicon.ico', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'HipsterBrown', // Usually your GitHub org/user name. 21 | projectName: 'robo-wizard', // Usually your repo name. 22 | 23 | // Even if you don't use internalization, you can use this field to set useful 24 | // metadata like html lang. For example, if your site is Chinese, you may want 25 | // to replace "en" with "zh-Hans". 26 | i18n: { 27 | defaultLocale: 'en', 28 | locales: ['en'], 29 | }, 30 | 31 | plugins: [ 32 | [ 33 | 'docusaurus-plugin-typedoc', 34 | { 35 | "entryPoints": path.join(__dirname, '..'), 36 | "entryPointStrategy": "packages", 37 | "exclude": "../**/*.test.ts", 38 | "name": "API", 39 | "readme": path.join(__dirname, '..', 'packages', 'README.md'), 40 | "includeVersion": true, 41 | "tsconfig": path.join(__dirname, '..', 'tsconfig.json') 42 | } 43 | ], 44 | require.resolve('@cmfcmf/docusaurus-search-local'), 45 | ], 46 | 47 | presets: [ 48 | [ 49 | 'classic', 50 | /** @type {import('@docusaurus/preset-classic').Options} */ 51 | ({ 52 | docs: { 53 | sidebarPath: require.resolve('./sidebars.js'), 54 | // Please change this to your repo. 55 | // Remove this to remove the "edit this page" links. 56 | editUrl: 57 | 'https://github.com/HipsterBrown/robo-wizard/docs/docs/', 58 | }, 59 | blog: { 60 | showReadingTime: false, 61 | editUrl: 62 | 'https://github.com/HipsterBrown/robo-wizard/docs/blog/', 63 | }, 64 | theme: { 65 | customCss: require.resolve('./src/css/custom.css'), 66 | }, 67 | }), 68 | ], 69 | ], 70 | 71 | themeConfig: 72 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 73 | ({ 74 | navbar: { 75 | title: 'Robo Wizard', 76 | logo: { 77 | alt: 'Robo Wizard', 78 | src: 'img/robo-wizard.png' 79 | }, 80 | items: [ 81 | { 82 | type: 'doc', 83 | docId: 'intro', 84 | position: 'left', 85 | label: 'Overview', 86 | }, 87 | { 88 | type: 'doc', 89 | docId: 'api/index', 90 | position: 'left', 91 | label: 'API', 92 | }, 93 | { to: '/blog', label: 'Blog', position: 'left' }, 94 | { 95 | href: 'https://github.com/HipsterBrown/robo-wizard', 96 | label: 'GitHub', 97 | position: 'right', 98 | }, 99 | ], 100 | }, 101 | footer: { 102 | style: 'dark', 103 | links: [ 104 | { 105 | title: 'Docs', 106 | items: [ 107 | { 108 | label: 'API', 109 | to: '/docs/api/index', 110 | }, 111 | ], 112 | }, 113 | { 114 | title: 'More', 115 | items: [ 116 | { 117 | label: 'Blog', 118 | to: '/blog', 119 | }, 120 | { 121 | label: 'GitHub', 122 | href: 'https://github.com/facebook/docusaurus', 123 | }, 124 | ], 125 | }, 126 | ], 127 | copyright: `Copyright © ${new Date().getFullYear()} HipsterBrown Creative. Built with Docusaurus.`, 128 | }, 129 | prism: { 130 | theme: lightCodeTheme, 131 | darkTheme: darkCodeTheme, 132 | }, 133 | }), 134 | }; 135 | 136 | module.exports = config; 137 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build --out-dir ../_site", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.2.0", 19 | "@docusaurus/preset-classic": "2.2.0", 20 | "@mdx-js/react": "^1.6.22", 21 | "clsx": "^1.1.1", 22 | "prism-react-renderer": "^1.3.1", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@cmfcmf/docusaurus-search-local": "^0.10.0", 28 | "@docusaurus/module-type-aliases": "2.2.0", 29 | "@tsconfig/docusaurus": "^1.0.5", 30 | "docusaurus-plugin-typedoc": "^0.17.4", 31 | "typedoc": "^0.22.15", 32 | "typedoc-plugin-markdown": "^3.12.1", 33 | "typescript": "^4.6.4" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg?: React.ComponentType>; 8 | image?: string; 9 | description: JSX.Element; 10 | }; 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | { 14 | title: 'Framework & Form Agnostic', 15 | Svg: require('@site/static/img/form-icon.svg').default, 16 | description: ( 17 | <> 18 | Bring your own UI and form manager, even if it's just plain old HTML! 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Powered by XState FSM', 24 | image: require('@site/static/img/xstate-fsm-logo.png').default, 25 | description: ( 26 | <> 27 | While the underlying implementation could be anything, the flexibility and maturity of the 28 | XState ecosystem provides stellar support for Robo Wizard. 29 | 30 | ), 31 | }, 32 | ]; 33 | 34 | function Feature({ title, Svg, description, image }: FeatureItem) { 35 | return ( 36 |
37 |
38 | {Svg && } 39 | {image && } 40 |
41 |
42 |

{title}

43 |

{description}

44 |
45 |
46 | ); 47 | } 48 | 49 | export default function HomepageFeatures(): JSX.Element { 50 | return ( 51 |
52 |
53 |
54 | {FeatureList.map((props, idx) => ( 55 | 56 | ))} 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .grid { 9 | display: grid; 10 | align-items: baseline; 11 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 12 | gap: 20px; 13 | margin: 0 auto; 14 | } 15 | 16 | .featureSvg { 17 | height: 200px; 18 | width: 200px; 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #8B55CD; 10 | --ifm-color-primary-dark: #602BA1; 11 | --ifm-color-primary-darker: #622FA1; 12 | --ifm-color-primary-darkest: #26074D; 13 | --ifm-color-primary-light: #7A16F5; 14 | --ifm-color-primary-lighter: #B592DF; 15 | --ifm-color-primary-lightest: #DECEF1; 16 | --ifm-color-secondary: #54AECC; 17 | --ifm-color-warning: #DD1155; 18 | --ifm-code-font-size: 95%; 19 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 23 | [data-theme='dark'] { 24 | --ifm-color-primary: var(--ifm-color-primary-lighter); 25 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 26 | } 27 | 28 | [data-theme='dark'] .footer--dark { 29 | --ifm-footer-link-color: var(--ifm-color-primary-darkest); 30 | --ifm-footer-title-color: var(--ifm-color-primary-darkest); 31 | background-color: var(--ifm-color-primary-lighter); 32 | color: var(--ifm-color-primary-darkest); 33 | } 34 | 35 | .footer--dark { 36 | background-color: var(--ifm-color-primary-darkest); 37 | } 38 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .u-color-white { 26 | color: var(--ifm-color-white) !important; 27 | } 28 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import useBaseURL from '@docusaurus/useBaseUrl' 7 | import styles from './index.module.css'; 8 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | {siteConfig.title} 16 |

{siteConfig.title}

17 |

{siteConfig.tagline}

18 |
19 | 22 | Check out the API! 23 | 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | export default function Home(): JSX.Element { 31 | const { siteConfig } = useDocusaurusContext(); 32 | return ( 33 | 36 | 37 |
38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipsterBrown/robo-wizard/c3d4b309242e33682bb1dc7b85a1d2688e1f86ff/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipsterBrown/robo-wizard/c3d4b309242e33682bb1dc7b85a1d2688e1f86ff/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/form-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /docs/static/img/robo-wizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipsterBrown/robo-wizard/c3d4b309242e33682bb1dc7b85a1d2688e1f86ff/docs/static/img/robo-wizard.png -------------------------------------------------------------------------------- /docs/static/img/xstate-fsm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HipsterBrown/robo-wizard/c3d4b309242e33682bb1dc7b85a1d2688e1f86ff/docs/static/img/xstate-fsm-logo.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/docusaurus/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/alpine/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
14 |

Robo Wizard w/ AlpineJS

15 | 16 |

17 | step 18 |

19 | 20 |
23 |
24 | 25 | 27 |
28 | 29 |
30 | 31 | 33 |
34 | 35 |
36 |

Welcome !

38 |
39 | 40 |
41 | 44 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/alpine/index.js: -------------------------------------------------------------------------------- 1 | import 'alpinejs'; 2 | import { createWizard } from 'robo-wizard'; 3 | window.createWizard = createWizard; 4 | -------------------------------------------------------------------------------- /examples/alpine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-alpine-example", 3 | "version": "0.0.0-example", 4 | "description": "Example usage of robo-wizard with alpine.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "vite" 8 | }, 9 | "author": "HipsterBrown", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "alpinejs": "^2.3.5", 13 | "robo-wizard": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "vite": "^2.9.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/history/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/history/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/history/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Robo Wizard Example App 10 | 11 | 12 | 13 |
14 |

Robo Wizard w/ History

15 | 16 |

17 | step 18 |

19 | 20 |
21 | 26 | 27 | 32 | 33 | 37 | 38 |
39 | 42 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/history/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "history", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "typescript": "^4.5.4", 12 | "vite": "^2.9.7" 13 | }, 14 | "dependencies": { 15 | "history": "^5.3.0", 16 | "robo-wizard": "workspace:*" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/history/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createWizard } from 'robo-wizard'; 2 | import history from 'history/browser'; 3 | 4 | 5 | type Values = { 6 | firstName?: string; 7 | lastName?: string; 8 | }; 9 | 10 | const steps: HTMLElement[] = Array.prototype.slice.call( 11 | document.querySelectorAll('[data-step]') 12 | ); 13 | const currentStep = document.querySelector('[data-current-step]') 14 | const currentValues = document.querySelectorAll('[data-values]') 15 | const stepInputs = document.querySelectorAll('input'); 16 | 17 | const wizard = createWizard(steps.map(element => element.dataset.step), { firstName: '', lastName: '' }, { 18 | navigate: () => { 19 | history.push(`/${wizard.currentStep}`) 20 | } 21 | }); 22 | 23 | function render() { 24 | if (currentStep) currentStep.textContent = wizard.currentStep; 25 | currentValues.forEach((element: HTMLElement) => { 26 | element.textContent = wizard.currentValues[element.dataset.values] || ''; 27 | }); 28 | stepInputs.forEach((element: HTMLInputElement) => { 29 | element.value = wizard.currentValues[element.name] || ''; 30 | }); 31 | 32 | for (const step of steps) { 33 | if (step.dataset.step === wizard.currentStep) { 34 | step.classList.remove('hidden'); 35 | } else { 36 | step.classList.add('hidden'); 37 | } 38 | } 39 | } 40 | 41 | document.querySelectorAll('button[data-event]').forEach(button => 42 | button.addEventListener('click', ({ target }) => { 43 | const { dataset } = target as HTMLButtonElement; 44 | if (dataset.event === 'previous') { 45 | wizard.goToPreviousStep(); 46 | } 47 | }) 48 | ); 49 | 50 | document.querySelectorAll('form[data-event]').forEach(form => { 51 | form.addEventListener('submit', event => { 52 | event.preventDefault(); 53 | const values = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement)) 54 | wizard.goToNextStep({ values }); 55 | }); 56 | }); 57 | 58 | if (history.location.pathname === '/') { 59 | history.push(`/${steps[0].dataset.step}`) 60 | } 61 | 62 | history.listen(({ location }) => { 63 | const stepFromPath = location.pathname.split('/').pop(); 64 | if (stepFromPath && stepFromPath !== wizard.currentStep) wizard.sync({ step: stepFromPath }) 65 | }) 66 | 67 | wizard.start(render); 68 | -------------------------------------------------------------------------------- /examples/history/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/history/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/html/app.ts: -------------------------------------------------------------------------------- 1 | import { createWizard } from 'robo-wizard'; 2 | 3 | type Values = { 4 | firstName?: string; 5 | lastName?: string; 6 | }; 7 | 8 | const steps: HTMLElement[] = Array.prototype.slice.call( 9 | document.querySelectorAll('[data-step]') 10 | ); 11 | 12 | const wizard = createWizard(steps.map(element => element.dataset.step)); 13 | 14 | function render() { 15 | document.querySelector('[data-current-step]').textContent = 16 | wizard.currentStep; 17 | document.querySelectorAll('[data-values]').forEach((element: HTMLElement) => { 18 | element.textContent = wizard.currentValues[element.dataset.values] || ''; 19 | }); 20 | document.querySelectorAll('input').forEach((element: HTMLInputElement) => { 21 | element.value = wizard.currentValues[element.name] || ''; 22 | }); 23 | 24 | for (const step of steps) { 25 | if (step.dataset.step === wizard.currentStep) { 26 | step.classList.remove('hidden'); 27 | } else { 28 | step.classList.add('hidden'); 29 | } 30 | } 31 | } 32 | 33 | document.querySelectorAll('button[data-event]').forEach(button => 34 | button.addEventListener('click', ({ target }) => { 35 | const { dataset } = target as HTMLButtonElement; 36 | if (dataset.event === 'previous') { 37 | wizard.goToPreviousStep(); 38 | } 39 | }) 40 | ); 41 | 42 | document.querySelectorAll('form[data-event]').forEach(form => { 43 | form.addEventListener('submit', event => { 44 | event.preventDefault(); 45 | const data = new FormData(event.target as HTMLFormElement); 46 | const values = {}; 47 | data.forEach((value, field) => { 48 | values[field] = value; 49 | }); 50 | 51 | wizard.goToNextStep({ values }); 52 | }); 53 | }); 54 | 55 | wizard.start(render); 56 | -------------------------------------------------------------------------------- /examples/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
13 |

Robo Wizard w/ TypeScript

14 | 15 |

16 | step 17 |

18 | 19 |
20 | 25 | 26 | 31 | 32 | 36 | 37 |
38 | 41 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-html-example", 3 | "version": "0.0.0-example", 4 | "description": "An html example for robo-wizard multi-step form", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "vite" 8 | }, 9 | "author": "HipsterBrown", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "robo-wizard": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "vite": "^2.9.8" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/react-context/app.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { Step, Wizard, useWizardContext } from '@robo-wizard/react'; 3 | 4 | type Values = { 5 | firstName?: string; 6 | lastName?: string; 7 | }; 8 | 9 | const useWizardSubmit = () => { 10 | const wizard = useWizardContext() 11 | 12 | const onSubmit: React.FormEventHandler = event => { 13 | event.preventDefault(); 14 | const values = Object.fromEntries(new FormData(event.currentTarget)) 15 | wizard.goToNextStep({ values }); 16 | }; 17 | 18 | return onSubmit; 19 | } 20 | 21 | const First: React.FC = () => { 22 | const wizard = useWizardContext() 23 | const onSubmit = useWizardSubmit(); 24 | 25 | return ( 26 | <> 27 |

28 | {wizard.currentStep} step 29 |

30 | 31 |
32 |
33 | 40 | 48 |
49 |
50 | 57 | 60 |
61 |
62 | 63 | ) 64 | } 65 | 66 | const Second: React.FC = () => { 67 | const wizard = useWizardContext() 68 | const onSubmit = useWizardSubmit(); 69 | 70 | return ( 71 | <> 72 |

73 | {wizard.currentStep} step 74 |

75 | 76 |
77 |
78 | 85 | 93 |
94 |
95 | 102 | 105 |
106 |
107 | 108 | ) 109 | } 110 | 111 | const Third: React.FC = () => { 112 | const wizard = useWizardContext() 113 | const onSubmit = useWizardSubmit() 114 | 115 | return ( 116 | <> 117 |

118 | {wizard.currentStep} step 119 |

120 | 121 |
122 |
123 |

124 | Welcome {wizard.currentValues.firstName}{' '} 125 | {wizard.currentValues.lastName}! 126 |

127 |
128 |
129 | 136 | 139 |
140 |
141 | 142 | ) 143 | } 144 | 145 | const App: React.FC = () => { 146 | return ( 147 |
148 |

Robo Wizard w/ React

149 | 150 | } /> 151 | } /> 152 | } /> 153 | 154 |
155 | ); 156 | }; 157 | 158 | createRoot(document.getElementById('app')).render(); 159 | -------------------------------------------------------------------------------- /examples/react-context/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/react-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-react-context-example", 3 | "version": "0.0.0-example", 4 | "description": "An example usage of robo-wizard with React Context", 5 | "scripts": { 6 | "start": "vite" 7 | }, 8 | "author": "HipsterBrown", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "@robo-wizard/react": "workspace:*", 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.8", 17 | "@types/react-dom": "^18.0.3", 18 | "@vitejs/plugin-react": "^1.3.2", 19 | "vite": "^2.9.8" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-context/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "lib": [ 5 | "DOM", 6 | "DOM.Iterable", 7 | "es2020" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/react-context/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/react-router/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Robo-Wizard Example App 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-router": "^6.3.0", 14 | "react-router-dom": "^6.3.0", 15 | "@robo-wizard/react-router": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.8", 19 | "@types/react-dom": "^18.0.3", 20 | "@vitejs/plugin-react": "^1.3.2", 21 | "typescript": "^4.6.4", 22 | "vite": "^2.9.7" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-router/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { FormEvent } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom' 3 | import { Step, useWizardContext, Wizard } from '@robo-wizard/react-router' 4 | 5 | type Values = { 6 | firstName?: string; 7 | lastName?: string; 8 | } 9 | 10 | const First = () => { 11 | const wizard = useWizardContext(); 12 | 13 | const onSubmit = (event: FormEvent) => { 14 | event.preventDefault(); 15 | const values = Object.fromEntries(new FormData(event.currentTarget)) 16 | wizard.goToNextStep({ values }) 17 | } 18 | 19 | return ( 20 | <> 21 |

22 | {wizard.currentStep} step 23 |

24 |
25 |
26 | 29 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | ) 46 | } 47 | 48 | const Second = () => { 49 | const wizard = useWizardContext(); 50 | 51 | const onSubmit = (event: FormEvent) => { 52 | event.preventDefault(); 53 | const values = Object.fromEntries(new FormData(event.currentTarget)) 54 | wizard.goToNextStep({ values }) 55 | } 56 | 57 | return ( 58 | <> 59 |

60 | {wizard.currentStep} step 61 |

62 |
63 |
64 | 67 | 76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | ) 84 | } 85 | 86 | const Third = () => { 87 | const wizard = useWizardContext(); 88 | 89 | return ( 90 | <> 91 |

92 | {wizard.currentStep} step 93 |

94 |
95 |

96 | Welcome {wizard.currentValues.firstName}{' '} 97 | {wizard.currentValues.lastName}! 98 |

99 |
100 |
101 | 109 | 112 |
113 | 114 | ); 115 | }; 116 | 117 | function App() { 118 | return ( 119 |
120 |

Robo Wizard w/ React-Router

121 | 122 | initialValues={{ firstName: '', lastName: '' }}> 123 | } /> 124 | } /> 125 | } /> 126 | 127 | 128 |
129 | ) 130 | } 131 | 132 | export default App 133 | -------------------------------------------------------------------------------- /examples/react-router/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/react-router/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /examples/react-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-router/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react-router/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/react/app.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { useWizard } from '@robo-wizard/react'; 3 | 4 | type Values = { 5 | firstName?: string; 6 | lastName?: string; 7 | }; 8 | 9 | const App: React.FC = () => { 10 | const wizard = useWizard(['first', 'second', 'third']); 11 | 12 | const onSubmit: React.FormEventHandler = event => { 13 | event.preventDefault(); 14 | const values = Object.fromEntries(new FormData(event.currentTarget)) 15 | wizard.goToNextStep({ values }); 16 | }; 17 | 18 | return ( 19 |
20 |

Robo Wizard w/ React

21 | 22 |

23 | {wizard.currentStep} step 24 |

25 | 26 |
27 | {wizard.currentStep === 'first' && ( 28 |
29 | 36 | 44 |
45 | )} 46 | 47 | {wizard.currentStep === 'second' && ( 48 |
49 | 56 | 64 |
65 | )} 66 | 67 | {wizard.currentStep === 'third' && ( 68 |
69 |

70 | Welcome {wizard.currentValues.firstName}{' '} 71 | {wizard.currentValues.lastName}! 72 |

73 |
74 | )} 75 | 76 |
77 | 84 | 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | createRoot(document.getElementById('app')).render(); 94 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-react-example", 3 | "version": "0.0.0-example", 4 | "description": "An example usage of robo-wizard with React", 5 | "scripts": { 6 | "start": "vite" 7 | }, 8 | "author": "HipsterBrown", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "@robo-wizard/react": "workspace:*", 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.8", 17 | "@types/react-dom": "^18.0.3", 18 | "@vitejs/plugin-react": "^1.3.2", 19 | "vite": "^2.9.8" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "lib": [ 5 | "DOM", 6 | "DOM.Iterable", 7 | "es2020" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/react/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/svelte/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@parcel/config-default"], 3 | "transformers": { 4 | "*.svelte": ["parcel-transformer-svelte"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/svelte/App.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 |

Robo Wizard w/ Svelte

23 | 24 |

25 | {wizard.currentStep} step 26 |

27 | 28 |
29 | {#if wizard.currentStep === "first"} 30 | 33 | 41 | {/if} 42 | 43 | {#if wizard.currentStep === "second"} 44 | 47 | 55 | {/if} 56 | 57 | {#if wizard.currentStep === "third"} 58 |

59 | Welcome {fullName}! 60 |

61 | {/if} 62 | 63 |
64 | 71 | 74 |
75 |
76 |
77 | -------------------------------------------------------------------------------- /examples/svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/svelte/index.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | new App({ 4 | target: document.getElementById('app'), 5 | }); 6 | -------------------------------------------------------------------------------- /examples/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-svelte-example", 3 | "version": "0.0.0-example", 4 | "description": "Example usage of robo-wizard with svelte", 5 | "scripts": { 6 | "start": "vite" 7 | }, 8 | "author": "HipsterBrown", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "robo-wizard": "workspace:*" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/vite-plugin-svelte": "1.0.0-next.42", 15 | "svelte": "^3.22.2", 16 | "vite": "^2.9.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/svelte/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | export default defineConfig({ 5 | plugins: [svelte()] 6 | }) 7 | -------------------------------------------------------------------------------- /examples/vue/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 78 | 79 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Robo-Wizard Example App 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/vue/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-vue-example", 3 | "version": "0.0.0-example", 4 | "description": "Example usage of robo-wizard with Vue", 5 | "scripts": { 6 | "start": "vite" 7 | }, 8 | "author": "HipsterBrown", 9 | "license": "Apache-2.0", 10 | "dependencies": { 11 | "robo-wizard": "workspace:*", 12 | "vue": "^3.2.33" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^2.3.2", 16 | "vite": "^2.9.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robo-wizard-monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/HipsterBrown/robo-wizard.git" 8 | }, 9 | "author": "HipsterBrown ", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/HipsterBrown/robo-wizard/issues" 13 | }, 14 | "homepage": "http://robo-wizard.js.org", 15 | "packageManager": "pnpm@7.0.0", 16 | "engines": { 17 | "node": "^14.15.0 || >= 16.0.0", 18 | "pnpm": ">=7.0.0" 19 | }, 20 | "scripts": { 21 | "build": "pnpm --filter \"./packages/**\" build", 22 | "build:docs": "pnpm --filter docs build", 23 | "start:docs": "pnpm --filter docs start", 24 | "lint": "eslint \"packages/**/src/**/*.ts\"", 25 | "test": "pnpm --filter \"./packages/**\" test", 26 | "semantic-release": "multi-semantic-release" 27 | }, 28 | "workspaces": [ 29 | "packages/*" 30 | ], 31 | "devDependencies": { 32 | "@qiwi/multi-semantic-release": "^6.1.1", 33 | "@semantic-release/changelog": "^6.0.1", 34 | "@semantic-release/git": "^10.0.1", 35 | "@tsconfig/node16-strictest-esm": "^1.0.1", 36 | "@typescript-eslint/eslint-plugin": "^5.21.0", 37 | "@typescript-eslint/parser": "^5.21.0", 38 | "eslint": "^8.14.0", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-import-resolver-typescript": "^2.7.1", 42 | "eslint-plugin-import": "^2.26.0", 43 | "eslint-plugin-prettier": "^4.0.0", 44 | "prettier": "^2.6.2", 45 | "semantic-release": "^19.0.2", 46 | "tslib": "^2.4.0", 47 | "typedoc": "^0.22.15", 48 | "typescript": "^4.6.3" 49 | }, 50 | "pnpm": { 51 | "overrides": { 52 | "node-notifier@<8.0.1": ">=8.0.1", 53 | "marked@<4.0.10": ">=4.0.10" 54 | }, 55 | "peerDependencyRules": { 56 | "ignoreMissing": [ 57 | "@types/react", 58 | "react", 59 | "react-dom" 60 | ] 61 | } 62 | }, 63 | "release": { 64 | "branches": [ 65 | { 66 | "name": "main" 67 | } 68 | ], 69 | "plugins": [ 70 | "@semantic-release/commit-analyzer", 71 | "@semantic-release/release-notes-generator", 72 | "@semantic-release/changelog", 73 | "@semantic-release/npm", 74 | [ 75 | "@semantic-release/github", 76 | { 77 | "assets": [ 78 | "package.json", 79 | "CHANGELOG.md", 80 | "packages/**/dist/**" 81 | ] 82 | } 83 | ], 84 | [ 85 | "@semantic-release/git", 86 | { 87 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 88 | } 89 | ] 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Robo Wizard packages 2 | 3 | Robo Wizard is collection of packages for building multi-step workflows backed by a state machine using [@xstate/fsm](https://xstate.js.org/docs/packages/xstate-fsm/). 4 | 5 | Available packages: 6 | 7 | - [`robo-wizard`](https://robo-wizard.js.org/docs/api/modules/robo_wizard) 8 | - [`@robo-wizard/react`](https://robo-wizard.js.org/docs/api/modules/robo_wizard_react) 9 | - [`@robo-wizard/react-router`](https://robo-wizard.js.org/docs/api/modules/robo_wizard_react_router) 10 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Robo Wizard 2 | 3 | A library for building multi-step workflows backed by a state machine using [@xstate/fsm](https://xstate.js.org/docs/packages/xstate-fsm/). 4 | 5 | **Installation** 6 | 7 | This package is written in TypeScript so type definitions are included by default: 8 | 9 | ``` 10 | npm install robo-wizard 11 | ``` 12 | 13 | ``` 14 | pnpm install robo-wizard 15 | ``` 16 | 17 | ``` 18 | yarn add robo-wizard 19 | ``` 20 | 21 | **Basic usage:** 22 | 23 | ```typescript 24 | import { createWizard } from 'robo-wizard'; 25 | 26 | const wizard = createWizard(['first', 'second', 'third']); 27 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep) }); 28 | 29 | console.log(wizard.currentStep); // first 30 | 31 | wizard.goToNextStep(); 32 | 33 | console.log(wizard.currentStep); // second 34 | 35 | wizard.goToNextStep(); 36 | 37 | console.log(wizard.currentStep); // third 38 | 39 | wizard.goToPreviousStep(); 40 | 41 | console.log(wizard.currentStep); // second 42 | ``` 43 | 44 | **Gathering values:** 45 | 46 | ```typescript 47 | import { createWizard } from 'robo-wizard'; 48 | 49 | const wizard = createWizard(['first', 'second', 'third'], { firstName: '', lastName: '' }); 50 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep), updatedWizard.currentValues }); 51 | 52 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 53 | 54 | wizard.goToNextStep({ values: { firstName: 'Jane' } }); 55 | 56 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: '' } 57 | 58 | wizard.goToNextStep({ values: { lastName: 'Doe' } }); 59 | 60 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: 'Doe' } 61 | 62 | wizard.goToPreviousStep({ values: { firstName: '', lastName: '' } }); 63 | 64 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 65 | ``` 66 | 67 | **Navigation** 68 | 69 | In order to act as a good web citizen, robo-wizard provides a way to integrate with client-side routing APIs for steps that map to real URL paths. 70 | 71 | ```typescript 72 | import { createWizard } from 'robo-wizard'; 73 | 74 | const wizard = createWizard( 75 | ['first', 'second', 'third'], 76 | { firstName: '', lastName: '' } 77 | { 78 | navigate: () => history.pushState({}, '', `/${wizard.currentStep}`) 79 | } 80 | ); 81 | 82 | window.addEventListener('popstate', () => { 83 | const stepFromPath = window.location.pathname.split('/').pop(); 84 | if (stepFromPath && stepFromPath !== wizard.currentStep) wizard.sync({ step: stepFromPath }) 85 | }) 86 | 87 | wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep), updatedWizard.currentValues }); 88 | 89 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 90 | 91 | wizard.goToNextStep({ values: { firstName: 'Jane' } }); 92 | 93 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: '' } 94 | 95 | wizard.goToNextStep({ values: { lastName: 'Doe' } }); 96 | 97 | console.log(wizard.currentValues); // { firstName: 'Jane', lastName: 'Doe' } 98 | 99 | wizard.goToPreviousStep({ values: { firstName: '', lastName: '' } }); 100 | 101 | console.log(wizard.currentValues); // { firstName: '', lastName: '' } 102 | ``` 103 | 104 | While the above example demonstrates using the [History API](http://developer.mozilla.org/en-US/docs/Web/API/History_API), see the examples directory for how the [`history`](https://www.npmjs.com/package/history) and [`react-router`](https://www.npmjs.com/package/react-router) packages can be integrated. 105 | 106 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.0", 3 | "license": "MIT", 4 | "main": "./dist/robo-wizard.umd.js", 5 | "module": "./dist/robo-wizard.es.js", 6 | "exports": { 7 | ".": { 8 | "import": "./dist/robo-wizard.es.js", 9 | "require": "./dist/robo-wizard.umd.js" 10 | } 11 | }, 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "dist" 15 | ], 16 | "sideEffects": false, 17 | "scripts": { 18 | "start": "vite build --watch", 19 | "build": "vite build", 20 | "test": "vitest", 21 | "prepare": "vite build" 22 | }, 23 | "typedocMain": "src/index.ts", 24 | "prettier": { 25 | "printWidth": 80, 26 | "semi": true, 27 | "singleQuote": true, 28 | "trailingComma": "es5" 29 | }, 30 | "name": "robo-wizard", 31 | "author": "HipsterBrown", 32 | "homepage": "http://robo-wizard.js.org", 33 | "devDependencies": { 34 | "@vitest/ui": "^0.26.1", 35 | "tslib": "^2.4.1", 36 | "typescript": "^4.9.4", 37 | "vite": "^4.0.2", 38 | "vite-plugin-dts": "^1.7.1", 39 | "vitest": "^0.26.1" 40 | }, 41 | "dependencies": { 42 | "@xstate/fsm": "^1.5.2" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/HipsterBrown/robo-wizard.git", 47 | "directory": "packages/core" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createMachine, interpret, StateMachine, assign } from '@xstate/fsm'; 2 | 3 | /** Base type describing an object of string keys and `any` value */ 4 | export type BaseValues = object; 5 | 6 | /** 7 | * Event object containing any new values to be updated 8 | */ 9 | type UpdateEvent = { 10 | type: 'next' | 'previous'; 11 | values?: Partial; 12 | }; 13 | 14 | /** 15 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 16 | * @param values Current state of values gathered through the wizard 17 | * @param event Event object containing any new values to be updated, see [[UpdateEvent]] 18 | */ 19 | export type WhenFunction = ( 20 | values: Values, 21 | event: UpdateEvent 22 | ) => boolean; 23 | 24 | /** 25 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 26 | * 27 | * A string is a shorthand for an always true conditional guard, i.e. `['nextStep', when(() => true)]` 28 | * The array version should be a tuple with the string name of a step, paired with a guard, like [[when]]: 29 | * @example 30 | * ```typescript 31 | * [ 32 | * ['conditionalStep', when((currentValues, { values }) => values.showConditional === true)], 33 | * 'fallbackStep' 34 | * ] // StepTransition[] 35 | * ``` 36 | */ 37 | export type StepTransition = 38 | | string 39 | | Array }>; 40 | 41 | /** 42 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 43 | */ 44 | export type StepConfig = { 45 | /** Name of the step */ 46 | name: string; 47 | /** 48 | * Name of the next step in progression after calling [[goToNextStep]]. 49 | * Passing `false` will prevent the wizard from progressing forward at this step. 50 | * Passing a [[StepTransition]] array allows for conditional progression using the [[`when`]] helper. 51 | */ 52 | next?: string | StepTransition[] | boolean; 53 | /** 54 | * Name of the previous step in progression after calling [[goToPreviousStep]]. 55 | * Passing `false` will prevent the wizard from progressing backwards at this step. 56 | * Passing a [[StepTransition]] array allows for conditional progression using the [[`when`]] helper. 57 | */ 58 | previous?: string | StepTransition[] | boolean; 59 | }; 60 | 61 | /** 62 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 63 | * 64 | * A step in the wizard can be described as a string for default progression, or an object for custom progression, see [[StepConfig]] for more details 65 | */ 66 | export type FlowStep = 67 | | string 68 | | StepConfig; 69 | 70 | export type WizardEvent = 71 | | { 72 | type: 'next'; 73 | values?: Partial; 74 | } 75 | | { 76 | type: 'previous'; 77 | } 78 | | { 79 | type: string; 80 | values?: Partial; 81 | }; 82 | 83 | export type WizardMachine = 84 | StateMachine.Machine, any>; 85 | 86 | /** 87 | * @typeParam StepMachine Generic type for the configured state machine, based on [[Machine]] from [robot](https://thisrobot.life) 88 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 89 | * @param wizard The [[RoboWizard]] instance that has just been updated 90 | * 91 | * An event handler for reacting when the wizard updates, i.e. after step progression or values have been updated 92 | */ 93 | type ChangeHandler = ( 94 | wizard: RoboWizard // eslint-disable-line no-use-before-define 95 | ) => void; 96 | 97 | /** 98 | * @typeParam StepMachine Generic type for the configured state machine, based on the `Machine` type from [robot](https://thisrobot.life) 99 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 100 | * 101 | * This class is the return value from [[createWizard]] and the only way to be instantiated. 102 | */ 103 | export class RoboWizard { 104 | /** @ignore */ 105 | private _service?: StateMachine.Service, any>; // eslint-disable-line @typescript-eslint/no-explicit-any 106 | 107 | /** @ignore */ 108 | private machine: WizardMachine; 109 | 110 | /** @ignore */ 111 | constructor(machine: WizardMachine) { 112 | this.machine = machine; 113 | } 114 | 115 | /** 116 | * @param onChange [[ChangeHandler]] for reacting when the wizard updates, this will be called immediately 117 | * @param values Optional object of initial values to use when creating the wizard 118 | * 119 | * This should be called before trying to access any other methods or properties, otherwise an Error will be thrown 120 | */ 121 | public start(onChange: ChangeHandler, values?: Values) { 122 | if (this._service) return; 123 | this._service = interpret>(this.machine); 124 | this._service.subscribe(() => onChange(this)); 125 | this._service.start({ 126 | context: values || this.machine.config.context, 127 | value: this.machine.initialState.value, 128 | }); 129 | } 130 | 131 | /** 132 | * Returns the current step of the wizard 133 | */ 134 | get currentStep() { 135 | return this.service.state.value; 136 | } 137 | 138 | /** 139 | * Returns the current state of values been gathered through the wizard 140 | */ 141 | get currentValues() { 142 | return this.service.state.context; 143 | } 144 | 145 | /** 146 | * @param event Optional object with a `values` field to described what Values should be updated, see [[UpdateEvent]] 147 | * Progress to the next step in the wizard 148 | */ 149 | public goToNextStep(event: Partial> = {}) { 150 | this.service.send({ type: 'next', ...event }); 151 | } 152 | 153 | /** 154 | * @param event Optional object with a `values` field to described what Values should be updated, see [[UpdateEvent]] 155 | * Progress to the previous step in the wizard 156 | */ 157 | public goToPreviousStep(event: Partial> = {}) { 158 | this.service.send({ type: 'previous', ...event }); 159 | } 160 | 161 | /** 162 | * @param event Optional object with a `step` field to described what the current step _should_ be 163 | * Sync the external updated step with internal service 164 | */ 165 | public sync(event: { step: string }) { 166 | this.service.send({ type: event.step }); 167 | } 168 | 169 | /** @ignore */ 170 | private get service() { 171 | if (typeof this._service === 'undefined') { 172 | throw new Error( 173 | 'Please call "start" before any other method or property.' 174 | ); 175 | } 176 | return this._service; 177 | } 178 | } 179 | 180 | function getPreviousTarget( 181 | step: StepConfig, 182 | index: number, 183 | steps: StepConfig[] 184 | ): string | undefined { 185 | if (typeof step.previous === 'string') { 186 | return step.previous; 187 | } 188 | if (step.previous !== false && step.name !== steps[0]?.name) { 189 | return steps[index - 1]?.name; 190 | } 191 | return undefined; 192 | } 193 | 194 | function getNextTarget( 195 | step: StepConfig, 196 | index: number, 197 | steps: StepConfig[] 198 | ) { 199 | let stepName: string | undefined; 200 | if (typeof step.next === 'string') { 201 | stepName = step.next; 202 | } else if ( 203 | step.next !== false && 204 | step.name !== steps[steps.length - 1]?.name 205 | ) { 206 | stepName = steps[index + 1]?.name; 207 | } 208 | return { 209 | target: stepName, 210 | actions: 'assignNewValues', 211 | }; 212 | } 213 | 214 | function getNextTargets( 215 | nextSteps: StepTransition[] 216 | ) { 217 | return nextSteps.map((step) => { 218 | const [stepName, guard] = typeof step === 'string' ? [step] : step; 219 | const { cond } = typeof guard === 'object' ? guard : { cond: () => true }; 220 | return { 221 | target: typeof stepName === 'string' ? stepName : undefined, 222 | cond, 223 | actions: 'assignNewValues', 224 | }; 225 | }); 226 | } 227 | 228 | type MaybeTarget = { target: string | undefined }; 229 | type HasTarget = 230 | | Required<{ target: string }> 231 | | Array>; 232 | 233 | function hasTarget(config: MaybeTarget | MaybeTarget[]): config is HasTarget { 234 | if (Array.isArray(config)) return config.every((o) => !!o.target); 235 | return !!config.target; 236 | } 237 | 238 | /** 239 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 240 | * @param steps Configuration of steps for the wizard, see [[FlowStep]] 241 | * @param initialValues Optional object with intial values to use when starting the wizard 242 | * @param actions Optional object with navigate field with a function to be called when entering a step 243 | * 244 | * 245 | * @example Initial set up with a listener for updates to the wizard 246 | * ```typescript 247 | * import { createWizard } from 'robo-wizard'; 248 | * 249 | * const wizard = createWizard(['first', 'second', 'third']); 250 | * wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep) }); 251 | * 252 | * console.log(wizard.currentStep); // first 253 | * 254 | * wizard.goToNextStep(); 255 | * 256 | * console.log(wizard.currentStep); // second 257 | * 258 | * wizard.goToNextStep(); 259 | * 260 | * console.log(wizard.currentStep); // third 261 | * 262 | * wizard.goToPreviousStep(); 263 | * 264 | * console.log(wizard.currentStep); // second 265 | * ``` 266 | * 267 | * @example Gathering values through each step when progressing forward 268 | * ```typescript 269 | * import { createWizard } from 'robo-wizard'; 270 | * 271 | * const wizard = createWizard(['first', 'second', 'third'], { firstName: '', lastName: '' }); 272 | * wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep), updatedWizard.currentValues }); 273 | * 274 | * console.log(wizard.currentValues); // { firstName: '', lastName: '' } 275 | *; 276 | * wizard.goToNextStep({ values: { firstName: 'Jane' } }); 277 | * 278 | * console.log(wizard.currentValues); // { firstName: 'Jane', lastName: '' } 279 | * 280 | * wizard.goToNextStep({ values: { lastName: 'Doe' } }); 281 | * 282 | * console.log(wizard.currentValues); // { firstName: 'Jane', lastName: 'Doe' } 283 | * 284 | * wizard.goToPreviousStep({ values: { firstName: '', lastName: '' } }); 285 | * 286 | * console.log(wizard.currentValues); // { firstName: '', lastName: '' } 287 | * ``` 288 | * 289 | * By default, the wizard will progress linearly in the order of array passed to `createWizard`. That behavior can be overriden by passing an [[StepConfig]] to in place of the string step name: 290 | * @example 291 | * ```typescript 292 | * import { createWizard } from 'robo-wizard'; 293 | * 294 | * const wizard = createWizard([ 295 | * { name: 'first', next: 'third' }, 'skipped', {name: 'third', previous: 'first'} 296 | * ]); 297 | * wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep) }); 298 | * 299 | * console.log(wizard.currentStep); // first 300 | *; 301 | * wizard.goToNextStep(); 302 | * 303 | * console.log(wizard.currentStep); // third 304 | * 305 | * wizard.goToPreviousStep(); 306 | * 307 | * console.log(wizard.currentStep); // first 308 | * ``` 309 | * 310 | * @example Progression can be conditional using the [[when]] helper 311 | * ```typescript 312 | * import { createWizard, when } from 'robo-wizard'; 313 | * 314 | * const wizard = createWizard([ 315 | * { name: 'first', next: [['third', when((currentValues, { values }) => values.goToThird )], 'second'] }, 316 | * 'second', 317 | * {name: 'third', previous: 'first'} 318 | * ], { goToThird: false }); 319 | * wizard.start(updatedWizard => { console.log('Updated!', updatedWizard.currentStep) }); 320 | * 321 | * console.log(wizard.currentStep); // first 322 | *; 323 | * wizard.goToNextStep({ values: { goToThird: true } }); 324 | * 325 | * console.log(wizard.currentStep); // third 326 | * 327 | * wizard.goToPreviousStep({ values: { goToThird: false } }); 328 | * 329 | * console.log(wizard.currentStep); // first 330 | * 331 | * wizard.goToNextStep(); 332 | * 333 | * console.log(wizard.currentStep); // second 334 | * 335 | * wizard.goToNextStep(); 336 | * 337 | * console.log(wizard.currentStep); // third 338 | * ``` 339 | */ 340 | export function createWizard( 341 | steps: FlowStep[], 342 | initialValues: Values = {} as Values, 343 | actions: { 344 | navigate?: StateMachine.ActionFunction>; 345 | } = { 346 | navigate: () => { 347 | /* noop */ 348 | }, 349 | } 350 | ): RoboWizard { 351 | const normalizedSteps: StepConfig[] = steps.map((step) => 352 | typeof step === 'string' ? { name: step } : step 353 | ); 354 | const syncTargets = normalizedSteps.reduce( 355 | (result, { name }) => ({ ...result, [name]: { target: name } }), 356 | {} 357 | ); 358 | const config: StateMachine.Config> = { 359 | id: 'robo-wizard', 360 | initial: normalizedSteps[0]?.name ?? 'unknown', 361 | context: initialValues, 362 | states: normalizedSteps.reduce< 363 | StateMachine.Config>['states'] 364 | >((result, step, index) => { 365 | const previousTarget = getPreviousTarget(step, index, normalizedSteps); 366 | const nextTarget = Array.isArray(step.next) 367 | ? getNextTargets(step.next) 368 | : getNextTarget(step, index, normalizedSteps); 369 | 370 | // eslint-disable-next-line no-param-reassign 371 | result[step.name] = { 372 | entry: ['navigate'], 373 | on: { 374 | ...(previousTarget ? { previous: { target: previousTarget } } : {}), 375 | ...(hasTarget(nextTarget) ? { next: nextTarget } : {}), 376 | ...syncTargets, 377 | }, 378 | }; 379 | return result; 380 | }, {}), 381 | }; 382 | const assignNewValues = assign>( 383 | (context, event) => { 384 | if (event.type === 'next') { 385 | return { 386 | ...context, 387 | ...event.values, 388 | }; 389 | } 390 | return context; 391 | } 392 | ); 393 | const machine = createMachine, any>(config, { 394 | actions: { 395 | assignNewValues, 396 | navigate: (values, event) => { 397 | if (['next', 'previous'].includes(event.type)) 398 | actions.navigate?.(values, event); 399 | }, 400 | }, 401 | }); 402 | return new RoboWizard(machine); 403 | } 404 | 405 | /** 406 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 407 | * @param cond Guard function to be called to test if the step should transition, see [[WhenFunction]] 408 | * See [[createWizard]] for example usage 409 | */ 410 | export function when( 411 | cond: WhenFunction 412 | ) { 413 | return { 414 | cond, 415 | }; 416 | } 417 | -------------------------------------------------------------------------------- /packages/core/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import { createWizard, when, WhenFunction } from '../src'; 3 | 4 | describe('createWizard', () => { 5 | describe('using array of strings', () => { 6 | const steps = ['first', 'second', 'third']; 7 | 8 | it('progresses forward linearly', () => { 9 | let currentStep; 10 | let nextStep = (_event?: { values: object }) => { }; 11 | let previousStep = () => { }; 12 | const wizard = createWizard(steps); 13 | 14 | wizard.start(wiz => { 15 | currentStep = wiz.currentStep; 16 | nextStep = wiz.goToNextStep.bind(wiz); 17 | previousStep = wiz.goToPreviousStep.bind(wiz); 18 | }); 19 | 20 | expect(currentStep).toBe('first'); 21 | 22 | nextStep(); 23 | 24 | expect(currentStep).toBe('second'); 25 | 26 | nextStep(); 27 | 28 | expect(currentStep).toBe('third'); 29 | 30 | nextStep(); 31 | 32 | // should not change 33 | expect(currentStep).toBe('third'); 34 | 35 | previousStep(); 36 | 37 | expect(currentStep).toBe('second'); 38 | 39 | previousStep(); 40 | 41 | expect(currentStep).toBe('first'); 42 | 43 | previousStep(); 44 | 45 | // should not change 46 | expect(currentStep).toBe('first'); 47 | }); 48 | 49 | it('updates values during progression', () => { 50 | let currentValues; 51 | let nextStep = (_event: { values?: object }) => { }; 52 | let previousStep = () => { }; 53 | const wizard = createWizard(steps); 54 | 55 | wizard.start(wiz => { 56 | currentValues = wiz.currentValues; 57 | nextStep = wiz.goToNextStep.bind(wiz); 58 | previousStep = wiz.goToPreviousStep.bind(wiz); 59 | }); 60 | 61 | expect(currentValues).toMatchObject({}); 62 | 63 | nextStep({ values: { test: 'string' } }); 64 | 65 | expect(currentValues).toMatchObject({ test: 'string' }); 66 | 67 | nextStep({ values: { another: 'value' } }); 68 | 69 | expect(currentValues).toMatchObject({ test: 'string', another: 'value' }); 70 | 71 | nextStep({ values: { nothing: 'doing' } }); 72 | 73 | // should not update because no progress forward 74 | expect(currentValues).toMatchObject({ test: 'string', another: 'value' }); 75 | 76 | previousStep(); 77 | 78 | // does not update values 79 | expect(currentValues).toMatchObject({ test: 'string', another: 'value' }); 80 | }); 81 | }); 82 | 83 | describe('array of config objects', () => { 84 | const steps = [{ name: 'first' }, { name: 'second' }, { name: 'third' }]; 85 | 86 | it('progresses forward linearly', () => { 87 | const wizard = createWizard(steps); 88 | wizard.start(() => { }); 89 | 90 | expect(wizard.currentStep).toBe('first'); 91 | 92 | wizard.goToNextStep(); 93 | 94 | expect(wizard.currentStep).toBe('second'); 95 | 96 | wizard.goToNextStep(); 97 | 98 | expect(wizard.currentStep).toBe('third'); 99 | 100 | wizard.goToNextStep(); 101 | 102 | // should not change 103 | expect(wizard.currentStep).toBe('third'); 104 | 105 | wizard.goToPreviousStep(); 106 | 107 | expect(wizard.currentStep).toBe('second'); 108 | 109 | wizard.goToPreviousStep(); 110 | 111 | expect(wizard.currentStep).toBe('first'); 112 | 113 | wizard.goToPreviousStep(); 114 | 115 | // should not change 116 | expect(wizard.currentStep).toBe('first'); 117 | }); 118 | }); 119 | 120 | describe('steps with next, previous config', () => { 121 | const steps = [ 122 | { name: 'first', next: 'third' }, 123 | { name: 'second', next: false, previous: 'third' }, 124 | { name: 'third' }, 125 | ]; 126 | 127 | it('progresses forward linearly', () => { 128 | const wizard = createWizard(steps); 129 | wizard.start(() => { }); 130 | 131 | expect(wizard.currentStep).toBe('first'); 132 | 133 | wizard.goToNextStep(); 134 | 135 | expect(wizard.currentStep).toBe('third'); 136 | 137 | wizard.goToPreviousStep(); 138 | 139 | expect(wizard.currentStep).toBe('second'); 140 | 141 | wizard.goToNextStep(); 142 | 143 | // should not change 144 | expect(wizard.currentStep).toBe('second'); 145 | 146 | wizard.goToPreviousStep(); 147 | 148 | expect(wizard.currentStep).toBe('third'); 149 | }); 150 | }); 151 | 152 | describe('steps with conditional next config', () => { 153 | const initialValues = { skip: false }; 154 | type Values = typeof initialValues; 155 | const skipIsTrue: WhenFunction = (values, { values: nextValues }) => 156 | ({ ...values, ...nextValues }.skip); 157 | const steps = [ 158 | { name: 'first', next: [['third', when(skipIsTrue)], 'second'] }, 159 | 'second', 160 | 'third', 161 | ]; 162 | 163 | it('progresses forward conditionally', () => { 164 | const wizard = createWizard(steps, initialValues); 165 | 166 | wizard.start(() => { }); 167 | 168 | expect(wizard.currentValues).toMatchObject(initialValues); 169 | 170 | wizard.goToNextStep(); 171 | 172 | expect(wizard.currentStep).toBe('second'); 173 | 174 | wizard.goToPreviousStep(); 175 | 176 | expect(wizard.currentStep).toBe('first'); 177 | 178 | wizard.goToNextStep({ values: { skip: true } }); 179 | 180 | expect(wizard.currentStep).toBe('third'); 181 | }); 182 | }); 183 | 184 | describe('when passing navigate action', () => { 185 | const steps = ['first', 'second', 'third']; 186 | 187 | it('calls navigate when entering a step', () => { 188 | const navigate = vi.fn(); 189 | const wizard = createWizard(steps, {}, { navigate }) 190 | 191 | wizard.start(() => { }); 192 | 193 | expect(navigate).not.toHaveBeenCalled(); 194 | 195 | wizard.goToNextStep() 196 | 197 | expect(navigate).toHaveBeenCalled(); 198 | 199 | wizard.goToPreviousStep() 200 | 201 | expect(navigate).toHaveBeenCalledTimes(2); 202 | }) 203 | }) 204 | 205 | describe('when calling sync method', () => { 206 | const steps = ['first', 'second', 'third']; 207 | 208 | it('updates the currentStep without navigating', () => { 209 | const navigate = vi.fn(); 210 | const wizard = createWizard(steps, {}, { navigate }) 211 | 212 | wizard.start(() => { }); 213 | 214 | expect(wizard.currentStep).toBe('first') 215 | 216 | wizard.sync({ step: 'third' }); 217 | 218 | expect(wizard.currentStep).toBe('third') 219 | 220 | expect(navigate).not.toHaveBeenCalled(); 221 | }) 222 | }) 223 | }); 224 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src", 5 | "./types" 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | "test", 10 | "dist" 11 | ], 12 | "compilerOptions": { 13 | "rootDir": "src" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import dts from 'vite-plugin-dts' 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: path.resolve(__dirname, 'src/index.ts'), 9 | name: 'RoboWizard', 10 | fileName: format => `robo-wizard.${format}.js` 11 | }, 12 | sourcemap: true, 13 | }, 14 | plugins: [dts()] 15 | }) 16 | -------------------------------------------------------------------------------- /packages/react-router/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## @robo-wizard/react-router [1.0.3](https://github.com/HipsterBrown/robo-wizard/compare/@robo-wizard/react-router@1.0.2...@robo-wizard/react-router@1.0.3) (2022-12-20) 2 | 3 | 4 | 5 | 6 | 7 | ### Dependencies 8 | 9 | * **@robo-wizard/react:** upgraded to 1.1.0 10 | 11 | ## @robo-wizard/react-router [1.0.2](https://github.com/HipsterBrown/robo-wizard/compare/@robo-wizard/react-router@1.0.1...@robo-wizard/react-router@1.0.2) (2022-11-29) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * **react-router:** group index redirect route with steps config ([#33](https://github.com/HipsterBrown/robo-wizard/issues/33)) ([29073ce](https://github.com/HipsterBrown/robo-wizard/commit/29073ce47b4bcdd762a85222178fa492fc6c7ff6)) 17 | 18 | ## @robo-wizard/react-router [1.0.1](https://github.com/HipsterBrown/robo-wizard/compare/@robo-wizard/react-router@1.0.0...@robo-wizard/react-router@1.0.1) (2022-11-29) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * wrap routed steps in Fragment to avoid keyed children warning ([#29](https://github.com/HipsterBrown/robo-wizard/issues/29)) ([d0f6f77](https://github.com/HipsterBrown/robo-wizard/commit/d0f6f77a0276d57ff9692ece3fa602dd749d20b3)) 24 | 25 | # @robo-wizard/react-router 1.0.0 (2022-05-05) 26 | 27 | 28 | ### Features 29 | 30 | * **react-router:** initial package w/ Wizard, Step components ([#20](https://github.com/HipsterBrown/robo-wizard/issues/20)) ([e918544](https://github.com/HipsterBrown/robo-wizard/commit/e9185445ee1dedc6256790e4a2443d093d90cda2)) 31 | -------------------------------------------------------------------------------- /packages/react-router/README.md: -------------------------------------------------------------------------------- 1 | # Robo Wizard React-Router 2 | 3 | A library for building _routed_, multi-step workflows in React. 4 | 5 | ## Getting Started 6 | 7 | ``` 8 | npm i @robo-wizard/react-router 9 | ``` 10 | 11 | Scaffold a basic wizard experience: 12 | 13 | ```tsx 14 | import { BrowserRouter } from 'react-router-dom' 15 | import { Wizard, Step } from '@robo-wizard/react-router' 16 | // import FirstName, LastName, Success from relevant modules 17 | 18 | function App() { 19 | return ( 20 | 21 | 22 | } /> 23 | } /> 24 | } /> 25 | 26 | 27 | ) 28 | } 29 | ``` 30 | 31 | Create a step that gathers relevant values (bring your own form library as needed): 32 | 33 | ```tsx 34 | import { useWizardContext } from '@robo-wizard/react-router' 35 | 36 | export const FirstName = () => { 37 | const wizard = useWizardContext(); 38 | 39 | const onSubmit = (event) => { 40 | event.preventDefault(); 41 | const values = Object.fromEntries(new FormData(event.currentTarget)) 42 | wizard.goToNextStep({ values }) 43 | } 44 | 45 | return ( 46 | <> 47 |

{wizard.currentStep} step

48 |
49 |
50 | 53 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | ) 69 | } 70 | ``` 71 | 72 | Go forth and make the web a bit more magical! ✨ 73 | -------------------------------------------------------------------------------- /packages/react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@robo-wizard/react-router", 3 | "version": "1.0.3", 4 | "description": "RoboWizard components and hooks for react-router", 5 | "main": "./dist/robo-wizard_react-router.umd.js", 6 | "module": "./dist/robo-wizard_react-router.es.js", 7 | "private": false, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./dist/robo-wizard_react-router.es.js", 14 | "require": "./dist/robo-wizard_react-router.umd.js" 15 | } 16 | }, 17 | "types": "dist/index.d.ts", 18 | "typedocMain": "src/index.ts", 19 | "sideEffects": false, 20 | "scripts": { 21 | "start": "vite build --watch", 22 | "build": "vite build", 23 | "test": "vitest", 24 | "prepare": "vite build" 25 | }, 26 | "keywords": [ 27 | "wizard", 28 | "workflow", 29 | "state machine", 30 | "xstate" 31 | ], 32 | "author": "HipsterBrown", 33 | "homepage": "http://robo-wizard.js.org", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/HipsterBrown/robo-wizard.git", 38 | "directory": "packages/react-router" 39 | }, 40 | "dependencies": { 41 | "@robo-wizard/react": "1.1.0", 42 | "robo-wizard": "1.2.0" 43 | }, 44 | "devDependencies": { 45 | "@testing-library/jest-dom": "^5.16.5", 46 | "@testing-library/react": "^13.4.0", 47 | "@types/react": "^18.0.8", 48 | "@types/react-dom": "^18.0.3", 49 | "@vitejs/plugin-react": "^3.0.0", 50 | "jsdom": "^20.0.3", 51 | "react": "^18.1.0", 52 | "react-dom": "^18.1.0", 53 | "react-router": "^6.5.0", 54 | "typescript": "^4.9.4", 55 | "vite": "^4.0.2", 56 | "vite-plugin-dts": "^1.7.1", 57 | "vitest": "^0.26.1" 58 | }, 59 | "peerDependencies": { 60 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0", 61 | "react-router": "^6.0.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/react-router/src/components/Step.tsx: -------------------------------------------------------------------------------- 1 | import { Route, RouteProps } from 'react-router' 2 | import type { StepConfig } from 'robo-wizard' 3 | 4 | /** 5 | * @param props - accept same props as the [Route component from react-router](https://reactrouter.com/docs/en/v6/api#routes-and-route), excepte for `path`, as well as [[StepConfig]] from `robo-wizard` 6 | **/ 7 | export function Step({ name, ...routeProps }: Exclude & StepConfig) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-router/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Step"; 2 | -------------------------------------------------------------------------------- /packages/react-router/src/context/index.tsx: -------------------------------------------------------------------------------- 1 | import { Children, createContext, useContext, useEffect, PropsWithChildren, ReactNode, ReactElement } from 'react'; 2 | import { useNavigate, useRoutes, Navigate, useLocation, RouteObject } from 'react-router'; 3 | import type { RoboWizard, BaseValues, StepConfig } from 'robo-wizard' 4 | import { useWizard } from '@robo-wizard/react' 5 | import { Step } from '../components' 6 | 7 | const WizardContext = createContext(null); 8 | 9 | export type WizardProviderProps = PropsWithChildren<{ 10 | initialValues?: Values; 11 | }> 12 | 13 | function isReactElement(child: ReactNode): child is ReactElement { 14 | return typeof child === 'object' && child !== null && 'type' in child; 15 | } 16 | 17 | /** 18 | * @typeParam Values Optional object to describe values being gathered by the wizard 19 | * @param props 20 | * 21 | * Create a routed wizard experience under a Router from [react-router](https://reactrouter.com) 22 | * 23 | * 24 | * @example Set up the Wizard under a BrowserRouter 25 | * ```tsx 26 | * function App() { 27 | * return ( 28 | * 29 | * initialValues={{ firstName: '', lastName: '' }}> 30 | * } /> 31 | * } /> 32 | * } /> 33 | * 34 | * 35 | * ) 36 | * } 37 | * ``` 38 | * 39 | * 40 | * @example An example step component 41 | * ```tsx 42 | * const First = () => { 43 | * const wizard = useWizardContext(); 44 | * 45 | * const onSubmit = (event: FormEvent) => { 46 | * event.preventDefault(); 47 | * const values = Object.fromEntries(new FormData(event.currentTarget)) 48 | * wizard.goToNextStep({ values }) 49 | * } 50 | * 51 | * return ( 52 | * <> 53 | *

{wizard.currentStep} step

54 | *
55 | *
56 | * 59 | * 67 | *
68 | *
69 | * 70 | * 71 | *
72 | *
73 | * 74 | * ) 75 | * } 76 | * ``` 77 | **/ 78 | export function Wizard({ children, initialValues = {} as Values }: WizardProviderProps) { 79 | if (typeof children !== 'object' || children === null) throw new Error('WizardProvider must have at least one child Step component') 80 | 81 | const steps: Array & RouteObject> = Children.map(children, child => { 82 | if (isReactElement(child) && child.type === Step) { 83 | return { ...child.props as StepConfig, path: child.props.name as string }; 84 | } 85 | return null; 86 | })?.filter(Boolean) ?? []; 87 | const navigate = useNavigate(); 88 | const location = useLocation(); 89 | const wizard = useWizard(steps, initialValues, { 90 | navigate: () => { 91 | navigate(wizard.currentStep); 92 | } 93 | }); 94 | const stepsWithRedirect = steps.concat({ 95 | index: true, 96 | element: (), 97 | path: "/", 98 | name: 'index-redirect' 99 | }); 100 | const step = useRoutes(stepsWithRedirect) 101 | const stepFromLocation = location.pathname.split('/').pop(); 102 | 103 | useEffect(() => { 104 | if (stepFromLocation && stepFromLocation !== wizard.currentStep) { 105 | wizard.sync({ step: stepFromLocation }) 106 | } 107 | }, [stepFromLocation, wizard]) 108 | 109 | return ( 110 | 111 | {step} 112 | 113 | ) 114 | } 115 | 116 | /** 117 | * @typeParam Values - object describing the [[currentValues]] gathered by [[RoboWizard]] 118 | * 119 | * Access the [[RoboWizard]] from the [[Wizard]] Context Provider 120 | **/ 121 | export function useWizardContext() { 122 | const wizard = useContext(WizardContext); 123 | if (wizard === null) throw new Error('useWizardContext must be used within WizardProvider') 124 | return wizard as RoboWizard; 125 | } 126 | -------------------------------------------------------------------------------- /packages/react-router/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./context"; 2 | export * from "./components"; 3 | -------------------------------------------------------------------------------- /packages/react-router/test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { render, screen, fireEvent } from '@testing-library/react' 3 | import { MemoryRouter, useNavigate } from 'react-router' 4 | import { Wizard, Step, useWizardContext } from '../src' 5 | 6 | function TestStep() { 7 | const wizard = useWizardContext() 8 | const navigate = useNavigate() 9 | 10 | return ( 11 | <> 12 |

{wizard.currentStep} step

13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | describe('Wizard', () => { 20 | const subject = () => ( 21 | 22 | 23 | } /> 24 | } /> 25 | } /> 26 | 27 | 28 | ) 29 | 30 | it('navigates through steps', () => { 31 | render(subject()); 32 | 33 | expect(screen.getByText('first step')).toBeTruthy(); 34 | 35 | fireEvent.click(screen.getByText('Next')) 36 | 37 | expect(screen.getByText('second step')).toBeTruthy(); 38 | 39 | fireEvent.click(screen.getByText('Next')) 40 | 41 | expect(screen.getByText('third step')).toBeTruthy(); 42 | 43 | fireEvent.click(screen.getByText('Back')) 44 | 45 | expect(screen.getByText('second step')).toBeTruthy(); 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/react-router/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /packages/react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src", 5 | "./types" 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | "test", 10 | "dist" 11 | ], 12 | "compilerOptions": { 13 | "rootDir": "src", 14 | "lib": [ 15 | "DOM", 16 | "DOM.Iterable", 17 | "ES2019" 18 | ], 19 | "jsx": "react-jsx", 20 | "target": "es2020", 21 | "skipLibCheck": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-router/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import path from 'path' 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | import react from '@vitejs/plugin-react' 7 | 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: path.resolve(__dirname, 'src/index.ts'), 12 | name: 'RoboWizardReactRouter', 13 | fileName: format => `robo-wizard_react-router.${format}.js` 14 | }, 15 | sourcemap: true, 16 | rollupOptions: { 17 | external: ['react', 'react-dom', 'react-router'], 18 | output: { 19 | globals: { 20 | react: 'React', 21 | 'react-dom': 'ReactDom', 22 | 'react-router': 'ReactRouter' 23 | } 24 | } 25 | } 26 | }, 27 | plugins: [dts(), react()], 28 | test: { 29 | globals: true, 30 | environment: 'jsdom', 31 | setupFiles: './test/setup.ts', 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @robo-wizard/react [1.1.0](https://github.com/HipsterBrown/robo-wizard/compare/@robo-wizard/react@1.0.0...@robo-wizard/react@1.1.0) (2022-12-20) 2 | 3 | 4 | ### Features 5 | 6 | * **react:** create Wizard, Step components ([#34](https://github.com/HipsterBrown/robo-wizard/issues/34)) ([fd13b0e](https://github.com/HipsterBrown/robo-wizard/commit/fd13b0e690c48db378ab3ed1a41fba1d77bc54a7)) 7 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # Robo Wizard React 2 | 3 | A library for building multi-step workflows in React. 4 | 5 | ## Getting Started 6 | 7 | ``` 8 | npm i @robo-wizard/react 9 | ``` 10 | 11 | Scaffold a basic wizard experience (bring your own form library as needed): 12 | 13 | ```tsx 14 | const App: React.FC = () => { 15 | const wizard = useWizard(['first-name', 'last-name', 'success'], { firstName: '', lastName: '' }); 16 | 17 | const onSubmit = event => { 18 | event.preventDefault(); 19 | const values = Object.fromEntries(new FormData(event.currentTarget)) 20 | wizard.goToNextStep({ values }); 21 | }; 22 | 23 | return ( 24 |
25 |

{wizard.currentStep} step

26 |
27 | {wizard.currentStep === 'first-name' && ( 28 |
29 | 35 | 42 |
43 | )} 44 | 45 | {wizard.currentStep === 'last-name' && ( 46 |
47 | 53 | 60 |
61 | )} 62 | 63 | {wizard.currentStep === 'success' && ( 64 |
65 |

66 | Welcome {wizard.currentValues.firstName}{' '} 67 | {wizard.currentValues.lastName}! 68 |

69 |
70 | )} 71 | 72 |
73 | 80 | 83 |
84 |
85 |
86 | ); 87 | }; 88 | ``` 89 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@robo-wizard/react", 3 | "version": "1.1.0", 4 | "description": "RoboWizard components and hooks for React", 5 | "main": "./dist/robo-wizard_react.umd.js", 6 | "module": "./dist/robo-wizard_react.es.js", 7 | "private": false, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": "./dist/robo-wizard_react.es.js", 14 | "require": "./dist/robo-wizard_react.umd.js" 15 | } 16 | }, 17 | "types": "dist/index.d.ts", 18 | "typedocMain": "src/index.ts", 19 | "sideEffects": false, 20 | "scripts": { 21 | "start": "vite build --watch", 22 | "build": "vite build", 23 | "test": "vitest", 24 | "prepare": "vite build" 25 | }, 26 | "keywords": [ 27 | "wizard", 28 | "workflow", 29 | "state machine", 30 | "xstate" 31 | ], 32 | "author": "HipsterBrown", 33 | "homepage": "http://robo-wizard.js.org", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/HipsterBrown/robo-wizard.git", 38 | "directory": "packages/react" 39 | }, 40 | "dependencies": { 41 | "robo-wizard": "1.2.0" 42 | }, 43 | "devDependencies": { 44 | "@testing-library/jest-dom": "^5.16.5", 45 | "@testing-library/react": "^13.4.0", 46 | "@types/react": "^18.0.8", 47 | "@types/react-dom": "^18.0.3", 48 | "@vitejs/plugin-react": "^3.0.0", 49 | "jsdom": "^20.0.3", 50 | "react": "^18.1.0", 51 | "react-dom": "^18.1.0", 52 | "typescript": "^4.9.4", 53 | "vite": "^4.0.2", 54 | "vite-plugin-dts": "^1.7.1", 55 | "vitest": "^0.26.1" 56 | }, 57 | "peerDependencies": { 58 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/react/src/contexts/index.tsx: -------------------------------------------------------------------------------- 1 | import { Children, createContext, Fragment, isValidElement, PropsWithChildren, ReactNode, useContext } from "react"; 2 | import type { RoboWizard, StepConfig } from "robo-wizard"; 3 | import { useWizard } from "../hooks"; 4 | 5 | /** 6 | * @param props - accepts same props to match [[StepConfig]] for [[useWizard]], as well as the `element` to display 7 | **/ 8 | export function Step(_props: StepConfig & { element: ReactNode | null }): React.ReactElement | null { 9 | return null; 10 | } 11 | 12 | /** 13 | * @param steps = The Children opaque object passed to components, only Step components will be processed 14 | **/ 15 | export function createStepsFromChildren(steps: ReactNode): Array { 16 | return Children.map(steps, element => { 17 | if (isValidElement(element) === false) return; 18 | if (!element) return; 19 | if (typeof element === 'object' && 'type' in element) { 20 | if (element.type === Fragment) { 21 | return createStepsFromChildren(element.props.children); 22 | } 23 | if (element.type === Step) { 24 | return { 25 | name: element.props.name, 26 | next: element.props.next, 27 | previous: element.props.previous, 28 | element: element.props.element, 29 | } 30 | } 31 | } 32 | return; 33 | })?.flat().filter(Boolean) ?? []; 34 | } 35 | 36 | /** 37 | * @internal 38 | **/ 39 | export const WizardContext = createContext(null); 40 | 41 | export type WizardProviderProps> = PropsWithChildren<{ 42 | initialValues?: Values; 43 | }> 44 | 45 | /** 46 | * @typeParam Values Optional object to describe values being gathered by the wizard 47 | * @param props 48 | * 49 | * Create a wizard experience 50 | * 51 | * 52 | * @example Set up the Wizard with Step components 53 | * ```tsx 54 | * function App() { 55 | * return ( 56 | * initialValues={{ firstName: '', lastName: '' }}> 57 | * } /> 58 | * } /> 59 | * } /> 60 | * 61 | * ) 62 | * } 63 | * ``` 64 | * 65 | * 66 | * @example An example step component 67 | * ```tsx 68 | * const First = () => { 69 | * const wizard = useWizardContext(); 70 | * 71 | * const onSubmit = (event: FormEvent) => { 72 | * event.preventDefault(); 73 | * const values = Object.fromEntries(new FormData(event.currentTarget)) 74 | * wizard.goToNextStep({ values }) 75 | * } 76 | * 77 | * return ( 78 | * <> 79 | *

{wizard.currentStep} step

80 | *
81 | *
82 | * 85 | * 93 | *
94 | *
95 | * 96 | * 97 | *
98 | *
99 | * 100 | * ) 101 | * } 102 | * ``` 103 | **/ 104 | export function Wizard>({ children, initialValues = {} as Values }: WizardProviderProps) { 105 | const steps = createStepsFromChildren(children); 106 | const wizard = useWizard(steps, initialValues) 107 | const step = steps.find(({ name }) => name === wizard.currentStep) 108 | 109 | return ( 110 | 111 | {step?.element} 112 | 113 | ) 114 | } 115 | 116 | /** 117 | * @typeParam Values - object describing the [[currentValues]] gathered by [[RoboWizard]] 118 | * 119 | * Access the [[RoboWizard]] from the [[Wizard]] Context Provider 120 | **/ 121 | export function useWizardContext() { 122 | const wizard = useContext(WizardContext); 123 | if (wizard === null) throw new Error('useWizardContext must be used within WizardProvider') 124 | return wizard as RoboWizard; 125 | } 126 | -------------------------------------------------------------------------------- /packages/react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-wizard"; 2 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-wizard.ts: -------------------------------------------------------------------------------- 1 | import { useState, useReducer } from "react"; 2 | import { createWizard, FlowStep, BaseValues } from "robo-wizard"; 3 | 4 | /** 5 | * @typeParam Values Generic type for object of values being gathered through the wizard steps 6 | * @param steps Configuration of steps for the wizard, see [[FlowStep]] 7 | * @param initialValues Optional object with intial values to use when starting the wizard 8 | * @param actions Optional object with navigate field with a function to be called when entering a step 9 | * 10 | * @example Basic usage 11 | * ```tsx 12 | * 13 | * const App: React.FC = () => { 14 | * const wizard = useWizard(['first', 'second', 'third']); 15 | 16 | * const onSubmit: React.FormEventHandler = event => { 17 | * event.preventDefault(); 18 | * const values = Object.fromEntries(new FormData(event.currentTarget)) 19 | * wizard.goToNextStep({ values }); 20 | * }; 21 | 22 | * return ( 23 | *
24 | *

{wizard.currentStep} step

25 | *
26 | * {wizard.currentStep === 'first' && ( 27 | *
28 | * 34 | * 41 | *
42 | * )} 43 | 44 | * {wizard.currentStep === 'second' && ( 45 | *
46 | * 52 | * 59 | *
60 | * )} 61 | 62 | * {wizard.currentStep === 'third' && ( 63 | *
64 | *

65 | * Welcome {wizard.currentValues.firstName}{' '} 66 | * {wizard.currentValues.lastName}! 67 | *

68 | *
69 | * )} 70 | 71 | *
72 | * 79 | * 82 | *
83 | *
84 | *
85 | * ); 86 | * }; 87 | * ``` 88 | * */ 89 | export function useWizard( 90 | steps: FlowStep[], 91 | initialValues: Values = {} as Values, 92 | actions: Parameters[2] = {} 93 | ) { 94 | const [_, refreshState] = useReducer((s: boolean) => !s, false); 95 | const [currentWizard] = useState(() => { 96 | const wizard = createWizard(steps, initialValues, actions); 97 | wizard.start(refreshState); 98 | return wizard; 99 | }); 100 | return currentWizard; 101 | } 102 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hooks/index"; 2 | export * from "./contexts/index"; 3 | -------------------------------------------------------------------------------- /packages/react/test/hooks/use-wizard.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { act, renderHook } from '@testing-library/react' 3 | import { useWizard } from '../../src/hooks/use-wizard' 4 | 5 | describe('useWizard', () => { 6 | let steps = ['first', 'second', 'third'] 7 | let initialValues: { test?: string } = {} 8 | 9 | const subject = () => useWizard(steps, initialValues) 10 | 11 | it('should progress through the steps', () => { 12 | const { result } = renderHook(subject) 13 | 14 | expect(result.current.currentStep).toBe('first') 15 | 16 | act(() => { 17 | result.current.goToNextStep() 18 | }) 19 | 20 | expect(result.current.currentStep).toBe('second') 21 | }) 22 | 23 | it('should gather values', () => { 24 | const { result } = renderHook(subject) 25 | 26 | expect(result.current.currentValues).toMatchObject({}) 27 | 28 | act(() => { 29 | result.current.goToNextStep({ values: { test: 'values' } }) 30 | }) 31 | 32 | expect(result.current.currentValues).toMatchObject({ test: 'values' }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/react/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src", 5 | "./types" 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | "test", 10 | "dist" 11 | ], 12 | "compilerOptions": { 13 | "rootDir": "src", 14 | "lib": [ 15 | "DOM", 16 | "DOM.Iterable", 17 | "ES2019" 18 | ], 19 | "jsx": "react-jsx", 20 | "target": "es2020", 21 | "skipLibCheck": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import path from 'path' 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | import react from '@vitejs/plugin-react' 7 | 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: path.resolve(__dirname, 'src/index.ts'), 12 | name: 'RoboWizardReact', 13 | fileName: format => `robo-wizard_react.${format}.js` 14 | }, 15 | sourcemap: true, 16 | rollupOptions: { 17 | external: ['react', 'react-dom'], 18 | output: { 19 | globals: { 20 | react: 'React', 21 | 'react-dom': 'ReactDom' 22 | } 23 | } 24 | } 25 | }, 26 | plugins: [dts(), react()], 27 | test: { 28 | globals: true, 29 | environment: 'jsdom', 30 | setupFiles: './test/setup.ts', 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages: true 2 | packages: 3 | - 'docs' 4 | - 'packages/*' 5 | - 'examples/*' 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16-strictest-esm/tsconfig.json", 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "declaration": true, 6 | "sourceMap": true, 7 | "baseUrl": "./", 8 | "moduleResolution": "node", 9 | "skipLibCheck": true 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------