Skip to content

Instantly share code, notes, and snippets.

@bmingles
Created July 24, 2020 16:46
Show Gist options
  • Save bmingles/f6d70449bae9214ef968f6678dfd9762 to your computer and use it in GitHub Desktop.
Save bmingles/f6d70449bae9214ef968f6678dfd9762 to your computer and use it in GitHub Desktop.
React + TypeScript ValidatedInput component
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