67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
"use client";
|
|
import { useRef, useState, type ComponentProps, type ReactNode } from "react";
|
|
import { useFormStatus } from "react-dom";
|
|
|
|
function FormFields({ children }: { children: ReactNode }) {
|
|
const { pending } = useFormStatus();
|
|
return (
|
|
<fieldset disabled={pending} style={{ display: "contents" }}>
|
|
{children}
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
/** Keeps action errors beside the submitted form, without losing entered values. */
|
|
export function ActionForm({
|
|
action,
|
|
children,
|
|
...props
|
|
}: Omit<ComponentProps<"form">, "action"> & {
|
|
action: (data: FormData) => Promise<void>;
|
|
}) {
|
|
const preserveValues = useRef(false);
|
|
const [message, setMessage] = useState("");
|
|
const [failed, setFailed] = useState(false);
|
|
return (
|
|
<form
|
|
{...props}
|
|
onReset={(event) => {
|
|
if (preserveValues.current) event.preventDefault();
|
|
}}
|
|
onChange={() => setMessage("")}
|
|
onKeyDown={(event) => {
|
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
event.preventDefault();
|
|
const button = event.currentTarget.querySelector<HTMLButtonElement>(
|
|
'button[type="submit"]',
|
|
);
|
|
if (button && !button.disabled) button.click();
|
|
}
|
|
}}
|
|
action={async (data) => {
|
|
setMessage("");
|
|
preserveValues.current = false;
|
|
try {
|
|
await action(data);
|
|
setFailed(false);
|
|
setMessage("已保存");
|
|
} catch {
|
|
preserveValues.current = true;
|
|
setFailed(true);
|
|
setMessage("操作失败,请检查输入后重试。填写内容已保留。");
|
|
}
|
|
}}
|
|
>
|
|
<FormFields>{children}</FormFields>
|
|
{message && (
|
|
<p
|
|
role={failed ? "alert" : "status"}
|
|
className={`full-span ${failed ? "error-text" : "muted"}`}
|
|
>
|
|
{message}
|
|
</p>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|