-
-
Save Frans06/62d47bd9494b9cfcd3a9ee72868c0617 to your computer and use it in GitHub Desktop.
Tanstack form custom Input field.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Layout | |
| import { | |
| FieldApi, | |
| functionalUpdate, | |
| useField, | |
| type DeepKeys, | |
| type DeepValue, | |
| type FieldAsyncValidateOrFn, | |
| type FieldValidateOrFn, | |
| type FormAsyncValidateOrFn, | |
| type FormValidateOrFn, | |
| } from "@tanstack/react-form"; | |
| import { UseFieldOptions } from "@tanstack/react-form/dist/esm/types"; | |
| import { FunctionComponent, useMemo, type ReactNode } from "react"; | |
| export type InputLayoutProps = { | |
| title?: string | null; | |
| required?: boolean; | |
| icon?: ReactNode; | |
| extraInfo?: ReactNode; | |
| note?: ReactNode; | |
| className?: string; | |
| }; | |
| export type FieldComponentProps< | |
| TParentData, | |
| TName extends DeepKeys<TParentData>, | |
| TData extends DeepValue<TParentData, TName>, | |
| TOnMount extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChange extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChangeAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlur extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlurAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmit extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmitAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TFormOnMount extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChange extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChangeAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnBlur extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnBlurAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmit extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmitAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnServer extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TParentSubmitMeta = {}, | |
| ExtendedApi = {}, | |
| > = { | |
| children: ( | |
| fieldApi: FieldApi< | |
| TParentData, | |
| TName, | |
| TData, | |
| TOnMount, | |
| TOnChange, | |
| TOnChangeAsync, | |
| TOnBlur, | |
| TOnBlurAsync, | |
| TOnSubmit, | |
| TOnSubmitAsync, | |
| TFormOnMount, | |
| TFormOnChange, | |
| TFormOnChangeAsync, | |
| TFormOnBlur, | |
| TFormOnBlurAsync, | |
| TFormOnSubmit, | |
| TFormOnSubmitAsync, | |
| TFormOnServer, | |
| TParentSubmitMeta | |
| > & | |
| ExtendedApi | |
| ) => ReactNode; | |
| } & UseFieldOptions< | |
| TParentData, | |
| TName, | |
| TData, | |
| TOnMount, | |
| TOnChange, | |
| TOnChangeAsync, | |
| TOnBlur, | |
| TOnBlurAsync, | |
| TOnSubmit, | |
| TOnSubmitAsync, | |
| TFormOnMount, | |
| TFormOnChange, | |
| TFormOnChangeAsync, | |
| TFormOnBlur, | |
| TFormOnBlurAsync, | |
| TFormOnSubmit, | |
| TFormOnSubmitAsync, | |
| TFormOnServer, | |
| TParentSubmitMeta | |
| > & | |
| ExtendedApi; | |
| export const InputLayout = (< | |
| TParentData, | |
| TName extends DeepKeys<TParentData>, | |
| TData extends DeepValue<TParentData, TName>, | |
| TOnMount extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChange extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChangeAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlur extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlurAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmit extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmitAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TFormOnMount extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChange extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChangeAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnBlur extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnBlurAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmit extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmitAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnServer extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TParentSubmitMeta = {}, | |
| >({ | |
| children, | |
| ...fieldOptions | |
| }: FieldComponentProps< | |
| TParentData, | |
| TName, | |
| TData, | |
| TOnMount, | |
| TOnChange, | |
| TOnChangeAsync, | |
| TOnBlur, | |
| TOnBlurAsync, | |
| TOnSubmit, | |
| TOnSubmitAsync, | |
| TFormOnMount, | |
| TFormOnChange, | |
| TFormOnChangeAsync, | |
| TFormOnBlur, | |
| TFormOnBlurAsync, | |
| TFormOnSubmit, | |
| TFormOnSubmitAsync, | |
| TFormOnServer, | |
| TParentSubmitMeta, | |
| InputLayoutProps | |
| >): ReactNode => { | |
| const fieldApi = useField(fieldOptions); | |
| const jsxToDisplay = useMemo( | |
| () => functionalUpdate(children, fieldApi as any), | |
| /** | |
| * The reason this exists is to fix an issue with the React Compiler. | |
| * Namely, functionalUpdate is memoized where it checks for `fieldApi`, which is a static type. | |
| * This means that when `state.value` changes, it does not trigger a re-render. The useMemo explicitly fixes this problem | |
| */ | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| [children, fieldApi, fieldApi.state.value, fieldApi.state.meta] | |
| ); | |
| return ( | |
| <div className="flex flex-1 flex-col"> | |
| <div | |
| className={`${fieldOptions.icon || fieldOptions.title ? "pb-4" : ""} flex items-center gap-2`} | |
| > | |
| {fieldOptions.icon && ( | |
| <div className="text-nipon light:text-icon flex"> | |
| {fieldOptions.icon} | |
| </div> | |
| )} | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {fieldOptions.title && ( | |
| <label | |
| htmlFor={fieldOptions.name} | |
| className="text-inter text-nacre text-base font-medium" | |
| > | |
| {`${fieldOptions.title}${fieldOptions.required ? " *" : ""}`} | |
| </label> | |
| )} | |
| {fieldOptions.extraInfo && ( | |
| <p className="text-inter text-smoke light:text-pearl text-base"> | |
| {fieldOptions.extraInfo} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| {jsxToDisplay} | |
| {!fieldApi.state.meta.isValid && ( | |
| <em className="px-5 pt-2 text-red-700" role="alert"> | |
| {fieldApi.state.meta.errors.join(", ")} | |
| </em> | |
| )} | |
| <div> | |
| {fieldOptions.note && ( | |
| <p className="text-inter text-smoke-light light:font-medium pt-4 text-sm font-medium"> | |
| {fieldOptions.note} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }) satisfies FunctionComponent< | |
| FieldComponentProps< | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any, | |
| any | |
| > | |
| >; | |
| // Input field | |
| import { | |
| DeepKeys, | |
| DeepValue, | |
| FieldAsyncValidateOrFn, | |
| FieldValidateOrFn, | |
| FormAsyncValidateOrFn, | |
| FormValidateOrFn, | |
| } from "@tanstack/react-form"; | |
| import type { | |
| HTMLInputTypeAttribute, | |
| KeyboardEventHandler, | |
| Ref, | |
| WheelEvent, | |
| } from "react"; | |
| import { twMerge } from "tailwind-merge"; | |
| import { | |
| FieldComponentProps, | |
| InputLayout, | |
| InputLayoutProps, | |
| } from "./InputLayout"; | |
| type InputProps = { | |
| name: string; | |
| inputRef?: Ref<HTMLInputElement>; | |
| required?: boolean; | |
| autoFocus?: boolean; | |
| type: HTMLInputTypeAttribute; | |
| placeholder?: string | null; | |
| autoComplete?: string; | |
| className?: string; | |
| step?: string | number; | |
| min?: string | number; | |
| onKeyPress?: (e: KeyboardEventHandler) => void; | |
| }; | |
| export const Input = < | |
| TParentData, | |
| TName extends DeepKeys<TParentData>, | |
| TData extends DeepValue<TParentData, TName> & string, | |
| TOnMount extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChange extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnChangeAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlur extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnBlurAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmit extends | |
| | undefined | |
| | FieldValidateOrFn<TParentData, TName, TData> = undefined, | |
| TOnSubmitAsync extends | |
| | undefined | |
| | FieldAsyncValidateOrFn<TParentData, TName, TData> = undefined, | |
| TFormOnMount extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChange extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnChangeAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnBlur extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnBlurAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmit extends undefined | FormValidateOrFn<TParentData> = undefined, | |
| TFormOnSubmitAsync extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TFormOnServer extends | |
| | undefined | |
| | FormAsyncValidateOrFn<TParentData> = undefined, | |
| TParentSubmitMeta = {}, | |
| >({ | |
| title, | |
| name, | |
| inputRef, | |
| required = true, | |
| autoFocus = false, | |
| type, | |
| placeholder, | |
| autoComplete = type, | |
| icon, | |
| extraInfo, | |
| note, | |
| className, | |
| step = 1, | |
| min = 0, | |
| form, | |
| onKeyPress, | |
| }: Omit< | |
| FieldComponentProps< | |
| TParentData, | |
| TName, | |
| TData, | |
| TOnMount, | |
| TOnChange, | |
| TOnChangeAsync, | |
| TOnBlur, | |
| TOnBlurAsync, | |
| TOnSubmit, | |
| TOnSubmitAsync, | |
| TFormOnMount, | |
| TFormOnChange, | |
| TFormOnChangeAsync, | |
| TFormOnBlur, | |
| TFormOnBlurAsync, | |
| TFormOnSubmit, | |
| TFormOnSubmitAsync, | |
| TFormOnServer, | |
| TParentSubmitMeta, | |
| InputLayoutProps & InputProps | |
| >, | |
| "children" | |
| >) => { | |
| const extraPros = { | |
| ...(type === "number" | |
| ? { | |
| onKeyPress: (event) => { | |
| if (!/^\d*\.?\d*$/.test(event.key)) { | |
| event.preventDefault(); | |
| } | |
| }, | |
| } | |
| : { onKeyPress }), | |
| } as { onKeyPress: KeyboardEventHandler<HTMLInputElement> }; | |
| return ( | |
| <InputLayout | |
| name={name} | |
| title={title} | |
| extraInfo={extraInfo} | |
| required={required} | |
| icon={icon} | |
| note={note} | |
| form={form} | |
| > | |
| {(field) => ( | |
| <input | |
| {...extraPros} | |
| id={field.name} | |
| name={field.name} | |
| value={field.state.value as TData} | |
| onBlur={field.handleBlur} | |
| onChange={(e) => field.handleChange(e.target.value as TData)} | |
| ref={inputRef} | |
| data-testid={field.name} | |
| required={required} | |
| autoFocus={autoFocus} | |
| type={type} | |
| step={step} | |
| min={min} | |
| onWheel={(e: WheelEvent<HTMLInputElement>) => e.currentTarget.blur()} | |
| placeholder={placeholder || ""} | |
| autoComplete={autoComplete} | |
| className={twMerge( | |
| "text-inter bg-bokara-gray-light text-nacre caret-nacre placeholder:text-pearl box-border h-10 w-full rounded-full px-5 text-base font-normal outline-hidden", | |
| className | |
| )} | |
| /> | |
| )} | |
| </InputLayout> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment