Skip to content

Commit 38ffc11

Browse files
committed
DeepPartial, docs updates
1 parent 9e55087 commit 38ffc11

19 files changed

+111
-70
lines changed

README.md

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
# react-multi-page-form
1+
![React Multi Page Form](https://stutrek.github.io/react-multi-page-form/Logo.svg "")
22

33
This is a tool for managing the sequence and flow of multi-page workflows. Given a long series of screens, it can put them in the proper order makes it easy to show and hide screens based on previous input.
44

55
Workflows can be composed, allowing you to reuse parts of a flow.
66

77
This should be used in combination with a component library and validation schema to improve your form management.
88

9-
All of the examples use react-hook-form, but any library could be used.
9+
An integration with React Hook Form is provided, but the base could be used with any library.
10+
11+
[View the docs](https://stutrek.github.io/react-multi-page-form/)
1012

1113
## Basic Usage
1214

@@ -61,6 +63,9 @@ const MyMultiPageForm = () => {
6163
</>);
6264
};
6365
```
66+
[View the docs](https://stutrek.github.io/react-multi-page-form/)
67+
68+
6469
### Pages and Sequences
6570

6671
A **page** represents a single screen that will be shown to the user as part of this multi-step form. It can have as many fields or as few as you'd like. It can even have logic to show and hide fields built in.
@@ -71,18 +76,20 @@ A **sequence** is an array of pages and sequences that represent a specific flow
7176
type FormPage<DataT, ComponentProps, ErrorList> = {
7277
id: string;
7378
// determines whether or not this page is needed
74-
isRequired?: (data: Partial<DataT>) => boolean | undefined
79+
isRequired?: (data: DeepPartial<DataT>) => boolean | undefined
7580
// determines if the page is already complete
76-
isComplete: (data: Partial<DataT>) => boolean;
81+
isComplete: (data: DeepPartial<DataT>) => boolean;
7782
// determines if this should be a final step in the flow
78-
isFinal?: (data: Partial<DataT>) => boolean;
83+
isFinal?: (data: DeepPartial<DataT>) => boolean;
84+
// if you need to break the flow of the sequence, this makes that possible
85+
alternateNextPage?: (data: DeepPartial<DataT>) => Boolean
7986
// Mounted inputs are automatically validated.
8087
// If you need specific validation logic, put it here.
81-
validate?: (data: Partial<DataT>) => ErrorList | undefined;
88+
validate?: (data: DeepPartial<DataT>) => ErrorList | undefined;
8289
// callback on arrival
83-
onArrive?: (data: Partial<DataT>) => void;
90+
onArrive?: (data: DeepPartial<DataT>) => void;
8491
// callback on departure
85-
onExit?: (data: Partial<DataT>) => Promise<void> | void;
92+
onExit?: (data: DeepPartial<DataT>) => Promise<void> | void;
8693
// the component that will be rendered
8794
Component: (props: ComponentProps) => JSX.Element;
8895
};
@@ -96,6 +103,9 @@ export type FormSequence<DataT, ComponentProps, ErrorList> = {
96103
};
97104
```
98105

106+
[View the docs](https://stutrek.github.io/react-multi-page-form/)
107+
108+
99109
## A More Complete Example
100110

101111
```typescript
@@ -163,5 +173,6 @@ export function MyMultiPageForm() {
163173
);
164174
}
165175
```
176+
[View the docs](https://stutrek.github.io/react-multi-page-form/)
166177

167178

docs/src/app/Intro.mdx

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ This example features a short form that displays additional pages only if the us
1010

1111
## Integration within the Ecosystem
1212

13-
<img src="/how%20it%20fits%20in.png" alt="" className="mx-auto w-[32rem]" />
13+
<img src="/react-multi-page-form/how%20it%20fits%20in.png" alt="" className="mx-auto w-[32rem]" />
1414

1515
React Multi Page Form seamlessly integrates into the React ecosystem by focusing exclusively on sequence and flow management. It doesn’t handle UI rendering or data persistence, allowing developers to pair it with their preferred component libraries and state management tools. While it includes integration with React Hook Form, it can also work with Formik, Final Form, or any other React form library.
1616

1717
## Why Use React Multi Page Form?
1818

1919
- **Streamline Long Forms:** Skip unnecessary pages dynamically based on user inputs.
20+
- **Easy Testing:** Quickly see the list of pages used for any set of data, and render all pages at once.
2021
- **Simplified Navigation:** Implement next and previous buttons effortlessly.
2122
- **Dynamic Form Testing:** Quickly test and determine which forms are displayed to users depending on their selections.
2223
- **Resume Functionality:** Enable users to pick up where they left off, improving completion rates.
2324
- **Composable Workflows:** Combine multiple workflows into a single cohesive process, promoting reusability and modularity in form management.
2425
- **Great for AI:** Clear separation of concerns makes it easy for AI to generate the first draft of your sequences and forms.
2526

26-
Ready to simplify your multi-page forms? See the [getting started page](/docs) or check out the [GitHub repository](https://github.com/stutrek/react-multi-page-form).
27+
Ready to simplify your multi-page forms? See the [getting started page](/react-multi-page-form/docs) or check out the [GitHub repository](https://github.com/stutrek/react-multi-page-form).

docs/src/app/docs/api/page.mdx

+8-8
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ type HookFormPage<DataT, ComponentProps = { hookForm: UseFormReturn }> = {
2020
// these three are required
2121
id: string; // Unique identifier for the form page.
2222
Component: (props: ComponentProps) => JSX.Element; // React component to render the form page.
23-
isComplete: (data: Partial<DataT>) => boolean; // Checks if the user has already filled out this page.
23+
isComplete: (data: DeepPartial<DataT>) => boolean; // Checks if the user has already filled out this page.
2424

2525
// these manage the sequence
26-
isFinal?: (data: Partial<DataT>) => boolean; // return true if this should be the final page of the form.
27-
isRequired?: (data: Partial<DataT>) => boolean | undefined; // Determines if this page is needed based on form data. Default: () => true
28-
validate?: (data: Partial<DataT>) => Promise<FieldErrors | undefined>; // Determines whether or not to continue.
29-
alternateNextPage: (data: Partial<DataT>) => pageId; // Useful if you need to override the default order of pages
26+
isFinal?: (data: DeepPartial<DataT>) => boolean; // return true if this should be the final page of the form.
27+
isRequired?: (data: DeepPartial<DataT>) => boolean | undefined; // Determines if this page is needed based on form data. Default: () => true
28+
validate?: (data: DeepPartial<DataT>) => Promise<FieldErrors | undefined>; // Determines whether or not to continue.
29+
alternateNextPage?: (data: DeepPartial<DataT>) => pageId; // Useful if you need to override the default order of pages
3030

3131
// event handlers
32-
onArrive?: (data: Partial<DataT>) => void; // Function to execute upon arriving at this page.
33-
onExit?: (data: Partial<DataT>) => Promise<void> | void; // Function to execute when exiting the page.
32+
onArrive?: (data: DeepPartial<DataT>) => void; // Function to execute upon arriving at this page.
33+
onExit?: (data: DeepPartial<DataT>) => Promise<void> | void; // Function to execute when exiting the page.
3434
};
3535
```
3636

@@ -63,7 +63,7 @@ A sequence contains pages or more sequences that represent a single workflow. Th
6363
export type FormSequence<DataT, ComponentProps, ErrorList> = {
6464
id: string; // Unique identifier for the form sequence.
6565
pages: HookFormSequenceChild[]; // A SequenceChild is either a FormPage or a FormSequence.
66-
isRequired?: (data: Partial<DataT>) => boolean | undefined; // Determines if the sequence is needed based on form data.
66+
isRequired?: (data: DeepPartial<DataT>) => boolean | undefined; // Determines if the sequence is needed based on form data.
6767
};
6868
```
6969

docs/src/app/docs/page.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ To allow consistent typing throughout the library, you need a single type for yo
2929

3030
### Schema
3131

32-
This is the shape the data takes for the entirety of the form. Not all of it will be on the screen at the same time, and you'll have a chance to save data when changing pages. This data will be Partial'd, since it won't all be available until the user is done providing it.
32+
This is the shape the data takes for the entirety of the form. Not all of it will be on the screen at the same time, and you'll have a chance to save data when changing pages. This data will be DeepPartial'd, since it won't all be available until the user is done providing it.
3333

3434
```typescript
3535
type MySchema = {

docs/src/app/header.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function Header({ showLogo = true }: HeaderProps) {
1616
<li>
1717
<Link href="/">
1818
<img
19-
src="./Logo.svg"
19+
src="/react-multi-page-form/Logo.svg"
2020
alt="React Multi Page Form"
2121
className="h-7"
2222
/>

docs/src/app/layout.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ export default function RootLayout({
99
}>) {
1010
return (
1111
<html lang="en">
12-
<head>
13-
<base href="/react-multi-page-form/" />
14-
</head>
1512
<body className="antialiased">{children}</body>
13+
<div className="container max-w-3xl mx-auto p-4 text-sm text-gray-400">
14+
<hr className="mb-3" />
15+
&copy; 2024 Stu Kabakoff
16+
</div>
1617
</html>
1718
);
1819
}

docs/src/app/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default function () {
55
<div className="container max-w-3xl mx-auto prose p-4">
66
<div className="text-center">
77
<img
8-
src="./Logo.svg"
8+
src="/react-multi-page-form/Logo.svg"
99
alt="React Multi Page Form"
1010
className="h-13"
1111
/>

docs/src/mdx-components.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import type { MDXComponents } from 'mdx/types';
22

33
export function useMDXComponents(components: MDXComponents): MDXComponents {
44
return {
5+
a: (props) => {
6+
let href = props.href as string;
7+
if (href?.startsWith('/')) {
8+
href = `/react-multi-page-form${href}`;
9+
}
10+
return <a href={href} {...props} />;
11+
},
512
...components,
613
};
714
}

package.json

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
"version": "0.1.0",
44
"description": "Tools to handle multi-page forms",
55
"main": "dist/index.js",
6-
"files": [
7-
"dist"
8-
],
6+
"files": ["dist"],
97
"scripts": {
108
"build": "tsc",
119
"build:watch": "tsc -w",
@@ -16,10 +14,7 @@
1614
"test": "jest",
1715
"test:watch": "jest --watch"
1816
},
19-
"keywords": [
20-
"forms",
21-
"multi-page"
22-
],
17+
"keywords": ["forms", "multi-page"],
2318
"author": "Stu Kabakoff",
2419
"license": "MIT",
2520
"devDependencies": {

src/__tests__/initialization.spec.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { renderHook } from '@testing-library/react';
33
import { useMultiPageFormBase } from '../base';
4-
import { StartingPage } from '../types';
4+
import { type DeepPartial, StartingPage } from '../types';
55

66
describe('useMultiPageFormBase - Initialization Tests', () => {
77
const pages = [
@@ -285,7 +285,7 @@ describe('useMultiPageFormBase - Initialization Tests', () => {
285285
{
286286
id: 'page2',
287287
isComplete: () => false,
288-
isRequired: (data: Partial<{ skipPage2: boolean }>) =>
288+
isRequired: (data: DeepPartial<{ skipPage2: boolean }>) =>
289289
!data.skipPage2,
290290
Component: () => <div />,
291291
},

src/__tests__/sequenceFollower.spec.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React from 'react';
44
import { followSequence } from '../testUtils/followSequence';
5-
import type { SequenceChild } from '../types';
5+
import type { DeepPartial, SequenceChild } from '../types';
66

77
describe('followSequence', () => {
88
beforeEach(() => {
@@ -142,14 +142,14 @@ describe('followSequence', () => {
142142
const pages = [
143143
{
144144
id: 'page1',
145-
isRequired: (data: Partial<typeof formData>) =>
145+
isRequired: (data: DeepPartial<typeof formData>) =>
146146
data.includePage1,
147147
isComplete: () => false,
148148
Component: () => <div />,
149149
},
150150
{
151151
id: 'page2',
152-
alternateNextPage: (data: Partial<typeof formData>) =>
152+
alternateNextPage: (data: DeepPartial<typeof formData>) =>
153153
data.skipToPage4 ? 'page4' : undefined,
154154
isComplete: () => false,
155155
Component: () => <div />,
@@ -209,7 +209,7 @@ describe('followSequence', () => {
209209
const pages = [
210210
{
211211
id: 'page1',
212-
isFinal: (data: Partial<typeof formData>) => !!data.endHere,
212+
isFinal: (data: DeepPartial<typeof formData>) => !!data.endHere,
213213
isComplete: () => false,
214214
Component: () => <div />,
215215
},

src/base.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
useState,
77
} from 'react';
88
import { StartingPage } from './types';
9-
import type { FormPage, MultiPageFormParams } from './types';
9+
import type { DeepPartial, FormPage, MultiPageFormParams } from './types';
1010
import { flattenPages, useCallbackRef } from './utils';
1111

1212
function isRequired<DataT, Page extends FormPage<DataT, any, any>>(
1313
page: Page,
14-
data: DataT,
14+
data: DeepPartial<DataT>,
1515
) {
1616
if (page.isRequired === undefined || page.isRequired(data) !== false) {
1717
return true;

src/hookForm.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
DeepPartial,
23
FieldErrors,
34
FieldValues,
45
Path,
@@ -124,6 +125,7 @@ export const useMultiPageHookForm = <
124125
const { trigger, reset, control } = hookForm;
125126

126127
const multiPageForm = useMultiPageFormBase({
128+
// @ts-ignore
127129
getCurrentData: () => hookForm.getValues(),
128130
onBeforePageChange: async (data, page) => {
129131
if (onBeforePageChange) {

src/testUtils/FormPagesTester.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import type { FormPage } from '../types'; // Adjust the import path as needed
2+
import type { DeepPartial, FormPage } from '../types'; // Adjust the import path as needed
33
import { FormPageMultipleTester } from './components/FormPageMultipleTester'; // Adjust the import path as needed
44
import type { DefaultValues, FieldValues } from 'react-hook-form';
55

@@ -14,8 +14,8 @@ import type { DefaultValues, FieldValues } from 'react-hook-form';
1414
*
1515
* @param {object} props - The component props.
1616
* @param {HookFormPage<DataT, ComponentProps>[]} props.pages - An array of `FormPage` objects to be tested.
17-
* @param {Partial<DataT>} props.sampleData - Sample data to use for testing.
18-
* @param {Partial<ComponentProps>} [props.additionalProps] - Optional additional props to pass to the components.
17+
* @param {DeepPartial<DataT>} props.sampleData - Sample data to use for testing.
18+
* @param {DeepPartial<ComponentProps>} [props.additionalProps] - Optional additional props to pass to the components.
1919
* @param {Resolver<DataT>} [props.validator] - Optional React Hook Form resolver for validation.
2020
*
2121
* @returns {JSX.Element} The rendered component.
@@ -28,7 +28,7 @@ export function FormPagesTester<DataT extends FieldValues, ComponentProps>({
2828
}: {
2929
pages: FormPage<DataT, ComponentProps, any>[];
3030
sampleData: DefaultValues<DataT>;
31-
additionalProps?: Partial<ComponentProps>;
31+
additionalProps?: DeepPartial<ComponentProps>;
3232
validator?: any; // Replace 'any' with the appropriate type for the validator
3333
}): JSX.Element {
3434
return (

src/testUtils/components/FormPageMultipleTester.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DefaultValues, FieldValues } from 'react-hook-form';
22
import { FormPageTester } from './FormPageTester';
33
import type { HookFormPage } from '../../hookForm';
4+
import type { DeepPartial } from '../../types';
45

56
export function FormPageMultipleTester<
67
DataT extends FieldValues,
@@ -13,7 +14,7 @@ export function FormPageMultipleTester<
1314
}: {
1415
page: HookFormPage<DataT, ComponentProps>;
1516
sampleData: DefaultValues<DataT>;
16-
additionalProps?: Partial<ComponentProps>;
17+
additionalProps?: DeepPartial<ComponentProps>;
1718
validator?: any; // Replace 'any' with the appropriate type for the validator
1819
}): JSX.Element {
1920
return (

src/testUtils/components/FormPageTester.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
useForm,
55
type UseFormReturn,
66
} from 'react-hook-form';
7-
import type { FormPage } from '../../types'; // Adjust the import path as needed
7+
import type { DeepPartial, FormPage } from '../../types'; // Adjust the import path as needed
88
import { useLayoutEffect } from 'react';
99

1010
export function FormPageTester<DataT extends FieldValues, ComponentProps>({
@@ -16,7 +16,7 @@ export function FormPageTester<DataT extends FieldValues, ComponentProps>({
1616
}: {
1717
page: FormPage<DataT, ComponentProps, any>;
1818
defaultValues?: DefaultValues<DataT>;
19-
additionalProps?: Partial<ComponentProps>;
19+
additionalProps?: DeepPartial<ComponentProps>;
2020
validator?: any; // Replace 'any' with the appropriate type for the validator
2121
shouldValidate?: boolean;
2222
}): JSX.Element {

src/testUtils/followSequence.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { FormPage, FormSequence, SequenceChild } from '../types';
1+
import type {
2+
DeepPartial,
3+
FormPage,
4+
FormSequence,
5+
SequenceChild,
6+
} from '../types';
27
import { flattenPages } from '../utils';
38

49
/**
@@ -11,7 +16,7 @@ import { flattenPages } from '../utils';
1116
*/
1217
export function followSequence<DataT, U, V>(
1318
sequence: SequenceChild<DataT, U, V>[] | FormSequence<DataT, U, V>,
14-
data: DataT,
19+
data: DeepPartial<DataT>,
1520
): FormPage<DataT, U, V>[] {
1621
if (!Array.isArray(sequence)) {
1722
sequence = sequence.pages;

0 commit comments

Comments
 (0)