diff --git a/app/src/app/[locale]/serverAction/actions.ts b/app/src/app/[locale]/serverAction/actions.ts new file mode 100644 index 00000000..b5bd235a --- /dev/null +++ b/app/src/app/[locale]/serverAction/actions.ts @@ -0,0 +1,29 @@ +"use server"; + +interface FormDataState { + name: string; + email: string; +} + +export async function updateServerData( + prevState: FormDataState, + formData: FormData +): Promise { + 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; +} diff --git a/app/src/app/[locale]/serverAction/clientForm.tsx b/app/src/app/[locale]/serverAction/clientForm.tsx new file mode 100644 index 00000000..2cb27b99 --- /dev/null +++ b/app/src/app/[locale]/serverAction/clientForm.tsx @@ -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 ( + <> +
+ + + + + + + + {hasReturnedFormData && ( +
+

{t("returnedDataHeader")}

+ {formData.name && ( +
+ {t("nameLabel")}: {formData.name} +
+ )} + {formData.email && ( +
+ {t("emailLabel")}: {formData.email} +
+ )} +
+ )} + + ); +} diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx new file mode 100644 index 00000000..e71d362e --- /dev/null +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -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 ( + +

{t("title")}

+ +
+ ); +} diff --git a/app/src/components/FormInput.tsx b/app/src/components/FormInput.tsx new file mode 100644 index 00000000..58d73c38 --- /dev/null +++ b/app/src/components/FormInput.tsx @@ -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 = ({ + id, + name, + type, + label, + defaultValue, +}) => { + return ( + <> + + + + ); +}; + +export default FormInput; diff --git a/app/src/components/SubmitButton.tsx b/app/src/components/SubmitButton.tsx new file mode 100644 index 00000000..f10f11c6 --- /dev/null +++ b/app/src/components/SubmitButton.tsx @@ -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 ( + + ); +} + +export default SubmitButton; diff --git a/app/src/i18n/messages/en-US/index.ts b/app/src/i18n/messages/en-US/index.ts index 5e856e07..0993602d 100644 --- a/app/src/i18n/messages/en-US/index.ts +++ b/app/src/i18n/messages/en-US/index.ts @@ -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", + }, };