Skip to content

Commit

Permalink
feat(Form): submit with onSubmit instead of action
Browse files Browse the repository at this point in the history
  • Loading branch information
MiroslavPetrik committed Dec 13, 2024
1 parent 6e4d927 commit e9db4f5
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 34 deletions.
41 changes: 23 additions & 18 deletions src/Form.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { describe, test } from "vitest";
import { describe, test, expect } from "vitest";
import { userEvent } from "@testing-library/user-event";
import { act, render, screen } from "@testing-library/react";
import { z } from "zod";
Expand All @@ -23,7 +23,6 @@ describe("Form", () => {
message: "Passwords don't match",
}),
)
.error(() => "fail")
.run(async () => {
// implementation
return "success";
Expand All @@ -36,14 +35,20 @@ describe("Form", () => {
return (
<>
{isInvalid && (
<>
<p>
<ZodFieldError errors={validationError} />
<ZodFieldError errors={validationError} name="user.email" />
<ZodFieldError errors={validationError} name="password" />
</>
</p>
)}
<input type="text" name="user.email" data-testid="email" />
{isInvalid && (
<ZodFieldError errors={validationError} name="user.email" />
)}
<input type="text" name="password" data-testid="pass" />
{isInvalid && (
<p>
<ZodFieldError errors={validationError} name="password" />
</p>
)}
<input type="text" name="confirm" />
<button type="submit" data-testid="submit" />
</>
Expand All @@ -61,18 +66,18 @@ describe("Form", () => {
const pass = screen.getByTestId("pass");
await act(() => userEvent.type(pass, "short"));

// TODO: this fails
// const submit = screen.getByTestId("submit");
// await act(() => userEvent.click(submit));
const submit = screen.getByTestId("submit");
await act(() => userEvent.click(submit));

// expect(screen.getByText("Passwords don't match")).toBeInTheDocument();
// expect(
// screen.getByText("Something email")
// // @ts-expect-error
// ).toBeInTheDocument();
// expect(
// screen.getByText("String must contain at least 8 character(s)")
// // @ts-expect-error
// ).toBeInTheDocument();
// @ts-expect-error
expect(screen.getByText("Passwords don't match")).toBeInTheDocument();
expect(
screen.getByText("Invalid email"),
// @ts-expect-error
).toBeInTheDocument();
expect(
screen.getByText("String must contain at least 8 character(s)"),
// @ts-expect-error
).toBeInTheDocument();
});
});
47 changes: 35 additions & 12 deletions src/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React from "react";
import type { FormHTMLAttributes } from "react";
import React, { useTransition } from "react";
import type { FormHTMLAttributes, FormEvent } from "react";
import type { RenderProp } from "react-render-prop-type";
import type {
FormState,
Expand All @@ -19,6 +19,13 @@ export type FormStateProps<Data, Error, ValidationError, Payload> = {
) => Promise<FormState<Data, Error, ValidationError>>;
initialData: Data;
permalink?: string;
/**
* Opt-in into automatic form reset by using the form "action" prop.
* By default, the onSubmit with a custom transition which opts-out of the implicit form reset.
* See for more. https://github.com/facebook/react/issues/29034
* @default false
*/
autoReset?: boolean;
};

export function initial<Data>(data: Data): InitialState<Data> {
Expand All @@ -34,7 +41,7 @@ type FormStatusFlags<
isSuccess: T extends "success" ? true : false;
};

type FormMetaState<T extends FormState<unknown, unknown, unknown>> = T &
export type FormMetaState<T extends FormState<unknown, unknown, unknown>> = T &
FormStatusFlags<T["type"]> & {
isPending: boolean;
};
Expand Down Expand Up @@ -63,29 +70,45 @@ export function Form<Data, Error, ValidationError>({
action,
initialData,
permalink,
autoReset = false,
...props
}: FormProps<Data, Error, ValidationError>) {
const [state, formAction, isPending] = useActionState(
action,
initial(initialData),
permalink,
);
const [, startTransition] = useTransition();

const metaState =
state.type === "initial"
? { ...state, ...neverMetaState, isInitial: true as const }
? { ...state, ...neverMetaState, isPending, isInitial: true as const }
: state.type === "invalid"
? { ...state, ...neverMetaState, isInvalid: true as const }
? { ...state, ...neverMetaState, isPending, isInvalid: true as const }
: state.type === "failure"
? { ...state, ...neverMetaState, isFailure: true as const }
: { ...state, ...neverMetaState, isSuccess: true as const };
? { ...state, ...neverMetaState, isPending, isFailure: true as const }
: {
...state,
...neverMetaState,
isPending,
isSuccess: true as const,
};

const submitStrategy = autoReset
? { action: formAction }
: {
onSubmit: (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
startTransition(() => {
formAction(new FormData(form));
});
},
};

return (
<form action={formAction} {...props}>
{children({
...metaState,
isPending,
})}
<form {...submitStrategy} {...props}>
{children(metaState)}
</form>
);
}
15 changes: 11 additions & 4 deletions src/createFormAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ export type FormState<Data, Error, ValidationError = Record<string, never>> =
| FailureState<Error>
| SuccessState<Data>;

export type FormAction<
Data,
Error = Data,
ValidationError = Record<string, never>,
Payload = FormData,
> = (
state: FormState<Data, Error, ValidationError>,
payload: Payload,
) => Promise<FormState<Data, Error, ValidationError>>;

export function createFormAction<
Data,
Error = Data,
Expand All @@ -44,10 +54,7 @@ export function createFormAction<
invalid: (
validationError: ValidationError,
) => InvalidState<ValidationError>;
}) => (
state: FormState<Data, Error, ValidationError>,
payload: Payload,
) => Promise<FormState<Data, Error, ValidationError>>,
}) => FormAction<Data, Error, ValidationError, Payload>,
) {
function success(data: Data): SuccessState<Data> {
return { type: "success", data, error: null, validationError: null };
Expand Down

0 comments on commit e9db4f5

Please sign in to comment.