├── .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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
37 |
38 |
Robo Wizard w/ Vue
39 |
40 |
{{ wizard.currentStep }} step
41 |
42 |
76 |
77 |
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 |
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 | *
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 |
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 | *
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 | *
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 |
--------------------------------------------------------------------------------