Compare commits
3 Commits
@blitzjs/n
...
remove-unm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4229e89825 | ||
|
|
cf6632340d | ||
|
|
49cb7344dc |
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm manypkg check
|
||||
pnpm lint
|
||||
pnpm pretty-quick --staged
|
||||
# pnpm manypkg check
|
||||
# pnpm lint
|
||||
# pnpm pretty-quick --staged
|
||||
|
||||
@@ -12,9 +12,8 @@ import {codegenTasks} from "../utils/codegen-tasks"
|
||||
|
||||
type NotUndefined<T> = T extends undefined ? never : T
|
||||
const forms: Record<NotUndefined<AppGeneratorOptions["form"]>, string> = {
|
||||
finalform: "React Final Form (recommended)",
|
||||
tanstack: "Tanstack Form (Recommended)",
|
||||
hookform: "React Hook Form",
|
||||
formik: "Formik",
|
||||
}
|
||||
|
||||
const language = {
|
||||
@@ -75,7 +74,7 @@ const args = arg(
|
||||
let projectName: string = ""
|
||||
let projectPath: string = ""
|
||||
let projectLanguage: string | TLanguage = ""
|
||||
let projectFormLib: AppGeneratorOptions["form"] = "finalform"
|
||||
let projectFormLib: AppGeneratorOptions["form"] = "tanstack"
|
||||
let projectTemplate: AppGeneratorOptions["template"] = templates.full
|
||||
let projectPkgManger: TPkgManager = PREFERABLE_PKG_MANAGER
|
||||
let shouldInstallDeps: boolean = true
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface AppGeneratorOptions extends GeneratorOptions {
|
||||
version: string
|
||||
skipInstall: boolean
|
||||
skipGit: boolean
|
||||
form: "finalform" | "hookform" | "formik"
|
||||
form: "tanstack" | "hookform"
|
||||
onPostInstall?: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -284,18 +284,14 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
|
||||
|
||||
const type = this.options.form
|
||||
switch (type) {
|
||||
case "finalform":
|
||||
pkg.dependencies["final-form"] = "4.x"
|
||||
pkg.dependencies["react-final-form"] = "6.x"
|
||||
case "tanstack":
|
||||
pkg.dependencies["@tanstack/react-form"] = "0.x"
|
||||
break
|
||||
case "hookform":
|
||||
pkg.dependencies["react-hook-form"] = "7.x"
|
||||
pkg.dependencies["@hookform/resolvers"] = "2.x"
|
||||
pkg.dependencies["@hookform/error-message"] = "2.x"
|
||||
break
|
||||
case "formik":
|
||||
pkg.dependencies["formik"] = "2.x"
|
||||
break
|
||||
}
|
||||
this.fs.move(
|
||||
this.destinationPath(`_forms/${type}/Form.${ext}`),
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { ReactNode, PropsWithoutRef } from "react"
|
||||
import { Form as FinalForm, FormProps as FinalFormProps } from "react-final-form"
|
||||
import { z } from "zod"
|
||||
import { validateZodSchema } from "blitz"
|
||||
export { FORM_ERROR } from "final-form"
|
||||
|
||||
export interface FormProps<S extends z.ZodType<any, any>>
|
||||
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
|
||||
/** All your form fields */
|
||||
children?: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText?: string
|
||||
schema?: S
|
||||
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
|
||||
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
|
||||
}
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
return (
|
||||
<FinalForm
|
||||
initialValues={initialValues}
|
||||
validate={validateZodSchema(schema)}
|
||||
onSubmit={onSubmit}
|
||||
render={({ handleSubmit, submitting, submitError }) => (
|
||||
<form onSubmit={handleSubmit} className="form" {...props}>
|
||||
{/* Form fields supplied as children are rendered here */}
|
||||
{children}
|
||||
|
||||
{submitError && (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submitText && (
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitText}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<style global jsx>{`
|
||||
.form > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -1,60 +0,0 @@
|
||||
import { forwardRef, PropsWithoutRef } from "react"
|
||||
import { useField } from "react-final-form"
|
||||
|
||||
export interface LabeledSelectFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
|
||||
/** Field name. */
|
||||
name: string
|
||||
/** Field label. */
|
||||
label: string
|
||||
type?: "number" | "string"
|
||||
options: any
|
||||
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
|
||||
}
|
||||
|
||||
export const LabeledSelectField = forwardRef<HTMLSelectElement, LabeledSelectFieldProps>(
|
||||
({ name, label, outerProps, options, type="number", ...props }, ref) => {
|
||||
const {
|
||||
input,
|
||||
meta: { touched, error, submitError, submitting },
|
||||
} = useField(name, {
|
||||
parse: type === "number" ? Number : undefined,
|
||||
})
|
||||
|
||||
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
|
||||
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
<label>
|
||||
{label}
|
||||
<select {...input} disabled={submitting} {...props} ref={ref}>
|
||||
{options && options.map((value) => <option key={value.id} value={value.id}>{value[name]}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{touched && normalizedError && (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{normalizedError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
font-size: 1rem;
|
||||
}
|
||||
select {
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
appearance: none;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default LabeledSelectField
|
||||
@@ -1,66 +0,0 @@
|
||||
import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react"
|
||||
import { useField, UseFieldConfig } from "react-final-form"
|
||||
|
||||
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"]>
|
||||
labelProps?: ComponentPropsWithoutRef<"label">
|
||||
fieldProps?: UseFieldConfig<string>
|
||||
}
|
||||
|
||||
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
|
||||
({ name, label, outerProps, fieldProps, labelProps, ...props }, ref) => {
|
||||
const {
|
||||
input,
|
||||
meta: { touched, error, submitError, submitting },
|
||||
} = useField(name, {
|
||||
parse:
|
||||
props.type === "number"
|
||||
? (Number as any)
|
||||
: // Converting `""` to `null` ensures empty values will be set to null in the DB
|
||||
(v) => (v === "" ? null : v),
|
||||
...fieldProps,
|
||||
})
|
||||
|
||||
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
|
||||
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
<label {...labelProps}>
|
||||
{label}
|
||||
<input {...input} disabled={submitting} {...props} ref={ref} />
|
||||
</label>
|
||||
|
||||
{touched && normalizedError && (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{normalizedError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useState, ReactNode, PropsWithoutRef } from "react"
|
||||
import { Formik, FormikProps } from "formik"
|
||||
import { validateZodSchema } from "blitz"
|
||||
import { z } from "zod"
|
||||
|
||||
export interface FormProps<S extends z.ZodType<any, any>>
|
||||
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
|
||||
/** All your form fields */
|
||||
children?: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText?: string
|
||||
schema?: S
|
||||
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
|
||||
initialValues?: FormikProps<z.infer<S>>["initialValues"]
|
||||
}
|
||||
|
||||
interface OnSubmitResult {
|
||||
FORM_ERROR?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const FORM_ERROR = "FORM_ERROR"
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues || {}}
|
||||
validate={validateZodSchema(schema)}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const { FORM_ERROR, ...otherErrors } = (await onSubmit(values)) || {}
|
||||
|
||||
if (FORM_ERROR) {
|
||||
setFormError(FORM_ERROR)
|
||||
}
|
||||
|
||||
if (Object.keys(otherErrors).length > 0) {
|
||||
setErrors(otherErrors)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ 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>
|
||||
)}
|
||||
|
||||
{submitText && (
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{submitText}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<style global jsx>{`
|
||||
.form > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -1,68 +0,0 @@
|
||||
import { forwardRef, PropsWithoutRef } from "react";
|
||||
import { useFormikContext, ErrorMessage, Field } from "formik";
|
||||
|
||||
export interface LabeledSelectFieldProps
|
||||
extends PropsWithoutRef<JSX.IntrinsicElements["select"]> {
|
||||
/** Field name. */
|
||||
name: string;
|
||||
/** Field label. */
|
||||
label: string;
|
||||
/** Field options. */
|
||||
options: any;
|
||||
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>;
|
||||
}
|
||||
|
||||
export const LabeledSelectField = forwardRef<
|
||||
HTMLSelectElement,
|
||||
LabeledSelectFieldProps
|
||||
>(({ name, label, outerProps, options, ...props }, ref) => {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<Field
|
||||
{...props}
|
||||
disabled={isSubmitting}
|
||||
ref={ref}
|
||||
name={name}
|
||||
as="select"
|
||||
style={{
|
||||
fontSize: "1rem",
|
||||
padding: " 0.25rem 0.4rem",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid purple",
|
||||
marginTop: "0.5rem",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
<option value="" selected disabled hidden>
|
||||
Select {label}
|
||||
</option>
|
||||
{options.map((option, index) => (
|
||||
<option key={index} value={option.id}>
|
||||
{option[name]}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</label>
|
||||
|
||||
<ErrorMessage name={name}>
|
||||
{(msg) => (
|
||||
<div role="alert" style={{ color: "red" }}>
|
||||
{msg}
|
||||
</div>
|
||||
)}
|
||||
</ErrorMessage>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default LabeledSelectField;
|
||||
@@ -1,55 +0,0 @@
|
||||
import { forwardRef, 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 = 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