Strongly type form components

Strongly type form components
This commit is contained in:
Timothy Armes
2023-10-10 15:19:01 +02:00
committed by Owen Rees
parent 3203c8af58
commit f2a245ba6d
14 changed files with 260 additions and 88 deletions
@@ -74,17 +74,24 @@ const TournamentForm = () => {
<div className="col-span-4"> <div className="col-span-4">
<TextField <TextField
name="tournament.tournament" name="tournament.tournament"
control={form.control}
label={t("tournamentNameLabel")} label={t("tournamentNameLabel")}
placeholder={t("tournamentNamePlaceholder")} placeholder={t("tournamentNamePlaceholder")}
required required
/> />
</div> </div>
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<DateField name="tournament.date" label={t("dateLabel")} required /> <DateField
name="tournament.date"
control={form.control}
label={t("dateLabel")}
required
/>
</div> </div>
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="tournament.url" name="tournament.url"
control={form.control}
label={t("urlLabel")} label={t("urlLabel")}
placeholder={t("urlPlaceholder")} placeholder={t("urlPlaceholder")}
required required
@@ -93,6 +100,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<SelectField <SelectField
name="tournament.time_control" name="tournament.time_control"
control={form.control}
label={t("tcLabel")} label={t("tcLabel")}
options={[ options={[
TimeControl.Classic, TimeControl.Classic,
@@ -108,12 +116,14 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<SwitchField <SwitchField
name="tournament.norm_tournament" name="tournament.norm_tournament"
control={form.control}
label={t("normLabel")} label={t("normLabel")}
/> />
</div> </div>
<div className="col-span-4"> <div className="col-span-4">
<TextField <TextField
name="tournament.address" name="tournament.address"
control={form.control}
label={t("addressLabel")} label={t("addressLabel")}
placeholder={t("addressPlaceholder")} placeholder={t("addressPlaceholder")}
required required
@@ -122,6 +132,7 @@ const TournamentForm = () => {
<div className="col-span-4 sm:col-span-2"> <div className="col-span-4 sm:col-span-2">
<TextField <TextField
name="tournament.town" name="tournament.town"
control={form.control}
label={t("townLabel")} label={t("townLabel")}
placeholder={t("townPlaceholder")} placeholder={t("townPlaceholder")}
required required
@@ -130,6 +141,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="tournament.department" name="tournament.department"
control={form.control}
label={t("departmentLabel")} label={t("departmentLabel")}
placeholder={t("departmentPlaceholder")} placeholder={t("departmentPlaceholder")}
required required
@@ -138,6 +150,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="tournament.country" name="tournament.country"
control={form.control}
label={t("countryLabel")} label={t("countryLabel")}
placeholder={t("countryPlaceholder")} placeholder={t("countryPlaceholder")}
required required
@@ -146,6 +159,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="name" name="name"
control={form.control}
label={t("yourNameLabel")} label={t("yourNameLabel")}
placeholder={t("yourNamePlaceholder")} placeholder={t("yourNamePlaceholder")}
required required
@@ -154,6 +168,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="email" name="email"
control={form.control}
label={t("yourEmailLabel")} label={t("yourEmailLabel")}
placeholder={t("yourEmailPlaceholder")} placeholder={t("yourEmailPlaceholder")}
required required
@@ -162,6 +177,7 @@ const TournamentForm = () => {
<div className="col-span-4 row-span-2 sm:col-span-2"> <div className="col-span-4 row-span-2 sm:col-span-2">
<TextAreaField <TextAreaField
name="message" name="message"
control={form.control}
label={t("messageLabel")} label={t("messageLabel")}
rows={6} rows={6}
placeholder={t("messagePlaceholder")} placeholder={t("messagePlaceholder")}
@@ -171,6 +187,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="tournament.coordinates.0" name="tournament.coordinates.0"
control={form.control}
label={t("latLabel")} label={t("latLabel")}
type="number" type="number"
required required
@@ -179,6 +196,7 @@ const TournamentForm = () => {
<div className="col-span-2 sm:col-span-1"> <div className="col-span-2 sm:col-span-1">
<TextField <TextField
name="tournament.coordinates.1" name="tournament.coordinates.1"
control={form.control}
label={t("lngLabel")} label={t("lngLabel")}
type="number" type="number"
required required
@@ -55,6 +55,7 @@ const ContactForm = () => {
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<TextField <TextField
name="email" name="email"
control={form.control}
label={t("emailLabel")} label={t("emailLabel")}
placeholder={t("emailPlaceholder")} placeholder={t("emailPlaceholder")}
required required
@@ -62,6 +63,7 @@ const ContactForm = () => {
<TextField <TextField
name="subject" name="subject"
control={form.control}
label={t("subjectLabel")} label={t("subjectLabel")}
placeholder={t("subjectPlaceholder")} placeholder={t("subjectPlaceholder")}
required required
@@ -69,6 +71,7 @@ const ContactForm = () => {
<TextAreaField <TextAreaField
name="message" name="message"
control={form.control}
rows={6} rows={6}
label={t("messageLabel")} label={t("messageLabel")}
placeholder={t("messagePlaceholder")} placeholder={t("messagePlaceholder")}
+4
View File
@@ -107,6 +107,7 @@ export const ManualEloForm = () => {
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
<TextField <TextField
name="currentElo" name="currentElo"
control={form.control}
label={t("currentEloLabel")} label={t("currentEloLabel")}
placeholder={t("currentEloPlaceholder")} placeholder={t("currentEloPlaceholder")}
type="number" type="number"
@@ -115,6 +116,7 @@ export const ManualEloForm = () => {
<SelectField <SelectField
name="kFactor" name="kFactor"
control={form.control}
label={t("yourKFactorLabel")} label={t("yourKFactorLabel")}
options={["40", "20", "10"].map((k) => ({ options={["40", "20", "10"].map((k) => ({
value: k, value: k,
@@ -139,6 +141,7 @@ export const ManualEloForm = () => {
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
<TextField <TextField
name={`games.${i}.opponentElo`} name={`games.${i}.opponentElo`}
control={form.control}
placeholder={t("opponentEloPlaceholder")} placeholder={t("opponentEloPlaceholder")}
type="number" type="number"
required required
@@ -147,6 +150,7 @@ export const ManualEloForm = () => {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<RadioGroupField <RadioGroupField
name={`games.${i}.result`} name={`games.${i}.result`}
control={form.control}
options={["win", "draw", "loss"].map((result) => ({ options={["win", "draw", "loss"].map((result) => ({
value: result, value: result,
label: t("gameResult", { result }), label: t("gameResult", { result }),
+3
View File
@@ -185,6 +185,7 @@ export default function Elo() {
<div className="flex items-end gap-4"> <div className="flex items-end gap-4">
<TextField <TextField
name="url" name="url"
control={form.control}
label={t("resultsUrlLabel")} label={t("resultsUrlLabel")}
placeholder={t("resultsUrlLabel")} placeholder={t("resultsUrlLabel")}
/> />
@@ -222,6 +223,7 @@ export default function Elo() {
<div className="grid w-full grid-cols-1 gap-2 sm:grid-cols-2"> <div className="grid w-full grid-cols-1 gap-2 sm:grid-cols-2">
<SelectField <SelectField
name="player" name="player"
control={form.control}
required required
label={t("choosePlayerLabel")} label={t("choosePlayerLabel")}
placeholder={t("choosePlayerPlaceholder")} placeholder={t("choosePlayerPlaceholder")}
@@ -231,6 +233,7 @@ export default function Elo() {
<SelectField <SelectField
name="kFactor" name="kFactor"
control={form.control}
label={t("kFactorLabel")} label={t("kFactorLabel")}
options={["40", "20", "10"].map((k) => ({ options={["40", "20", "10"].map((k) => ({
value: k, value: k,
+26 -9
View File
@@ -6,26 +6,41 @@ import { get, range } from "lodash";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import { registerLocale } 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 { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "../Field"; import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "../Field";
import { InputDatePicker } from "./components"; import { InputDatePicker } from "./components";
import { DatePickerCustomHeader } from "./components/DatePickerCustomHeader"; import { DatePickerCustomHeader } from "./components/DatePickerCustomHeader";
registerLocale("fr", fr); registerLocale("fr", fr);
type DateFieldProps = FieldProps & { type DateFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> & {
maxDate?: Date; maxDate?: Date;
minDate?: Date; minDate?: Date;
dateFormat?: string; dateFormat?: string;
className?: string; className?: string;
datePickerPopperClass?: string; datePickerPopperClass?: string;
required?: boolean; required?: boolean;
}; }
>;
export const DateField = ({ export const DateField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
minDate, minDate,
maxDate, maxDate,
dateFormat = "dd/MM/yyyy", dateFormat = "dd/MM/yyyy",
@@ -33,15 +48,17 @@ export const DateField = ({
datePickerPopperClass, datePickerPopperClass,
name, name,
control,
required,
...otherFieldProps ...otherFieldProps
}: DateFieldProps) => { }: DateFieldProps<TFieldValues, TFieldName>) => {
const locale = useLocale(); const locale = useLocale();
const at = useTranslations("App"); const at = useTranslations("App");
const min = minDate ? minDate.getFullYear() : 1900; const min = minDate ? minDate.getFullYear() : 1900;
const inputRef = useRef(null); const inputRef = useRef(null);
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
} = form; } = form;
@@ -49,10 +66,10 @@ export const DateField = ({
const hasError = name && !!get(errors, name)?.message; const hasError = name && !!get(errors, name)?.message;
return ( return (
<Field name={name} {...otherFieldProps}> <Field name={name} control={control} {...otherFieldProps}>
<div className={twMerge("relative flex w-full flex-col", className)}> <div className={twMerge("relative flex w-full flex-col", className)}>
<Controller <Controller
control={form.control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<DatePicker <DatePicker
+31 -8
View File
@@ -1,14 +1,25 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { get } from "lodash"; import { get } from "lodash";
import { useFormContext } from "react-hook-form"; import {
Control,
FieldPath,
FieldValues,
useFormContext,
} from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Prettify } from "@/types";
import { ErrorMessage } from "./ErrorMessage"; import { ErrorMessage } from "./ErrorMessage";
import { Label } from "./Label"; import { Label } from "./Label";
export type FieldProps = { export type GenericFieldProps<
name: string; TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TFieldName;
control: Control<TFieldValues>;
className?: string; className?: string;
labelClassName?: string; labelClassName?: string;
childrenWrapperClassName?: string; childrenWrapperClassName?: string;
@@ -16,12 +27,21 @@ export type FieldProps = {
hideErrorMessage?: boolean; hideErrorMessage?: boolean;
}; };
export const Field = ( type FieldProps<
props: Omit<FieldProps, "name"> & { TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> & {
required?: boolean; required?: boolean;
name?: string;
children: ReactNode; children: ReactNode;
}, }
>;
export const Field = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: FieldProps<TFieldValues, TFieldName>,
) => { ) => {
const { const {
name, name,
@@ -32,9 +52,12 @@ export const Field = (
children, children,
required, required,
hideErrorMessage = false, hideErrorMessage = false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used to restrict the `name` prop type
control,
} = props; } = props;
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
+6 -2
View File
@@ -1,13 +1,17 @@
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type LabelProps = React.DetailedHTMLProps< import { Prettify } from "@/types";
type LabelProps = Prettify<
React.DetailedHTMLProps<
React.LabelHTMLAttributes<HTMLLabelElement>, React.LabelHTMLAttributes<HTMLLabelElement>,
HTMLLabelElement HTMLLabelElement
> & { > & {
required?: boolean; required?: boolean;
className?: string; className?: string;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
}; }
>;
export const Label = ({ export const Label = ({
required, required,
+30 -11
View File
@@ -1,12 +1,19 @@
import React from "react"; import React from "react";
import { RadioGroup } from "@headlessui/react"; import { RadioGroup } from "@headlessui/react";
import { get, isNil } from "lodash"; import { isNil } from "lodash";
import { useTranslations } from "next-intl"; 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 { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "./Field"; import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field";
export type BaseOption<T = string, D = unknown> = { export type BaseOption<T = string, D = unknown> = {
value: T; value: T;
@@ -15,13 +22,26 @@ export type BaseOption<T = string, D = unknown> = {
data?: D; data?: D;
}; };
export type RadioGroupFieldProps<T = string, D = unknown> = FieldProps & { export type RadioGroupFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
T = string,
D = unknown,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> & {
required?: boolean; required?: boolean;
options: BaseOption<T, D>[]; options: BaseOption<T, D>[];
}; }
>;
export const RadioGroupField = <T extends React.Key = string, D = unknown>({ export const RadioGroupField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
T extends React.Key = string,
D = unknown,
>({
name, name,
control,
className, className,
labelClassName, labelClassName,
childrenWrapperClassName, childrenWrapperClassName,
@@ -30,16 +50,14 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
required, required,
options, options,
}: RadioGroupFieldProps<T, D>) => { }: RadioGroupFieldProps<TFieldValues, TFieldName, T, D>) => {
const t = useTranslations("App"); const t = useTranslations("App");
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
} = form; } = form;
const hasError = !!get(errors, name)?.message;
const valueToOption = (value: T): BaseOption<T, D> => const valueToOption = (value: T): BaseOption<T, D> =>
options.find((o) => o.value === value) ?? { value, label: "" }; options.find((o) => o.value === value) ?? { value, label: "" };
@@ -47,6 +65,7 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
<Field <Field
{...{ {...{
name, name,
control,
className, className,
label, label,
labelClassName, labelClassName,
@@ -57,7 +76,7 @@ export const RadioGroupField = <T extends React.Key = string, D = unknown>({
> >
<Controller <Controller
name={name} name={name}
control={form.control} control={control}
render={({ field: { onChange, value } }) => { render={({ field: { onChange, value } }) => {
const optionValue = isNil(value) ? null : valueToOption(value); const optionValue = isNil(value) ? null : valueToOption(value);
+26 -13
View File
@@ -2,9 +2,13 @@ import React from "react";
import { flatMap, get, isArray, isNil, isObject } from "lodash"; import { flatMap, get, isArray, isNil, isObject } from "lodash";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Controller, useFormContext } from "react-hook-form"; import {
Controller,
FieldPath,
FieldValues,
useFormContext,
} from "react-hook-form";
import Select, { import Select, {
ActionMeta,
ClassNamesConfig, ClassNamesConfig,
GroupBase, GroupBase,
OnChangeValue, OnChangeValue,
@@ -12,7 +16,9 @@ import Select, {
} from "react-select"; } from "react-select";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "./Field"; import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field";
export type BaseOption<T = string, D = unknown> = { export type BaseOption<T = string, D = unknown> = {
value: T; value: T;
@@ -54,7 +60,7 @@ export const classNames = <Option, IsMulti extends boolean = false>(
"dark:border-gray-600 dark:bg-gray-700 dark:text-white", "dark:border-gray-600 dark:bg-gray-700 dark:text-white",
), ),
groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase", groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase",
option: ({ isFocused, isSelected, isDisabled }) => option: ({ isFocused, isDisabled }) =>
twMerge( twMerge(
"px-3 py-2 hover:cursor-pointer", "px-3 py-2 hover:cursor-pointer",
isDisabled && "opacity-50", isDisabled && "opacity-50",
@@ -65,23 +71,30 @@ export const classNames = <Option, IsMulti extends boolean = false>(
}); });
export type SelectFieldProps< export type SelectFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
IsMulti extends boolean = false, IsMulti extends boolean = false,
T = string, T = string,
D = unknown, D = unknown,
> = FieldProps & > = Prettify<
GenericFieldProps<TFieldValues, TFieldName> &
Omit< Omit<
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>, Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>,
"onChange" | "value" | "classNames" "onChange" | "value" | "classNames" | "name"
> & { > & {
required?: boolean; required?: boolean;
}; }
>;
export const SelectField = < export const SelectField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
IsMulti extends boolean = false, IsMulti extends boolean = false,
T = string, T = string,
D = unknown, D = unknown,
>({ >({
name, name,
control,
className, className,
labelClassName, labelClassName,
childrenWrapperClassName, childrenWrapperClassName,
@@ -92,9 +105,9 @@ export const SelectField = <
placeholder, placeholder,
options, options,
...selectProps ...selectProps
}: SelectFieldProps<IsMulti, T, D>) => { }: SelectFieldProps<TFieldValues, TFieldName, IsMulti, T, D>) => {
const t = useTranslations("App"); const t = useTranslations("App");
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
@@ -118,6 +131,7 @@ export const SelectField = <
<Field <Field
{...{ {...{
name, name,
control,
className, className,
label, label,
labelClassName, labelClassName,
@@ -128,11 +142,10 @@ export const SelectField = <
> >
<Controller <Controller
name={name} name={name}
control={form.control} control={control}
render={({ field: { onChange, value } }) => { render={({ field: { onChange, value } }) => {
const onSelectChange = ( const onSelectChange = (
newValue: OnChangeValue<BaseOption<T, D>, IsMulti>, newValue: OnChangeValue<BaseOption<T, D>, IsMulti>,
actionMeta: ActionMeta<BaseOption<T, D>>,
) => { ) => {
if (isNil(newValue)) onChange(null); if (isNil(newValue)) onChange(null);
else if (isArray(newValue)) { else if (isArray(newValue)) {
@@ -143,12 +156,12 @@ export const SelectField = <
const optionValue = isNil(value) const optionValue = isNil(value)
? null ? null
: isArray(value) : isArray(value)
? value.map(valueToOption) ? // @ts-ignore - this is too complex for TS to understand
value.map(valueToOption)
: valueToOption(value); : valueToOption(value);
return ( return (
<Select <Select
isClearable
value={optionValue} value={optionValue}
onChange={onSelectChange} onChange={onSelectChange}
options={options} options={options}
+28 -6
View File
@@ -1,14 +1,35 @@
import React from "react"; import React from "react";
import { Switch, SwitchProps } from "@headlessui/react"; import { Switch, SwitchProps } from "@headlessui/react";
import { Controller, useFormContext } from "react-hook-form"; import {
Controller,
FieldPath,
FieldValues,
useFormContext,
} from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "./Field"; import { Prettify } from "@/types";
export const SwitchField = (props: FieldProps & SwitchProps<"button">) => { import { Field, GenericFieldProps } from "./Field";
type SwitchFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> &
Omit<SwitchProps<"button">, "name">
>;
export const SwitchField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: SwitchFieldProps<TFieldValues, TFieldName>,
) => {
const { const {
name, name,
control,
label, label,
disabled, disabled,
className, className,
@@ -19,12 +40,13 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
...rest ...rest
} = props; } = props;
const form = useFormContext(); const form = useFormContext<TFieldValues>();
return ( return (
<Field <Field
{...{ {...{
name, name,
control,
type: "checkbox", type: "checkbox",
label, label,
disabled, disabled,
@@ -34,7 +56,7 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
> >
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<Controller <Controller
control={form.control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<div className="flex h-[40px] items-center"> <div className="flex h-[40px] items-center">
@@ -49,7 +71,7 @@ export const SwitchField = (props: FieldProps & SwitchProps<"button">) => {
)} )}
{...rest} {...rest}
> >
<span className="ui-checked:translate-x-[19px] ui-not-checked:translate-x-[3px] inline-block h-[14px] w-[14px] transform rounded-full bg-white transition-transform" /> <span className="inline-block h-[14px] w-[14px] transform rounded-full bg-white transition-transform ui-checked:translate-x-[19px] ui-not-checked:translate-x-[3px]" />
</Switch> </Switch>
</div> </div>
)} )}
+30 -11
View File
@@ -1,23 +1,40 @@
import React from "react"; import React from "react";
import { get } from "lodash"; import { get } from "lodash";
import { Controller, useFormContext } from "react-hook-form"; import {
Controller,
FieldPath,
FieldValues,
useFormContext,
} from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "./Field"; import { Prettify } from "@/types";
export const TextAreaField = ( import { Field, GenericFieldProps } from "./Field";
props: FieldProps &
React.HTMLProps<HTMLTextAreaElement> & { type TextAreaFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> &
Omit<React.HTMLProps<HTMLTextAreaElement>, "ref" | "name"> & {
handleChanged?: (props: { name: string }) => void; handleChanged?: (props: { name: string }) => void;
startIcon?: React.ReactNode; startIcon?: React.ReactNode;
}, required?: boolean;
}
>;
export const TextAreaField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: TextAreaFieldProps<TFieldValues, TFieldName>,
) => { ) => {
const { const {
name, name,
control,
label, label,
required,
disabled,
className, className,
labelClassName, labelClassName,
childrenWrapperClassName, childrenWrapperClassName,
@@ -25,10 +42,12 @@ export const TextAreaField = (
startIcon, startIcon,
handleChanged, handleChanged,
required,
disabled,
...rest ...rest
} = props; } = props;
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
@@ -40,8 +59,8 @@ export const TextAreaField = (
<Field <Field
{...{ {...{
name, name,
control,
label, label,
required,
className, className,
labelClassName, labelClassName,
childrenWrapperClassName, childrenWrapperClassName,
@@ -50,7 +69,7 @@ export const TextAreaField = (
> >
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<Controller <Controller
control={form.control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<div className="relative"> <div className="relative">
+27 -8
View File
@@ -1,20 +1,38 @@
import React from "react"; import React from "react";
import { get } from "lodash"; import { get } from "lodash";
import { Controller, useFormContext } from "react-hook-form"; import {
Controller,
FieldPath,
FieldValues,
useFormContext,
} from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Field, FieldProps } from "./Field"; import { Prettify } from "@/types";
export const TextField = ( import { Field, GenericFieldProps } from "./Field";
props: FieldProps &
React.HTMLProps<HTMLInputElement> & { type TextFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> &
Omit<React.HTMLProps<HTMLInputElement>, "ref" | "name"> & {
handleChanged?: (props: { name: string }) => void; handleChanged?: (props: { name: string }) => void;
startIcon?: React.ReactNode; startIcon?: React.ReactNode;
}, }
>;
export const TextField = <
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: TextFieldProps<TFieldValues, TFieldName>,
) => { ) => {
const { const {
name, name,
control,
type = "text", type = "text",
label, label,
required, required,
@@ -29,7 +47,7 @@ export const TextField = (
...rest ...rest
} = props; } = props;
const form = useFormContext(); const form = useFormContext<TFieldValues>();
const { const {
formState: { errors }, formState: { errors },
@@ -57,6 +75,7 @@ export const TextField = (
<Field <Field
{...{ {...{
name, name,
control,
label, label,
required, required,
className, className,
@@ -67,7 +86,7 @@ export const TextField = (
> >
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<Controller <Controller
control={form.control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<div className="relative"> <div className="relative">
+1 -1
View File
@@ -151,7 +151,7 @@
"resultsTitle": "Résultats", "resultsTitle": "Résultats",
"opponentEloPlaceholder": "Classement de l'adversaire", "opponentEloPlaceholder": "Classement de l'adversaire",
"gameResultPlaceholder": "Résultat...", "gameResultPlaceholder": "Résultat...",
"gameResult": "{result, select, win {Victoire} draw {Match nul} loss {Défaite} other {{result}}}", "gameResult": "{result, select, win {Victoire} draw {Nul} loss {Défaite} other {{result}}}",
"addGameButton": "Ajouter un autre résultat de partie", "addGameButton": "Ajouter un autre résultat de partie",
"resultsUrlLabel": "Collez un lien vers la page de résultats de la FFE", "resultsUrlLabel": "Collez un lien vers la page de résultats de la FFE",
+8
View File
@@ -67,3 +67,11 @@ export type ResponseMessage = {
}; };
export type ScrollableElement = Window | HTMLElement; export type ScrollableElement = Window | HTMLElement;
// Prettify takes a type as its argument and returns a new type that has the same properties as the original type,
// but the properties are not intersected. This means that the new type is easier to read and understand.
// https://gist.github.com/palashmon/db68706d4f26d2dbf187e76409905399
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};