Skip to content

Commit

Permalink
feat(react): create Wizard, Step components (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
HipsterBrown authored Dec 20, 2022
1 parent 42da2b7 commit fd13b0e
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 14 deletions.
158 changes: 158 additions & 0 deletions examples/react-context/app.tsx
Original file line number Diff line number Diff line change
@@ -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<Values>()

const onSubmit: React.FormEventHandler<HTMLFormElement> = event => {
event.preventDefault();
const values = Object.fromEntries(new FormData(event.currentTarget))
wizard.goToNextStep({ values });
};

return onSubmit;
}

const First: React.FC = () => {
const wizard = useWizardContext<Values>()
const onSubmit = useWizardSubmit();

return (
<>
<p className="font-semibold mb-8 underline uppercase">
{wizard.currentStep} step
</p>

<form onSubmit={onSubmit} className="mb-4">
<div className="mb-6">
<label
htmlFor="firstName"
id="firstName-label"
className="block mb-2"
>
First Name:
</label>
<input
className="border-2 border-solid border-gray-600 px-4 py-2"
type="text"
name="firstName"
id="firstName"
aria-label="firstName-label"
defaultValue={wizard.currentValues.firstName}
/>
</div>
<div className="flex w-32 justify-between">
<button
type="button"
onClick={() => wizard.goToPreviousStep()}
className="p-3 mr-4"
>
Previous
</button>
<button type="submit" className="py-3 px-8 border-2 border-gray-900">
Next
</button>
</div>
</form>
</>
)
}

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

return (
<>
<p className="font-semibold mb-8 underline uppercase">
{wizard.currentStep} step
</p>

<form onSubmit={onSubmit} className="mb-4">
<div className="mb-6">
<label
htmlFor="lastName"
id="lastName-label"
className="block mb-2"
>
Last Name:
</label>
<input
className="border-2 border-solid border-gray-600 px-4 py-2"
type="text"
name="lastName"
id="lastName"
aria-label="lastName-label"
defaultValue={wizard.currentValues.lastName}
/>
</div>
<div className="flex w-32 justify-between">
<button
type="button"
onClick={() => wizard.goToPreviousStep()}
className="p-3 mr-4"
>
Previous
</button>
<button type="submit" className="py-3 px-8 border-2 border-gray-900">
Next
</button>
</div>
</form>
</>
)
}

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

return (
<>
<p className="font-semibold mb-8 underline uppercase">
{wizard.currentStep} step
</p>

<form onSubmit={onSubmit} className="mb-4">
<div className="mb-6">
<p className="text-green-600">
Welcome {wizard.currentValues.firstName}{' '}
{wizard.currentValues.lastName}!
</p>
</div>
<div className="flex w-32 justify-between">
<button
type="button"
onClick={() => wizard.goToPreviousStep()}
className="p-3 mr-4"
>
Previous
</button>
<button type="submit" className="py-3 px-8 border-2 border-gray-900">
Next
</button>
</div>
</form>
</>
)
}

const App: React.FC = () => {
return (
<div className="px-5">
<h1 className="text-2xl font-bold">Robo Wizard w/ React</h1>
<Wizard>
<Step name="first" element={<First />} />
<Step name="second" element={<Second />} />
<Step name="third" element={<Third />} />
</Wizard>
</div>
);
};

createRoot(document.getElementById('app')).render(<App />);
16 changes: 16 additions & 0 deletions examples/react-context/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robo-Wizard Example App</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
</head>

<body>
<div id="app"></div>
<script src="./app.tsx" type="module"></script>
</body>

</html>
21 changes: 21 additions & 0 deletions examples/react-context/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
10 changes: 10 additions & 0 deletions examples/react-context/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": [
"DOM",
"DOM.Iterable",
"es2020"
]
}
}
7 changes: 7 additions & 0 deletions examples/react-context/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
})
125 changes: 125 additions & 0 deletions packages/react/src/contexts/index.tsx
Original file line number Diff line number Diff line change
@@ -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<StepConfig & { element: ReactNode }> {
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 | RoboWizard>(null);

export type WizardProviderProps<Values extends Record<string, unknown>> = PropsWithChildren<{
initialValues?: Values;
}>

/**
* @typeParam Values Optional object to describe values being gathered by the wizard
* @param props
*
* Create a wizard experience
*
*
* @example <caption>Set up the Wizard with Step components</caption>
* ```tsx
* function App() {
* return (
* <Wizard<Values> initialValues={{ firstName: '', lastName: '' }}>
* <Step name="first" element={<First />} />
* <Step name="second" element={<Second />} />
* <Step name="third" element={<Third />} />
* </Wizard>
* )
* }
* ```
*
*
* @example <caption>An example step component</caption>
* ```tsx
* const First = () => {
* const wizard = useWizardContext<Values>();
*
* const onSubmit = (event: FormEvent<HTMLFormElement>) => {
* event.preventDefault();
* const values = Object.fromEntries(new FormData(event.currentTarget))
* wizard.goToNextStep({ values })
* }
*
* return (
* <>
* <p>{wizard.currentStep} step</p>
* <form onSubmit={onSubmit}>
* <div>
* <label htmlFor="firstName" id="firstName-label">
* First Name:
* </label>
* <input
* type="text"
* name="firstName"
* id="firstName"
* aria-labelledby="firstName-label"
* defaultValue={wizard.currentValues.firstName}
* autoFocus={true}
* />
* </div>
* <div>
* <button type="button" onClick={() => wizard.goToPreviousStep()} role="link">Previous</button>
* <button type="submit">Next</button>
* </div>
* </form>
* </>
* )
* }
* ```
**/
export function Wizard<Values extends Record<string, unknown>>({ children, initialValues = {} as Values }: WizardProviderProps<Values>) {
const steps = createStepsFromChildren(children);
const wizard = useWizard<Values>(steps, initialValues)
const step = steps.find(({ name }) => name === wizard.currentStep)

return (
<WizardContext.Provider value={Object.create(wizard)}>
{step?.element}
</WizardContext.Provider>
)
}

/**
* @typeParam Values - object describing the [[currentValues]] gathered by [[RoboWizard]]
*
* Access the [[RoboWizard]] from the [[Wizard]] Context Provider
**/
export function useWizardContext<Values extends object>() {
const wizard = useContext(WizardContext);
if (wizard === null) throw new Error('useWizardContext must be used within WizardProvider')
return wizard as RoboWizard<Values>;
}
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./hooks/index";
export * from "./contexts/index";
Loading

0 comments on commit fd13b0e

Please sign in to comment.