Created
July 24, 2020 16:46
-
-
Save bmingles/f6d70449bae9214ef968f6678dfd9762 to your computer and use it in GitHub Desktop.
React + TypeScript ValidatedInput component
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
| import React from 'react' | |
| import { isError, useDependentState } from '../util' | |
| import Bulma from '../bulma' | |
| export type ValidatedValue< | |
| TValue, | |
| TRequired extends boolean | |
| > = TRequired extends true ? TValue : TValue | null | |
| export interface ValidatedInputProps<TValue, TRequired extends boolean> { | |
| value: ValidatedValue<TValue, TRequired> | |
| onChange: (value: ValidatedValue<TValue, TRequired>) => void | |
| parseValue: (str: string) => Error | TValue | |
| placeholder?: string | |
| asString: (value: TValue) => string | |
| required: TRequired | |
| } | |
| /** | |
| * Generic input component for input that needs validation. | |
| * When a user types new text, a given parseValue function | |
| * will be used to attempt to parse the text. If it succeeds, | |
| * the parsed value will be passed to onChange callback. | |
| * If parsing fails, the input will be marked as invalid | |
| * until the text can be parsed. | |
| */ | |
| export const ValidatedInput = <TValue extends any, TRequired extends boolean>({ | |
| value: inputValue, | |
| onChange, | |
| parseValue, | |
| placeholder, | |
| asString, | |
| required, | |
| }: ValidatedInputProps<TValue, TRequired>) => { | |
| const [isValid, setIsValid] = React.useState<boolean>(true) | |
| const valueStr = inputValue == null && !required ? '' : asString(inputValue!) | |
| const [value, setValue] = useDependentState(valueStr) | |
| const onChangeInternal = React.useCallback( | |
| function onChangeInternal({ | |
| currentTarget: { value }, | |
| }: React.ChangeEvent<HTMLInputElement>) { | |
| setValue(value) | |
| if (!required && value === '') { | |
| setIsValid(true) | |
| onChange(null as ValidatedValue<TValue, TRequired>) | |
| return | |
| } | |
| const parsed = parseValue(value) | |
| // if parsing fails, mark as invalid | |
| // and return | |
| if (isError(parsed)) { | |
| setIsValid(false) | |
| return | |
| } | |
| // parsing was successful | |
| setIsValid(true) | |
| onChange(parsed as ValidatedValue<TValue, TRequired>) | |
| }, | |
| [onChange, parseValue, required, setValue] | |
| ) | |
| return ( | |
| <input | |
| className={Bulma.input} | |
| onChange={onChangeInternal} | |
| placeholder={placeholder} | |
| style={{ borderColor: isValid ? undefined : 'red' }} | |
| value={value} | |
| /> | |
| ) | |
| } | |
| /** | |
| * Date to mm/dd/yyyy string. | |
| */ | |
| export function dateString_MMDDYYYY(date: Date): string { | |
| const [year, month, day] = date.toISOString().substr(0, 10).split('-') | |
| return [month, day, year].join('/') | |
| } | |
| /** | |
| * Parse mm/dd/yyyy string to date. | |
| * If parsing fails return an Error. | |
| */ | |
| export function parseDate_MMDDYYYY(str: string): Date | Error { | |
| try { | |
| const [, month, day, year] = str.match(/^(\d{2})\/(\d{2})\/(\d{4})$/)! | |
| const date = new Date(Number(year), Number(month) - 1, Number(day)) | |
| return isNaN(date.valueOf()) ? new Error('') : date | |
| } catch (e) { | |
| return e | |
| } | |
| } | |
| export type DateInputProps<TRequired extends boolean> = { | |
| value: ValidatedValue<Date, TRequired> | |
| onChange: (date: ValidatedValue<Date, TRequired>) => void | |
| placeholder?: string | |
| required?: TRequired | |
| } | |
| export function DateInput<TRequired extends boolean>({ | |
| value, | |
| onChange, | |
| placeholder, | |
| required = true as TRequired, | |
| }: DateInputProps<TRequired>) { | |
| return ( | |
| <ValidatedInput | |
| onChange={onChange} | |
| value={value} | |
| parseValue={parseDate_MMDDYYYY} | |
| placeholder={placeholder} | |
| asString={dateString_MMDDYYYY} | |
| required={required} | |
| /> | |
| ) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment