Skip to content

Commit 08d485a

Browse files
committed
created a client-side preact form component
1 parent a6f44eb commit 08d485a

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

package-lock.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"astro-icon": "^1.0.3",
3030
"limax": "4.1.0",
3131
"lodash.merge": "^4.6.2",
32+
"preact": "^10.19.6",
33+
"preact-hooks": "^0.0.0-0.0.0",
3234
"react": "^18.2.0",
3335
"react-dom": "^18.2.0",
3436
"simple-stack-form": "^0.1.12",

src/components/Form.tsx

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Generated by simple:form
2+
3+
import { type ComponentProps, createContext } from "preact";
4+
import { useContext, useState } from "preact/hooks";
5+
import { navigate } from "astro:transitions/client";
6+
import {
7+
type FieldErrors,
8+
type FormState,
9+
type FormValidator,
10+
formNameInputProps,
11+
getInitialFormState,
12+
toSetValidationErrors,
13+
toTrackAstroSubmitStatus,
14+
toValidateField,
15+
validateForm,
16+
} from "simple:form";
17+
18+
export function useCreateFormContext(
19+
validator: FormValidator,
20+
fieldErrors?: FieldErrors,
21+
) {
22+
const initial = getInitialFormState({ validator, fieldErrors });
23+
const [formState, setFormState] = useState<FormState>(initial);
24+
return {
25+
value: formState,
26+
set: setFormState,
27+
setValidationErrors: toSetValidationErrors(setFormState),
28+
validateField: toValidateField(setFormState),
29+
trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState),
30+
};
31+
}
32+
33+
export function useFormContext() {
34+
const formContext = useContext(FormContext);
35+
if (!formContext) {
36+
throw new Error(
37+
"Form context not found. `useFormContext()` should only be called from children of a <Form> component.",
38+
);
39+
}
40+
return formContext;
41+
}
42+
43+
type FormContextType = ReturnType<typeof useCreateFormContext>;
44+
45+
const FormContext = createContext<FormContextType | undefined>(undefined);
46+
47+
export function Form({
48+
children,
49+
validator,
50+
context,
51+
fieldErrors,
52+
name,
53+
...formProps
54+
}: {
55+
validator: FormValidator;
56+
context?: FormContextType;
57+
fieldErrors?: FieldErrors;
58+
} & Omit<ComponentProps<"form">, "method" | "onSubmit">) {
59+
const formContext = context ?? useCreateFormContext(validator, fieldErrors);
60+
61+
return (
62+
<FormContext.Provider value={formContext}>
63+
<form
64+
{...formProps}
65+
method="POST"
66+
onSubmit={async (e) => {
67+
e.preventDefault();
68+
e.stopPropagation();
69+
const formData = new FormData(e.currentTarget);
70+
formContext.set((formState) => ({
71+
...formState,
72+
isSubmitPending: true,
73+
submitStatus: "validating",
74+
}));
75+
const parsed = await validateForm({ formData, validator });
76+
if (parsed.data) {
77+
const action =
78+
typeof formProps.action === "string"
79+
? formProps.action
80+
: // Check for Preact signals
81+
formProps.action?.value ?? "";
82+
navigate(action, { formData });
83+
return formContext.trackAstroSubmitStatus();
84+
}
85+
86+
formContext.setValidationErrors(parsed.fieldErrors);
87+
}}
88+
>
89+
{name ? <input {...formNameInputProps} value={name} /> : null}
90+
{children}
91+
</form>
92+
</FormContext.Provider>
93+
);
94+
}
95+
96+
export function Input(inputProps: ComponentProps<"input"> & { name: string }) {
97+
const formContext = useFormContext();
98+
const fieldState = formContext.value.fields[inputProps.name];
99+
if (!fieldState) {
100+
throw new Error(
101+
`Input "${inputProps.name}" not found in form. Did you use the <Form> component?`,
102+
);
103+
}
104+
105+
const { hasErroredOnce, validationErrors, validator } = fieldState;
106+
return (
107+
<>
108+
<input
109+
onBlur={async (e) => {
110+
const value = e.currentTarget.value;
111+
if (value === "") return;
112+
formContext.validateField(inputProps.name, value, validator);
113+
}}
114+
onInput={async (e) => {
115+
if (!hasErroredOnce) return;
116+
const value = e.currentTarget.value;
117+
formContext.validateField(inputProps.name, value, validator);
118+
}}
119+
{...inputProps}
120+
/>
121+
{validationErrors?.map((e) => (
122+
<p key={e}>{e}</p>
123+
))}
124+
</>
125+
);
126+
}

0 commit comments

Comments
 (0)