Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #236]: Server action example #341

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
29 changes: 29 additions & 0 deletions app/src/app/[locale]/serverAction/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use server";

interface FormDataState {
name: string;
email: string;
}

export async function updateServerData(
prevState: FormDataState,
formData: FormData
): Promise<FormDataState> {
console.log("prevState => ", prevState);
console.log("formData => ", formData);

// With a server form, formData is null and only prevState can be used
const name = (formData?.get("name") as string) || prevState.name;
const email = (formData?.get("email") as string) || prevState.email;

// In a real application, you would typically perform
// some server mutation.
await new Promise((resolve) => setTimeout(resolve, 2000));

const updatedData: FormDataState = {
name: name || prevState.name,
email: email || prevState.email,
};

return updatedData;
}
65 changes: 65 additions & 0 deletions app/src/app/[locale]/serverAction/clientForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { useFormState } from "react-dom";

import { useTranslations } from "next-intl";
import { Label, TextInput } from "@trussworks/react-uswds";

import SubmitButton from "../../../components/SubmitButton";
import { updateServerData } from "./actions";

const initialFormState = {
name: "",
email: "",
};

export default function ClientForm() {
const t = useTranslations("serverAction");
const [formData, updateFormData] = useFormState(
updateServerData,
initialFormState
);

const hasReturnedFormData = formData.name || formData.email;

return (
<>
<form action={updateFormData} className="usa-form">
<Label htmlFor="name">{t("nameLabel")}</Label>
<TextInput
id="name"
name="name"
type="text"
defaultValue={formData.name}
required
/>
<Label htmlFor="email">{t("emailLabel")}</Label>
<TextInput
id="email"
name="email"
type="email"
defaultValue={formData.email}
className="margin-bottom-1"
required
/>
<SubmitButton />
</form>

{hasReturnedFormData && (
<div className="margin-top-4">
<h2>{t("returnedDataHeader")}</h2>
{formData.name && (
<div>
<strong>{t("nameLabel")}:</strong> {formData.name}
</div>
)}
{formData.email && (
<div>
<strong>{t("emailLabel")}:</strong> {formData.email}
</div>
)}
</div>
)}
</>
);
}
47 changes: 47 additions & 0 deletions app/src/app/[locale]/serverAction/page.tsx
rylew1 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful for the example to also demonstrate how to test the page, as well as render it in Storybook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can give tests and storybook a shot when we decide on how we want to construct these example(s)

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Server Action example
// For more context: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

import { pick } from "lodash";
import { Metadata } from "next";

import {
NextIntlClientProvider,
useMessages,
useTranslations,
} from "next-intl";
import { getTranslations } from "next-intl/server";

import ClientForm from "./clientForm";

interface RouteParams {
locale: string;
}

export async function generateMetadata({ params }: { params: RouteParams }) {
const t = await getTranslations({ locale: params.locale });
const meta: Metadata = {
title: t("serverAction.title"),
};

return meta;
}

interface Props {
params: RouteParams;
}

export default function SimpleForm({ params }: Props) {
const { locale } = params;
const messages = useMessages();
const t = useTranslations("serverAction");

return (
<NextIntlClientProvider
locale={locale}
messages={pick(messages, "serverAction")}
>
<h1>{t("title")}</h1>
<ClientForm />
</NextIntlClientProvider>
);
}
34 changes: 34 additions & 0 deletions app/src/components/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { Label, TextInput } from "@trussworks/react-uswds";

interface FormInputProps {
id: string;
name: string;
type: "number" | "search" | "text" | "email" | "password" | "tel" | "url";
label: string;
defaultValue?: string;
}

const FormInput: React.FC<FormInputProps> = ({
id,
name,
type,
label,
defaultValue,
}) => {
return (
<>
<Label htmlFor={id}>{label}</Label>
<TextInput
id={id}
name={name}
type={type}
defaultValue={defaultValue}
className="margin-bottom-1"
/>
</>
);
};

export default FormInput;
19 changes: 19 additions & 0 deletions app/src/components/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { useFormStatus } from "react-dom";

import { useTranslations } from "next-intl";
import { Button } from "@trussworks/react-uswds";

function SubmitButton() {
const t = useTranslations("serverAction");
const { pending } = useFormStatus();

return (
<Button type="submit" disabled={pending}>
{pending ? t("submitting") : t("submit")}
</Button>
);
}

export default SubmitButton;
9 changes: 9 additions & 0 deletions app/src/i18n/messages/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ export const messages = {
formatting:
"The template includes an internationalization library with basic formatters built-in. Such as numbers: { amount, number, currency }, and dates: { isoDate, date, long}.",
},
serverAction: {
title: "Server Actions Example",
submitting: "Submitting...",
submit: "Submit",
nameLabel: "Name",
emailLabel: "Email",
submitLabel: "Submit",
returnedDataHeader: "Server Action returned data",
},
};
Loading