@@ -65,6 +65,7 @@ export class New extends Command {
|
||||
const formChoices: Array<{name: AppGeneratorOptions["form"]; message?: string}> = [
|
||||
{name: "React Final Form", message: "React Final Form (recommended)"},
|
||||
{name: "React Hook Form"},
|
||||
{name: "Formik"},
|
||||
]
|
||||
|
||||
const promptResult: any = await this.enquirer.prompt({
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface AppGeneratorOptions extends GeneratorOptions {
|
||||
version: string
|
||||
skipInstall: boolean
|
||||
skipGit: boolean
|
||||
form: "React Final Form" | "React Hook Form"
|
||||
form: "React Final Form" | "React Hook Form" | "Formik"
|
||||
}
|
||||
|
||||
export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
@@ -70,6 +70,17 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
)
|
||||
pkg.dependencies["react-hook-form"] = "6.x"
|
||||
break
|
||||
case "Formik":
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/formik/Form.tsx"),
|
||||
this.destinationPath("app/components/Form.tsx"),
|
||||
)
|
||||
this.fs.move(
|
||||
this.destinationPath("_forms/formik/LabeledTextField.tsx"),
|
||||
this.destinationPath("app/components/LabeledTextField.tsx"),
|
||||
)
|
||||
pkg.dependencies["formik"] = "2.x"
|
||||
break
|
||||
}
|
||||
this.fs.delete(this.destinationPath("_forms"))
|
||||
|
||||
|
||||
84
packages/generator/templates/app/_forms/formik/Form.tsx
Normal file
84
packages/generator/templates/app/_forms/formik/Form.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useState, ReactNode, PropsWithoutRef } from "react";
|
||||
import { Formik, FormikProps, FormikErrors } from "formik";
|
||||
import * as z from "zod";
|
||||
|
||||
type FormProps<FormValues> = {
|
||||
/** All your form fields */
|
||||
children: ReactNode;
|
||||
/** Text to display in the submit button */
|
||||
submitText: string;
|
||||
onSubmit: (values: FormValues) => Promise<void | OnSubmitResult>;
|
||||
initialValues?: FormikProps<FormValues>["initialValues"];
|
||||
schema?: z.ZodType<any, any>;
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">;
|
||||
|
||||
type OnSubmitResult = {
|
||||
FORM_ERROR?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const FORM_ERROR = "FORM_ERROR"
|
||||
|
||||
export function Form<FormValues extends Record<string, unknown>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<FormValues>) {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
return (
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues || {} as FormValues}
|
||||
validate={(values) => {
|
||||
if (!schema) return;
|
||||
try {
|
||||
schema.parse(values);
|
||||
} catch (error) {
|
||||
return error.formErrors.fieldErrors;
|
||||
}
|
||||
}}
|
||||
onSubmit={async (values, {setErrors}) => {
|
||||
const {FORM_ERROR, ...otherErrors} = (await onSubmit(values as FormValues)) || {}
|
||||
|
||||
if(FORM_ERROR) {
|
||||
setFormError(FORM_ERROR);
|
||||
}
|
||||
|
||||
if(Object.keys(otherErrors).length > 0) {
|
||||
setErrors(otherErrors as FormikErrors<FormValues>)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, isSubmitting, }) => (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="form"
|
||||
{...props}
|
||||
>
|
||||
{/* Form fields supplied as children are rendered here */}
|
||||
{children}
|
||||
|
||||
{formError && (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{formError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{submitText}
|
||||
</button>
|
||||
|
||||
<style global jsx>{`
|
||||
.form > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form;
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { PropsWithoutRef } from "react";
|
||||
import { useField, useFormikContext, ErrorMessage } from "formik";
|
||||
|
||||
export interface LabeledTextFieldProps
|
||||
extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
|
||||
/** Field name. */
|
||||
name: string;
|
||||
/** Field label. */
|
||||
label: string;
|
||||
/** Field type. Doesn't include radio buttons and checkboxes */
|
||||
type?: "text" | "password" | "email" | "number";
|
||||
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>;
|
||||
}
|
||||
|
||||
export const LabeledTextField = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
LabeledTextFieldProps
|
||||
>(({ name, label, outerProps, ...props }, ref) => {
|
||||
const [input] = useField(name);
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
<label>
|
||||
{label}
|
||||
<input {...input} disabled={isSubmitting} {...props} ref={ref} />
|
||||
</label>
|
||||
|
||||
<ErrorMessage name={name}>
|
||||
{(msg) => (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{msg}
|
||||
</div>
|
||||
)}
|
||||
</ErrorMessage>
|
||||
|
||||
<style jsx>{`
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
font-size: 1rem;
|
||||
}
|
||||
input {
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid purple;
|
||||
appearance: none;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default LabeledTextField;
|
||||
Reference in New Issue
Block a user