Skip to content

Instantly share code, notes, and snippets.

@rawnly
Last active July 4, 2023 17:52
Show Gist options
  • Save rawnly/a86ae349c931a0322902e83032034a3b to your computer and use it in GitHub Desktop.
Save rawnly/a86ae349c931a0322902e83032034a3b to your computer and use it in GitHub Desktop.

Usage

type Payload = { username: string; password: string }
import { useForm, FormProvider } from 'react-hook-form'


function Form() {
  const methods = useForm<Payload>({
    mode: 'all',
  });
  
  return (
    <form onSubmit={methods.handleSubmit(console.log, console.error)}>
      <FormProvider {...methods}>
        <GeneratedForm structure={[
            [{
              name: 'username',
              label: 'Username',
              type: 'text',
              placeholder: 'Enter username'
            }], [{
              name: "password",
              label: "Password",
              type: "password",
              placeholder: "Enter Password"
            }]
         ] satisfies Structure<Payload>} />
      </FormProvider>
    </form>
  )
}
"use client";
import { nanoid } from "nanoid";
import { ClassValue } from "clsx";
import TooltipIcon from "@aquacloud-dev/smartfish-icons/InfoCircleIcon";
import {
Controller,
FieldPath,
FieldValues,
PathValue,
RegisterOptions,
ValidationRule,
useFormContext,
} from "react-hook-form";
import { match } from "ts-pattern";
import CurrencyInput from "./currency-input";
import FormGroup from "./form-group";
import Hint from "./hint";
import Label from "./label";
import { TextArea } from "../base/inputs/input";
import Input from "../base/input";
import * as Tooltip from "../base/tooltip";
import DatePicker from "./date-picker";
import { CurrencySymbols } from "@smartfish/utils/currencies";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../base/select";
import isDate from "date-fns/isDate";
import { cx } from "./utils";
export interface BaseFieldProps<
T extends FieldValues = FieldValues,
K extends FieldPath<T> = FieldPath<T>
> {
label?: string;
name: K;
value?: PathValue<T, K>;
defaultValue?: PathValue<T, K>;
placeholder?: string;
hint?: string;
tooltip?: string | false;
readOnly?: boolean;
disabled?: boolean;
rules?: RegisterOptions<T, K>;
className?: ClassValue;
resize?: boolean;
/**
*
* default 12 / number of fields in the group
*/
span?:
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "10"
| "11"
| "12";
}
export type GenericFieldProps<T extends FieldValues = FieldValues> =
BaseFieldProps<T> & {
type:
| "text"
| "password"
| "email"
| "search"
| "date"
| "url"
| "textarea";
};
export type NumberInputProps<T extends FieldValues = FieldValues> =
BaseFieldProps<T> & {
type: "number";
min?: number;
max?: number;
step?: number;
};
export type CurrencyInputProps<T extends FieldValues = FieldValues> =
BaseFieldProps<T> & {
type: "currency";
currency?: CurrencySymbols;
};
export type SelectInputProps<T extends FieldValues = FieldValues> =
BaseFieldProps<T> & {
type: "select";
portal?: boolean;
options: {
label: string;
value: string;
}[];
};
export type Field<T extends FieldValues = FieldValues> =
| CurrencyInputProps<T>
| SelectInputProps<T>
| NumberInputProps<T>
| GenericFieldProps<T>;
export type Structure<T extends FieldValues = FieldValues> = Field<T>[][];
export function GeneratedForm<T extends FieldValues>({
structure,
}: {
structure: Structure<T>;
}) {
const {
control,
register,
formState: { errors },
} = useFormContext<T>();
return (
<>
{structure.map((row) => (
<div
key={nanoid()}
className="grid grid-cols-12 gap-4"
data-generated-row={nanoid()}
>
{row.map((field, idx) => {
return (
<FormGroup
key={idx}
className={`col-span-${field.span ?? 12 / row.length}`}
>
<Label
className={cx({
"flex items-center justify-start": field.tooltip,
})}
>
{field.label}
{field.tooltip && (
<Tooltip.Root>
<Tooltip.Trigger className="ml-auto">
<TooltipIcon className="w-4 h-4" />
</Tooltip.Trigger>
<Tooltip.Content>{field.tooltip}</Tooltip.Content>
</Tooltip.Root>
)}
</Label>
{match(field)
.with({ type: "select" }, (field) => (
<Controller
name={field.name}
control={control}
rules={field.rules}
render={({ field: f }) => (
<Select
value={f.value}
onValueChange={(s) => f.onChange(s as any)}
defaultValue={field.defaultValue}
>
<SelectTrigger
ref={f.ref}
name={f.name}
onBlur={f.onBlur}
id={f.name}
placeholder={field.placeholder}
>
<SelectValue />
</SelectTrigger>
<SelectContent portal={field.portal}>
{field.options.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
))
.with({ type: "date" }, (field) => (
<Controller
control={control}
name={field.name}
rules={field.rules}
defaultValue={field.defaultValue}
render={({ field: f }) => {
return (
<DatePicker
{...field}
ref={f.ref}
value={f.value}
onValueChange={(s) => f.onChange(s as any)}
className={cx(field.className)}
min={getValidationValue(field.rules?.min)}
max={getValidationValue(field.rules?.max)}
/>
);
}}
/>
))
.with({ type: "currency" }, (field) => (
<Controller
control={control}
name={field.name}
rules={{
validate(p) {
return (p ?? -1) >= 0 || "Invalid cost";
},
}}
render={({ field: f }) => (
<CurrencyInput
value={f.value?.toString()}
placeholder={field.placeholder}
prefix={`${field.currency} `}
onValueChange={(value) =>
f.onChange(
(value !== undefined ? Number(value) : 0) as any
)
}
decimalsLimit={2}
readOnly={field.readOnly}
disabled={field.disabled}
className={cx(field.className)}
/>
)}
/>
))
.with({ type: "textarea" }, (field) => (
<TextArea
{...register(field.name, field.rules)}
{...field}
className={cx(field.className)}
/>
))
.otherwise((field) => (
<Input
{...register(field.name, field.rules)}
{...field}
className={cx(field.className)}
/>
))}
{errors[field.name] && errors?.[field.name]?.message ? (
<Hint error>{(errors as any)[field.name]?.message}</Hint>
) : (
field.hint && <Hint>{field.hint}</Hint>
)}
</FormGroup>
);
})}
</div>
))}
</>
);
}
function getValidationValue(
validation?: ValidationRule<string | number>
): number | Date | undefined {
if (!validation) return;
if (isDate(validation)) return validation as unknown as Date;
if (typeof validation === "number") return validation;
if (typeof validation === "string") {
return new Date(validation).getTime();
}
return getValidationValue(validation.value);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment