Skip to content

Commit

Permalink
DeepPartial, docs updates
Browse files Browse the repository at this point in the history
  • Loading branch information
stutrek committed Oct 8, 2024
1 parent 9e55087 commit 38ffc11
Show file tree
Hide file tree
Showing 19 changed files with 111 additions and 70 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# react-multi-page-form
![React Multi Page Form](https://stutrek.github.io/react-multi-page-form/Logo.svg "")

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.

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

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

All of the examples use react-hook-form, but any library could be used.
An integration with React Hook Form is provided, but the base could be used with any library.

[View the docs](https://stutrek.github.io/react-multi-page-form/)

## Basic Usage

Expand Down Expand Up @@ -61,6 +63,9 @@ const MyMultiPageForm = () => {
</>);
};
```
[View the docs](https://stutrek.github.io/react-multi-page-form/)


### Pages and Sequences

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.
Expand All @@ -71,18 +76,20 @@ A **sequence** is an array of pages and sequences that represent a specific flow
type FormPage<DataT, ComponentProps, ErrorList> = {
id: string;
// determines whether or not this page is needed
isRequired?: (data: Partial<DataT>) => boolean | undefined
isRequired?: (data: DeepPartial<DataT>) => boolean | undefined
// determines if the page is already complete
isComplete: (data: Partial<DataT>) => boolean;
isComplete: (data: DeepPartial<DataT>) => boolean;
// determines if this should be a final step in the flow
isFinal?: (data: Partial<DataT>) => boolean;
isFinal?: (data: DeepPartial<DataT>) => boolean;
// if you need to break the flow of the sequence, this makes that possible
alternateNextPage?: (data: DeepPartial<DataT>) => Boolean
// Mounted inputs are automatically validated.
// If you need specific validation logic, put it here.
validate?: (data: Partial<DataT>) => ErrorList | undefined;
validate?: (data: DeepPartial<DataT>) => ErrorList | undefined;
// callback on arrival
onArrive?: (data: Partial<DataT>) => void;
onArrive?: (data: DeepPartial<DataT>) => void;
// callback on departure
onExit?: (data: Partial<DataT>) => Promise<void> | void;
onExit?: (data: DeepPartial<DataT>) => Promise<void> | void;
// the component that will be rendered
Component: (props: ComponentProps) => JSX.Element;
};
Expand All @@ -96,6 +103,9 @@ export type FormSequence<DataT, ComponentProps, ErrorList> = {
};
```

[View the docs](https://stutrek.github.io/react-multi-page-form/)


## A More Complete Example

```typescript
Expand Down Expand Up @@ -163,5 +173,6 @@ export function MyMultiPageForm() {
);
}
```
[View the docs](https://stutrek.github.io/react-multi-page-form/)


5 changes: 3 additions & 2 deletions docs/src/app/Intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ This example features a short form that displays additional pages only if the us

## Integration within the Ecosystem

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

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.

## Why Use React Multi Page Form?

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

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).
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).
16 changes: 8 additions & 8 deletions docs/src/app/docs/api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ type HookFormPage<DataT, ComponentProps = { hookForm: UseFormReturn }> = {
// these three are required
id: string; // Unique identifier for the form page.
Component: (props: ComponentProps) => JSX.Element; // React component to render the form page.
isComplete: (data: Partial<DataT>) => boolean; // Checks if the user has already filled out this page.
isComplete: (data: DeepPartial<DataT>) => boolean; // Checks if the user has already filled out this page.

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

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

Expand Down Expand Up @@ -63,7 +63,7 @@ A sequence contains pages or more sequences that represent a single workflow. Th
export type FormSequence<DataT, ComponentProps, ErrorList> = {
id: string; // Unique identifier for the form sequence.
pages: HookFormSequenceChild[]; // A SequenceChild is either a FormPage or a FormSequence.
isRequired?: (data: Partial<DataT>) => boolean | undefined; // Determines if the sequence is needed based on form data.
isRequired?: (data: DeepPartial<DataT>) => boolean | undefined; // Determines if the sequence is needed based on form data.
};
```
Expand Down
2 changes: 1 addition & 1 deletion docs/src/app/docs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ To allow consistent typing throughout the library, you need a single type for yo

### Schema

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.
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.

```typescript
type MySchema = {
Expand Down
2 changes: 1 addition & 1 deletion docs/src/app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function Header({ showLogo = true }: HeaderProps) {
<li>
<Link href="/">
<img
src="./Logo.svg"
src="/react-multi-page-form/Logo.svg"
alt="React Multi Page Form"
className="h-7"
/>
Expand Down
7 changes: 4 additions & 3 deletions docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<head>
<base href="/react-multi-page-form/" />
</head>
<body className="antialiased">{children}</body>
<div className="container max-w-3xl mx-auto p-4 text-sm text-gray-400">
<hr className="mb-3" />
&copy; 2024 Stu Kabakoff
</div>
</html>
);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function () {
<div className="container max-w-3xl mx-auto prose p-4">
<div className="text-center">
<img
src="./Logo.svg"
src="/react-multi-page-form/Logo.svg"
alt="React Multi Page Form"
className="h-13"
/>
Expand Down
7 changes: 7 additions & 0 deletions docs/src/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
a: (props) => {
let href = props.href as string;
if (href?.startsWith('/')) {
href = `/react-multi-page-form${href}`;
}
return <a href={href} {...props} />;
},
...components,
};
}
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"version": "0.1.0",
"description": "Tools to handle multi-page forms",
"main": "dist/index.js",
"files": [
"dist"
],
"files": ["dist"],
"scripts": {
"build": "tsc",
"build:watch": "tsc -w",
Expand All @@ -16,10 +14,7 @@
"test": "jest",
"test:watch": "jest --watch"
},
"keywords": [
"forms",
"multi-page"
],
"keywords": ["forms", "multi-page"],
"author": "Stu Kabakoff",
"license": "MIT",
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/initialization.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { renderHook } from '@testing-library/react';
import { useMultiPageFormBase } from '../base';
import { StartingPage } from '../types';
import { type DeepPartial, StartingPage } from '../types';

describe('useMultiPageFormBase - Initialization Tests', () => {
const pages = [
Expand Down Expand Up @@ -285,7 +285,7 @@ describe('useMultiPageFormBase - Initialization Tests', () => {
{
id: 'page2',
isComplete: () => false,
isRequired: (data: Partial<{ skipPage2: boolean }>) =>
isRequired: (data: DeepPartial<{ skipPage2: boolean }>) =>
!data.skipPage2,
Component: () => <div />,
},
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/sequenceFollower.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React from 'react';
import { followSequence } from '../testUtils/followSequence';
import type { SequenceChild } from '../types';
import type { DeepPartial, SequenceChild } from '../types';

describe('followSequence', () => {
beforeEach(() => {
Expand Down Expand Up @@ -142,14 +142,14 @@ describe('followSequence', () => {
const pages = [
{
id: 'page1',
isRequired: (data: Partial<typeof formData>) =>
isRequired: (data: DeepPartial<typeof formData>) =>
data.includePage1,
isComplete: () => false,
Component: () => <div />,
},
{
id: 'page2',
alternateNextPage: (data: Partial<typeof formData>) =>
alternateNextPage: (data: DeepPartial<typeof formData>) =>
data.skipToPage4 ? 'page4' : undefined,
isComplete: () => false,
Component: () => <div />,
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('followSequence', () => {
const pages = [
{
id: 'page1',
isFinal: (data: Partial<typeof formData>) => !!data.endHere,
isFinal: (data: DeepPartial<typeof formData>) => !!data.endHere,
isComplete: () => false,
Component: () => <div />,
},
Expand Down
4 changes: 2 additions & 2 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
useState,
} from 'react';
import { StartingPage } from './types';
import type { FormPage, MultiPageFormParams } from './types';
import type { DeepPartial, FormPage, MultiPageFormParams } from './types';
import { flattenPages, useCallbackRef } from './utils';

function isRequired<DataT, Page extends FormPage<DataT, any, any>>(
page: Page,
data: DataT,
data: DeepPartial<DataT>,
) {
if (page.isRequired === undefined || page.isRequired(data) !== false) {
return true;
Expand Down
2 changes: 2 additions & 0 deletions src/hookForm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
DeepPartial,
FieldErrors,
FieldValues,
Path,
Expand Down Expand Up @@ -124,6 +125,7 @@ export const useMultiPageHookForm = <
const { trigger, reset, control } = hookForm;

const multiPageForm = useMultiPageFormBase({
// @ts-ignore
getCurrentData: () => hookForm.getValues(),
onBeforePageChange: async (data, page) => {
if (onBeforePageChange) {
Expand Down
8 changes: 4 additions & 4 deletions src/testUtils/FormPagesTester.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import type { FormPage } from '../types'; // Adjust the import path as needed
import type { DeepPartial, FormPage } from '../types'; // Adjust the import path as needed
import { FormPageMultipleTester } from './components/FormPageMultipleTester'; // Adjust the import path as needed
import type { DefaultValues, FieldValues } from 'react-hook-form';

Expand All @@ -14,8 +14,8 @@ import type { DefaultValues, FieldValues } from 'react-hook-form';
*
* @param {object} props - The component props.
* @param {HookFormPage<DataT, ComponentProps>[]} props.pages - An array of `FormPage` objects to be tested.
* @param {Partial<DataT>} props.sampleData - Sample data to use for testing.
* @param {Partial<ComponentProps>} [props.additionalProps] - Optional additional props to pass to the components.
* @param {DeepPartial<DataT>} props.sampleData - Sample data to use for testing.
* @param {DeepPartial<ComponentProps>} [props.additionalProps] - Optional additional props to pass to the components.
* @param {Resolver<DataT>} [props.validator] - Optional React Hook Form resolver for validation.
*
* @returns {JSX.Element} The rendered component.
Expand All @@ -28,7 +28,7 @@ export function FormPagesTester<DataT extends FieldValues, ComponentProps>({
}: {
pages: FormPage<DataT, ComponentProps, any>[];
sampleData: DefaultValues<DataT>;
additionalProps?: Partial<ComponentProps>;
additionalProps?: DeepPartial<ComponentProps>;
validator?: any; // Replace 'any' with the appropriate type for the validator
}): JSX.Element {
return (
Expand Down
3 changes: 2 additions & 1 deletion src/testUtils/components/FormPageMultipleTester.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DefaultValues, FieldValues } from 'react-hook-form';
import { FormPageTester } from './FormPageTester';
import type { HookFormPage } from '../../hookForm';
import type { DeepPartial } from '../../types';

export function FormPageMultipleTester<
DataT extends FieldValues,
Expand All @@ -13,7 +14,7 @@ export function FormPageMultipleTester<
}: {
page: HookFormPage<DataT, ComponentProps>;
sampleData: DefaultValues<DataT>;
additionalProps?: Partial<ComponentProps>;
additionalProps?: DeepPartial<ComponentProps>;
validator?: any; // Replace 'any' with the appropriate type for the validator
}): JSX.Element {
return (
Expand Down
4 changes: 2 additions & 2 deletions src/testUtils/components/FormPageTester.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
useForm,
type UseFormReturn,
} from 'react-hook-form';
import type { FormPage } from '../../types'; // Adjust the import path as needed
import type { DeepPartial, FormPage } from '../../types'; // Adjust the import path as needed
import { useLayoutEffect } from 'react';

export function FormPageTester<DataT extends FieldValues, ComponentProps>({
Expand All @@ -16,7 +16,7 @@ export function FormPageTester<DataT extends FieldValues, ComponentProps>({
}: {
page: FormPage<DataT, ComponentProps, any>;
defaultValues?: DefaultValues<DataT>;
additionalProps?: Partial<ComponentProps>;
additionalProps?: DeepPartial<ComponentProps>;
validator?: any; // Replace 'any' with the appropriate type for the validator
shouldValidate?: boolean;
}): JSX.Element {
Expand Down
9 changes: 7 additions & 2 deletions src/testUtils/followSequence.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { FormPage, FormSequence, SequenceChild } from '../types';
import type {
DeepPartial,
FormPage,
FormSequence,
SequenceChild,
} from '../types';
import { flattenPages } from '../utils';

/**
Expand All @@ -11,7 +16,7 @@ import { flattenPages } from '../utils';
*/
export function followSequence<DataT, U, V>(
sequence: SequenceChild<DataT, U, V>[] | FormSequence<DataT, U, V>,
data: DataT,
data: DeepPartial<DataT>,
): FormPage<DataT, U, V>[] {
if (!Array.isArray(sequence)) {
sequence = sequence.pages;
Expand Down
Loading

0 comments on commit 38ffc11

Please sign in to comment.