Skip to content

Instantly share code, notes, and snippets.

@co3moz
Last active October 18, 2019 17:05
Show Gist options
  • Save co3moz/64704e9f7fd6f18663185ae8edb4f9a5 to your computer and use it in GitHub Desktop.
Save co3moz/64704e9f7fd6f18663185ae8edb4f9a5 to your computer and use it in GitHub Desktop.
React validator form component
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,
}
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