Last active
October 18, 2019 17:05
-
-
Save co3moz/64704e9f7fd6f18663185ae8edb4f9a5 to your computer and use it in GitHub Desktop.
React validator form 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
const PAGE_KEYS_CREATE: React.FC = () => { | |
return <Form | |
submit={(data) => console.log(data)} | |
submitText="Create" | |
fields={{ | |
name: { | |
label: 'Name', | |
type: 'text', | |
validation: GenericTextValidation | |
}, | |
privateKey: { | |
label: 'Private Key Content', | |
type: 'textarea', | |
validation: GenericTextValidation, | |
props: { | |
placeholder: '-----BEGIN RSA PRIVATE KEY-----', | |
rows: 10 | |
} | |
}, | |
life: { | |
label: 'How\'s your life goin?', | |
type: 'select', | |
description: 'We need information about your life (we are not facebook, don\'t worry)', | |
validation: { | |
...GenericValidation.required | |
}, | |
options: { | |
'': '', | |
'truth': 'It really sucks dude', | |
'lying': 'Its awesome, everything works perfectly well' | |
} | |
} | |
}} /> | |
} | |
export const Validation = { | |
notEmpty(v: string) { | |
return !!v; | |
}, | |
shorterThan(me: number) { | |
return (v: string) => v.length > me | |
}, | |
longerThan(me: number) { | |
return (v: string) => v.length < me | |
} | |
}; | |
export const GenericValidation = { | |
required: { | |
'This field is required': Validation.notEmpty, | |
}, | |
maxTextLength: { | |
'Too long (max length: 200)': Validation.longerThan(200) | |
}, | |
minTextLength: { | |
'Too short (min length: 2)': Validation.shorterThan(2) | |
} | |
}; | |
export const GenericTextValidation = { | |
...GenericValidation.required, | |
...GenericValidation.maxTextLength, | |
...GenericValidation.minTextLength, | |
} |
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, { useState, useEffect, TextareaHTMLAttributes, InputHTMLAttributes, SelectHTMLAttributes } from "react" | |
export const Form: React.FC<FormProps> = props => { | |
const [touch, setTouch] = useState({} as any); | |
const [form, setForm] = useState(() => Object.assign({}, props.fill)); | |
const [validation, setValidation] = useState({} as any); | |
const [shouldSummit, setShouldSummit] = useState(false); | |
useEffect(() => { | |
let isOk = true; | |
for (let fieldName in props.fields) { | |
let field: FormField = props.fields[fieldName]; | |
try { | |
let skip = validate(field, form, form[fieldName], touch[fieldName]); | |
validation[fieldName] = skip ? '' : 'ok'; | |
} catch (e) { | |
validation[fieldName] = e; | |
isOk = false; | |
} | |
} | |
setValidation({ ...validation }); | |
if (shouldSummit) { | |
setShouldSummit(false); | |
if (isOk) { | |
props.submit(form); | |
} | |
} | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [form, touch]); | |
return <form autoComplete="off" onSubmit={e => { | |
e.preventDefault(); | |
for (let fieldName in props.fields) { | |
touch[fieldName] = true; | |
} | |
setTouch({ ...touch }); | |
setShouldSummit(true); | |
}}> | |
{Object.keys(props.fields).map(fieldName => { | |
let field: FormField = props.fields[fieldName]; | |
let validateStyle = validation[fieldName] === 'ok' ? ' is-valid' : validation[fieldName] ? ' is-invalid' : ''; | |
let input; | |
if (field.type === 'text' || field.type === 'password') { | |
input = (<input | |
type={field.type} | |
className={"form-control" + validateStyle} | |
id={"form-" + fieldName} | |
value={form[fieldName] || ''} | |
{...field.props} | |
onChange={e => { | |
setTouch({ ...touch, [fieldName]: true }); | |
setForm({ ...form, [fieldName]: e.currentTarget.value }) | |
}} | |
onClick={e => setTouch({ ...touch, [fieldName]: true })} />); | |
} else if (field.type === 'textarea') { | |
input = (<textarea | |
className={"form-control" + validateStyle} | |
id={"form-" + fieldName} | |
value={form[fieldName] || ''} | |
{...field.props} | |
onChange={e => { | |
setTouch({ ...touch, [fieldName]: true }); | |
setForm({ ...form, [fieldName]: e.currentTarget.value }) | |
}} | |
onClick={e => setTouch({ ...touch, [fieldName]: true })} />); | |
} else if (field.type === 'select') { | |
input = (<select | |
className={"form-control" + validateStyle} | |
id={"form-" + fieldName} | |
value={form[fieldName] || ''} | |
{...field.props} | |
onChange={e => { | |
setTouch({ ...touch, [fieldName]: true }); | |
setForm({ ...form, [fieldName]: e.currentTarget.value }) | |
}} | |
onClick={e => setTouch({ ...touch, [fieldName]: true })}> | |
{Object.keys(field.options).map(key => <option value={key} key={key}>{(field as any).options[key]}</option>)} | |
</select>); | |
} | |
return <div key={fieldName} className={"form-group"}> | |
<label htmlFor={"form-" + fieldName}>{field.label}</label> | |
{input} | |
{field.description && <small className="form-text text-muted">{field.description}</small>} | |
{validation[fieldName] !== 'ok' && <div className="invalid-feedback">{validation[fieldName]}</div>} | |
</div> | |
})} | |
<button type="submit" className="btn btn-primary">{props.submitText || 'Submit'}</button> | |
</form> | |
} | |
function validate(field: FormField, form: any, value: string, touch: any) { | |
if (!field.validation || !touch) return 'no-need'; | |
for (let key in field.validation) { | |
if (!field.validation[key](value, form)) { | |
throw key; | |
} | |
} | |
} | |
interface FormProps { | |
fields: FormFields | |
submitText?: string | |
submit: (form: any) => void | |
fill?: any | |
} | |
interface FormFields { | |
[field: string]: FormField | |
} | |
type FormField<T = any> = FormTextField<T> | FormTextareaField<T> | FormSelectField<T>; | |
interface FormTextareaField<T = any> { | |
type: 'textarea' | |
label: string | |
validation?: ValidationRuleSet | |
description?: string | |
props?: TextareaHTMLAttributes<T> | |
} | |
interface FormTextField<T = any> { | |
type: 'text' | 'password' | |
label: string | |
validation?: ValidationRuleSet | |
description?: string | |
props?: InputHTMLAttributes<T> | |
} | |
interface FormSelectField<T = any> { | |
type: 'select' | |
label: string | |
validation?: ValidationRuleSet | |
description?: string | |
props?: SelectHTMLAttributes<T> | |
options: FormSelectOptions | |
} | |
interface FormSelectOptions { | |
[value: string]: string // value => name | |
} | |
interface ValidationRuleSet { | |
[message: string]: ValidationRule | |
} | |
interface ValidationRule { | |
(value: string, form: any): boolean | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment