diff --git a/examples/react-context/app.tsx b/examples/react-context/app.tsx new file mode 100644 index 0000000..a24e5f9 --- /dev/null +++ b/examples/react-context/app.tsx @@ -0,0 +1,158 @@ +import { createRoot } from 'react-dom/client'; +import { Step, Wizard, useWizardContext } from '@robo-wizard/react'; + +type Values = { + firstName?: string; + lastName?: string; +}; + +const useWizardSubmit = () => { + const wizard = useWizardContext() + + const onSubmit: React.FormEventHandler = event => { + event.preventDefault(); + const values = Object.fromEntries(new FormData(event.currentTarget)) + wizard.goToNextStep({ values }); + }; + + return onSubmit; +} + +const First: React.FC = () => { + const wizard = useWizardContext() + const onSubmit = useWizardSubmit(); + + return ( + <> +

+ {wizard.currentStep} step +

+ +
+
+ + +
+
+ + +
+
+ + ) +} + +const Second: React.FC = () => { + const wizard = useWizardContext() + const onSubmit = useWizardSubmit(); + + return ( + <> +

+ {wizard.currentStep} step +

+ +
+
+ + +
+
+ + +
+
+ + ) +} + +const Third: React.FC = () => { + const wizard = useWizardContext() + const onSubmit = useWizardSubmit() + + return ( + <> +

+ {wizard.currentStep} step +

+ +
+
+

+ Welcome {wizard.currentValues.firstName}{' '} + {wizard.currentValues.lastName}! +

+
+
+ + +
+
+ + ) +} + +const App: React.FC = () => { + return ( +
+

Robo Wizard w/ React

+ + } /> + } /> + } /> + +
+ ); +}; + +createRoot(document.getElementById('app')).render(); diff --git a/examples/react-context/index.html b/examples/react-context/index.html new file mode 100644 index 0000000..0bafc7e --- /dev/null +++ b/examples/react-context/index.html @@ -0,0 +1,16 @@ + + + + + + + Robo-Wizard Example App + + + + +
+ + + + diff --git a/examples/react-context/package.json b/examples/react-context/package.json new file mode 100644 index 0000000..c78bf72 --- /dev/null +++ b/examples/react-context/package.json @@ -0,0 +1,21 @@ +{ + "name": "robo-wizard-react-context-example", + "version": "0.0.0-example", + "description": "An example usage of robo-wizard with React Context", + "scripts": { + "start": "vite" + }, + "author": "HipsterBrown", + "license": "Apache-2.0", + "dependencies": { + "@robo-wizard/react": "workspace:*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.8", + "@types/react-dom": "^18.0.3", + "@vitejs/plugin-react": "^1.3.2", + "vite": "^2.9.8" + } +} diff --git a/examples/react-context/tsconfig.json b/examples/react-context/tsconfig.json new file mode 100644 index 0000000..96d5ac3 --- /dev/null +++ b/examples/react-context/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "lib": [ + "DOM", + "DOM.Iterable", + "es2020" + ] + } +} diff --git a/examples/react-context/vite.config.js b/examples/react-context/vite.config.js new file mode 100644 index 0000000..b1b5f91 --- /dev/null +++ b/examples/react-context/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}) diff --git a/packages/react/src/contexts/index.tsx b/packages/react/src/contexts/index.tsx new file mode 100644 index 0000000..3676e6a --- /dev/null +++ b/packages/react/src/contexts/index.tsx @@ -0,0 +1,125 @@ +import { Children, createContext, Fragment, isValidElement, PropsWithChildren, ReactNode, useContext } from "react"; +import type { RoboWizard, StepConfig } from "robo-wizard"; +import { useWizard } from "../hooks"; + +/** + * @param props - accepts same props to match [[StepConfig]] for [[useWizard]], as well as the `element` to display + **/ +export function Step(_props: StepConfig & { element: ReactNode | null }): React.ReactElement | null { + return null; +} + +/** + * @param steps = The Children opaque object passed to components, only Step components will be processed + **/ +export function createStepsFromChildren(steps: ReactNode): Array { + return Children.map(steps, element => { + if (isValidElement(element) === false) return; + if (!element) return; + if (typeof element === 'object' && 'type' in element) { + if (element.type === Fragment) { + return createStepsFromChildren(element.props.children); + } + if (element.type === Step) { + return { + name: element.props.name, + next: element.props.next, + previous: element.props.previous, + element: element.props.element, + } + } + } + return; + })?.flat().filter(Boolean) ?? []; +} + +/** + * @internal + **/ +export const WizardContext = createContext(null); + +export type WizardProviderProps> = PropsWithChildren<{ + initialValues?: Values; +}> + +/** + * @typeParam Values Optional object to describe values being gathered by the wizard + * @param props + * + * Create a wizard experience + * + * + * @example Set up the Wizard with Step components + * ```tsx + * function App() { + * return ( + * initialValues={{ firstName: '', lastName: '' }}> + * } /> + * } /> + * } /> + * + * ) + * } + * ``` + * + * + * @example An example step component + * ```tsx + * const First = () => { + * const wizard = useWizardContext(); + * + * const onSubmit = (event: FormEvent) => { + * event.preventDefault(); + * const values = Object.fromEntries(new FormData(event.currentTarget)) + * wizard.goToNextStep({ values }) + * } + * + * return ( + * <> + *

{wizard.currentStep} step

+ *
+ *
+ * + * + *
+ *
+ * + * + *
+ *
+ * + * ) + * } + * ``` + **/ +export function Wizard>({ children, initialValues = {} as Values }: WizardProviderProps) { + const steps = createStepsFromChildren(children); + const wizard = useWizard(steps, initialValues) + const step = steps.find(({ name }) => name === wizard.currentStep) + + return ( + + {step?.element} + + ) +} + +/** + * @typeParam Values - object describing the [[currentValues]] gathered by [[RoboWizard]] + * + * Access the [[RoboWizard]] from the [[Wizard]] Context Provider + **/ +export function useWizardContext() { + const wizard = useContext(WizardContext); + if (wizard === null) throw new Error('useWizardContext must be used within WizardProvider') + return wizard as RoboWizard; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cdeb92b..7e1f26a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1 +1,2 @@ export * from "./hooks/index"; +export * from "./contexts/index"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7791b62..03185a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,7 @@ importers: eslint-config-airbnb-base: 15.0.0_myxbwluo6p3kuxjcyp342zygci eslint-config-prettier: 8.5.0_eslint@8.14.0 eslint-import-resolver-typescript: 2.7.1_myxbwluo6p3kuxjcyp342zygci - eslint-plugin-import: 2.26.0_eslint@8.14.0 + eslint-plugin-import: 2.26.0_iqjbwfvwlbjep2xhectfqm3l6a eslint-plugin-prettier: 4.0.0_mzpligoj26dazigcet37nxg2zy prettier: 2.6.2 semantic-release: 19.0.2 @@ -129,6 +129,25 @@ importers: '@vitejs/plugin-react': 1.3.2 vite: 2.9.8 + examples/react-context: + specifiers: + '@robo-wizard/react': workspace:* + '@types/react': ^18.0.8 + '@types/react-dom': ^18.0.3 + '@vitejs/plugin-react': ^1.3.2 + react: ^18.0.0 + react-dom: ^18.0.0 + vite: ^2.9.8 + dependencies: + '@robo-wizard/react': link:../../packages/react + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + devDependencies: + '@types/react': 18.0.8 + '@types/react-dom': 18.0.3 + '@vitejs/plugin-react': 1.3.2 + vite: 2.9.8 + examples/react-router: specifiers: '@robo-wizard/react-router': workspace:* @@ -716,7 +735,7 @@ packages: resolution: {integrity: sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.10 + '@babel/types': 7.20.5 dev: true /@babel/helper-environment-visitor/7.18.9: @@ -734,7 +753,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.16.7 - '@babel/types': 7.17.10 + '@babel/types': 7.20.5 dev: true /@babel/helper-function-name/7.19.0: @@ -748,7 +767,7 @@ packages: resolution: {integrity: sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.10 + '@babel/types': 7.20.5 dev: true /@babel/helper-hoist-variables/7.18.6: @@ -854,7 +873,7 @@ packages: resolution: {integrity: sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.10 + '@babel/types': 7.20.5 dev: true /@babel/helper-simple-access/7.20.2: @@ -873,7 +892,7 @@ packages: resolution: {integrity: sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.10 + '@babel/types': 7.20.5 dev: true /@babel/helper-split-export-declaration/7.18.6: @@ -957,6 +976,8 @@ packages: resolution: {integrity: sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==} engines: {node: '>=6.0.0'} hasBin: true + dependencies: + '@babel/types': 7.20.5 /@babel/parser/7.20.5: resolution: {integrity: sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==} @@ -5783,6 +5804,11 @@ packages: /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: true @@ -6446,7 +6472,7 @@ packages: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.14.0 - eslint-plugin-import: 2.26.0_eslint@8.14.0 + eslint-plugin-import: 2.26.0_iqjbwfvwlbjep2xhectfqm3l6a object.assign: 4.1.2 object.entries: 1.1.5 semver: 6.3.0 @@ -6466,6 +6492,8 @@ packages: dependencies: debug: 3.2.7 resolve: 1.22.0 + transitivePeerDependencies: + - supports-color dev: true /eslint-import-resolver-typescript/2.7.1_myxbwluo6p3kuxjcyp342zygci: @@ -6477,7 +6505,7 @@ packages: dependencies: debug: 4.3.4 eslint: 8.14.0 - eslint-plugin-import: 2.26.0_eslint@8.14.0 + eslint-plugin-import: 2.26.0_iqjbwfvwlbjep2xhectfqm3l6a glob: 7.2.0 is-glob: 4.0.3 resolve: 1.22.0 @@ -6486,27 +6514,51 @@ packages: - supports-color dev: true - /eslint-module-utils/2.7.3: + /eslint-module-utils/2.7.3_5zeicuv6z6i32arielnnarwece: resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true dependencies: + '@typescript-eslint/parser': 5.21.0_5wsz2tb7zzudmaqxfve53vbauu debug: 3.2.7 + eslint-import-resolver-node: 0.3.6 + eslint-import-resolver-typescript: 2.7.1_myxbwluo6p3kuxjcyp342zygci find-up: 2.1.0 + transitivePeerDependencies: + - supports-color dev: true - /eslint-plugin-import/2.26.0_eslint@8.14.0: + /eslint-plugin-import/2.26.0_iqjbwfvwlbjep2xhectfqm3l6a: resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: + '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: + '@typescript-eslint/parser': 5.21.0_5wsz2tb7zzudmaqxfve53vbauu array-includes: 3.1.4 array.prototype.flat: 1.3.0 debug: 2.6.9 doctrine: 2.1.0 eslint: 8.14.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.3 + eslint-module-utils: 2.7.3_5zeicuv6z6i32arielnnarwece has: 1.0.3 is-core-module: 2.9.0 is-glob: 4.0.3 @@ -6514,6 +6566,10 @@ packages: object.values: 1.1.5 resolve: 1.22.0 tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color dev: true /eslint-plugin-prettier/4.0.0_mzpligoj26dazigcet37nxg2zy: @@ -8836,7 +8892,7 @@ packages: dev: true /object-assign/4.1.1: - resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} /object-inspect/1.12.0: @@ -11804,7 +11860,7 @@ packages: local-pkg: 0.4.1 tinypool: 0.1.3 tinyspy: 0.3.2 - vite: 2.9.7 + vite: 2.9.8 transitivePeerDependencies: - less - sass @@ -11837,7 +11893,7 @@ packages: local-pkg: 0.4.1 tinypool: 0.1.3 tinyspy: 0.3.2 - vite: 2.9.7 + vite: 2.9.8 transitivePeerDependencies: - less - sass