From f2a245ba6d8b6f6fc7905cf18dbb645007011e48 Mon Sep 17 00:00:00 2001 From: Timothy Armes Date: Tue, 10 Oct 2023 15:19:01 +0200 Subject: [PATCH] Strongly type form components Strongly type form components --- .../ajouter-un-tournoi/TournamentForm.tsx | 20 +++++++- app/[locale]/contactez-nous/ContactForm.tsx | 3 ++ app/[locale]/elo/ManualEloForm.tsx | 4 ++ app/[locale]/elo/page.tsx | 3 ++ components/form/DateField/index.tsx | 47 +++++++++++++------ components/form/Field.tsx | 39 +++++++++++---- components/form/Label.tsx | 20 ++++---- components/form/RadioGroupField.tsx | 45 +++++++++++++----- components/form/SelectField.tsx | 47 ++++++++++++------- components/form/SwitchField.tsx | 34 +++++++++++--- components/form/TextAreaField.tsx | 41 +++++++++++----- components/form/TextField.tsx | 35 ++++++++++---- messages/fr.json | 2 +- types.ts | 8 ++++ 14 files changed, 260 insertions(+), 88 deletions(-) diff --git a/app/[locale]/ajouter-un-tournoi/TournamentForm.tsx b/app/[locale]/ajouter-un-tournoi/TournamentForm.tsx index 859c0a5..ea82fe8 100644 --- a/app/[locale]/ajouter-un-tournoi/TournamentForm.tsx +++ b/app/[locale]/ajouter-un-tournoi/TournamentForm.tsx @@ -74,17 +74,24 @@ const TournamentForm = () => {
- +
{
{
{
{
{
{
{
{
{
{
{
{ { {
{ ({ value: k, @@ -139,6 +141,7 @@ export const ManualEloForm = () => {
{
({ value: result, label: t("gameResult", { result }), diff --git a/app/[locale]/elo/page.tsx b/app/[locale]/elo/page.tsx index 5e342f7..ae4c06f 100644 --- a/app/[locale]/elo/page.tsx +++ b/app/[locale]/elo/page.tsx @@ -185,6 +185,7 @@ export default function Elo() {
@@ -222,6 +223,7 @@ export default function Elo() {
({ value: k, diff --git a/components/form/DateField/index.tsx b/components/form/DateField/index.tsx index 27fe57d..750dd8b 100644 --- a/components/form/DateField/index.tsx +++ b/components/form/DateField/index.tsx @@ -6,26 +6,41 @@ import { get, range } from "lodash"; import { useLocale, useTranslations } from "next-intl"; import DatePicker from "react-datepicker"; import { registerLocale } from "react-datepicker"; -import { Controller, useFormContext } from "react-hook-form"; +import { + Controller, + FieldPath, + FieldValues, + useFormContext, +} from "react-hook-form"; import { twMerge } from "tailwind-merge"; -import { Field, FieldProps } from "../Field"; +import { Prettify } from "@/types"; + +import { Field, GenericFieldProps } from "../Field"; import { InputDatePicker } from "./components"; import { DatePickerCustomHeader } from "./components/DatePickerCustomHeader"; registerLocale("fr", fr); -type DateFieldProps = FieldProps & { - maxDate?: Date; - minDate?: Date; - dateFormat?: string; - className?: string; - datePickerPopperClass?: string; - required?: boolean; -}; +type DateFieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +> = Prettify< + GenericFieldProps & { + maxDate?: Date; + minDate?: Date; + dateFormat?: string; + className?: string; + datePickerPopperClass?: string; + required?: boolean; + } +>; -export const DateField = ({ +export const DateField = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +>({ minDate, maxDate, dateFormat = "dd/MM/yyyy", @@ -33,15 +48,17 @@ export const DateField = ({ datePickerPopperClass, name, + control, + required, ...otherFieldProps -}: DateFieldProps) => { +}: DateFieldProps) => { const locale = useLocale(); const at = useTranslations("App"); const min = minDate ? minDate.getFullYear() : 1900; const inputRef = useRef(null); - const form = useFormContext(); + const form = useFormContext(); const { formState: { errors }, } = form; @@ -49,10 +66,10 @@ export const DateField = ({ const hasError = name && !!get(errors, name)?.message; return ( - +
( = FieldPath, +> = { + name: TFieldName; + control: Control; className?: string; labelClassName?: string; childrenWrapperClassName?: string; @@ -16,12 +27,21 @@ export type FieldProps = { hideErrorMessage?: boolean; }; -export const Field = ( - props: Omit & { +type FieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +> = Prettify< + GenericFieldProps & { required?: boolean; - name?: string; children: ReactNode; - }, + } +>; + +export const Field = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, +>( + props: FieldProps, ) => { const { name, @@ -32,9 +52,12 @@ export const Field = ( children, required, hideErrorMessage = false, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used to restrict the `name` prop type + control, } = props; - const form = useFormContext(); + const form = useFormContext(); const { formState: { errors }, diff --git a/components/form/Label.tsx b/components/form/Label.tsx index e7ac14b..80c5b93 100644 --- a/components/form/Label.tsx +++ b/components/form/Label.tsx @@ -1,13 +1,17 @@ import { twMerge } from "tailwind-merge"; -type LabelProps = React.DetailedHTMLProps< - React.LabelHTMLAttributes, - HTMLLabelElement -> & { - required?: boolean; - className?: string; - tooltip?: React.ReactNode; -}; +import { Prettify } from "@/types"; + +type LabelProps = Prettify< + React.DetailedHTMLProps< + React.LabelHTMLAttributes, + HTMLLabelElement + > & { + required?: boolean; + className?: string; + tooltip?: React.ReactNode; + } +>; export const Label = ({ required, diff --git a/components/form/RadioGroupField.tsx b/components/form/RadioGroupField.tsx index 678d0d8..4e7725d 100644 --- a/components/form/RadioGroupField.tsx +++ b/components/form/RadioGroupField.tsx @@ -1,12 +1,19 @@ import React from "react"; import { RadioGroup } from "@headlessui/react"; -import { get, isNil } from "lodash"; +import { isNil } from "lodash"; import { useTranslations } from "next-intl"; -import { Controller, useFormContext } from "react-hook-form"; +import { + Controller, + FieldPath, + FieldValues, + useFormContext, +} from "react-hook-form"; import { twMerge } from "tailwind-merge"; -import { Field, FieldProps } from "./Field"; +import { Prettify } from "@/types"; + +import { Field, GenericFieldProps } from "./Field"; export type BaseOption = { value: T; @@ -15,13 +22,26 @@ export type BaseOption = { data?: D; }; -export type RadioGroupFieldProps = FieldProps & { - required?: boolean; - options: BaseOption[]; -}; +export type RadioGroupFieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, + T = string, + D = unknown, +> = Prettify< + GenericFieldProps & { + required?: boolean; + options: BaseOption[]; + } +>; -export const RadioGroupField = ({ +export const RadioGroupField = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, + T extends React.Key = string, + D = unknown, +>({ name, + control, className, labelClassName, childrenWrapperClassName, @@ -30,16 +50,14 @@ export const RadioGroupField = ({ required, options, -}: RadioGroupFieldProps) => { +}: RadioGroupFieldProps) => { const t = useTranslations("App"); - const form = useFormContext(); + const form = useFormContext(); const { formState: { errors }, } = form; - const hasError = !!get(errors, name)?.message; - const valueToOption = (value: T): BaseOption => options.find((o) => o.value === value) ?? { value, label: "" }; @@ -47,6 +65,7 @@ export const RadioGroupField = ({ ({ > { const optionValue = isNil(value) ? null : valueToOption(value); diff --git a/components/form/SelectField.tsx b/components/form/SelectField.tsx index 6501356..d29bd66 100644 --- a/components/form/SelectField.tsx +++ b/components/form/SelectField.tsx @@ -2,9 +2,13 @@ import React from "react"; import { flatMap, get, isArray, isNil, isObject } from "lodash"; import { useTranslations } from "next-intl"; -import { Controller, useFormContext } from "react-hook-form"; +import { + Controller, + FieldPath, + FieldValues, + useFormContext, +} from "react-hook-form"; import Select, { - ActionMeta, ClassNamesConfig, GroupBase, OnChangeValue, @@ -12,7 +16,9 @@ import Select, { } from "react-select"; import { twMerge } from "tailwind-merge"; -import { Field, FieldProps } from "./Field"; +import { Prettify } from "@/types"; + +import { Field, GenericFieldProps } from "./Field"; export type BaseOption = { value: T; @@ -54,7 +60,7 @@ export const classNames = ( "dark:border-gray-600 dark:bg-gray-700 dark:text-white", ), groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase", - option: ({ isFocused, isSelected, isDisabled }) => + option: ({ isFocused, isDisabled }) => twMerge( "px-3 py-2 hover:cursor-pointer", isDisabled && "opacity-50", @@ -65,23 +71,30 @@ export const classNames = ( }); export type SelectFieldProps< + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, IsMulti extends boolean = false, T = string, D = unknown, -> = FieldProps & - Omit< - Props, IsMulti, GroupBase>>, - "onChange" | "value" | "classNames" - > & { - required?: boolean; - }; +> = Prettify< + GenericFieldProps & + Omit< + Props, IsMulti, GroupBase>>, + "onChange" | "value" | "classNames" | "name" + > & { + required?: boolean; + } +>; export const SelectField = < + TFieldValues extends FieldValues = FieldValues, + TFieldName extends FieldPath = FieldPath, IsMulti extends boolean = false, T = string, D = unknown, >({ name, + control, className, labelClassName, childrenWrapperClassName, @@ -92,9 +105,9 @@ export const SelectField = < placeholder, options, ...selectProps -}: SelectFieldProps) => { +}: SelectFieldProps) => { const t = useTranslations("App"); - const form = useFormContext(); + const form = useFormContext(); const { formState: { errors }, @@ -118,6 +131,7 @@ export const SelectField = < { const onSelectChange = ( newValue: OnChangeValue, IsMulti>, - actionMeta: ActionMeta>, ) => { if (isNil(newValue)) onChange(null); else if (isArray(newValue)) { @@ -143,12 +156,12 @@ export const SelectField = < const optionValue = isNil(value) ? null : isArray(value) - ? value.map(valueToOption) + ? // @ts-ignore - this is too complex for TS to understand + value.map(valueToOption) : valueToOption(value); return (