Skip to content

Instantly share code, notes, and snippets.

@cryptiklemur
Created May 13, 2019 18:47
Show Gist options
  • Save cryptiklemur/a47244a0585e6532fc101518599cec3a to your computer and use it in GitHub Desktop.
Save cryptiklemur/a47244a0585e6532fc101518599cec3a to your computer and use it in GitHub Desktop.
type newable<T> = new (cls: newable<T>) => T;
export default abstract class AbstractFields<T extends AbstractFields<any>> {
protected constructor(protected readonly cls: newable<T>) {
}
public with(property: keyof T & string, value: string | number): T {
const newInstance: T = new this.cls(this.cls);
for (const [k, v] of Object.entries(this)) {
newInstance[k] = v;
}
newInstance[property as string] = value;
return newInstance;
}
}
export default () => {
const [submissionErrors, setSubmissionErrors] = useState([]);
const {input, handleSubmit, errors, isValid, isSubmitting} = useForm(form, async (fields: ForgotPasswordForm) => {
try {
await Auth.forgotPassword(fields.email);
await router.push(`/confirm?username=${fields.email}&type=forgot-password`);
} catch (e) {
if (e.message === 'Username/client id combination not found.') {
setSubmissionErrors(['No account found with that name.']);
return;
}
setSubmissionErrors(['Unknown Error. Try again later']);
console.error(e);
}
});
return (
<div>
<form onSubmit={handleSubmit}>
<Input size="large" placeholder="Email" {...input('email')}/>
{Object.keys(submissionErrors).length > 0 && <Box className="errors">
<h3>There is an error with your submission:</h3>
<ul>
{submissionErrors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</Box>}
{Object.keys(errors).length > 0 && <Box className="errors">
<h3>There is an error with your submission:</h3>
<ul>
{Object.entries(errors).map(([field, errors]) => {
return errors && errors.map((error, i) => (
<li key={`${field}-${i}`}>{error}</li>
));
})}
</ul>
</Box>}
<Submit disabled={!isValid} loading={isSubmitting}>
Send
</Submit>
</form>
</div>
);
};
import {Length} from 'class-validator';
class Form extends AbstractFields<Form> {
@Length(2, 32)
public username: string = '';
public constructor() {
super(Form);
}
}
import {validate as validateClass} from 'class-validator';
import {useEffect, useReducer, useState} from 'react';
import AbstractFields from '../forms/AbstractFields';
export interface Options<T extends AbstractFields<any>> {
validateOnChange: boolean;
validateOnBlur: boolean;
validateOnSubmit: boolean;
validationGroups: string[];
canSubmit: (fields: T, options: Options<T>) => Promise<boolean>;
validate: (fields: T, options: Options<T>) => Promise<ErrorsInterface>;
}
export interface ErrorsInterface {
[field: string]: string[];
}
export type onSubmit<T extends AbstractFields<any>> = (inputs?: T) => Promise<void>;
export default function useForm<T extends AbstractFields<T>>(
fields: T,
onSubmit: onSubmit<T> = () => Promise.resolve(),
userOptions: Partial<Options<T>> = {},
) {
async function validate(fields: T, options: Options<T>): Promise<ErrorsInterface> {
const validationErrors = await validateClass(fields, {groups: options.validationGroups});
const errors = {};
for (const error of validationErrors) {
errors[error.property] = Object.values(error.constraints);
}
return errors;
}
async function canSubmit(fields: T, options: Options<T>): Promise<boolean> {
return Object.keys(await validate(fields, options)).length === 0;
}
interface State {
fields: T;
}
interface Action {
type: 'setValue';
property: keyof T & string;
value: string | number;
}
function reducer(state: State, action: Action) {
switch (action.type) {
case 'setValue':
return {fields: state.fields.with(action.property, action.value)};
default:
throw new Error();
}
}
const options: Options<T> = {
...{
validateOnChange: false,
validateOnBlur: true,
validateOnSubmit: true,
validationGroups: [],
canSubmit,
validate,
},
...userOptions,
};
const [state, dispatch] = useReducer(reducer, {fields});
const [errors, setErrors] = useState<ErrorsInterface>({});
const [isValid, setIsValid] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
onSubmit(state.fields).then(() => setIsSubmitting(false)).catch(() => setIsSubmitting(false));
}
}, [errors, isSubmitting]);
useEffect(() => {
options.validateOnChange && options.validate(state.fields, options).then(setErrors);
}, [state.fields]);
useEffect(() => {
options.canSubmit(state.fields, options).then(setIsValid);
}, [state.fields]);
const handleSubmit = async (event) => {
if (event) {
event.preventDefault();
}
if (options.validateOnSubmit) {
setErrors(await options.validate(state.fields, options));
}
setIsSubmitting(true);
};
const handleInputChange = async (event, callback) => {
event.persist();
const {[event.target.name]: _, ...restErrors} = errors;
setErrors(restErrors);
dispatch({type: 'setValue', property: event.target.name, value: event.target.value});
typeof callback === 'function' && await callback(event);
};
const handleBlur = async (event, callback) => {
event.persist();
if (!event.target.value) {
return;
}
typeof callback === 'function' && await callback(event);
const validationErrors = await options.validate(state.fields, options);
if (!validationErrors[event.target.name]) {
return;
}
setErrors({...errors, [event.target.name]: validationErrors[event.target.name]});
};
const input = (name: keyof T & string, {onChange = null, onBlur = null} = {}) => {
return {
name,
className: errors[name] && errors[name].length > 0 ? 'hasError' : '',
onBlur: (event) => handleBlur(event, onBlur),
onChange: (event) => handleInputChange(event, onChange),
value: state.fields[name],
};
};
return {handleSubmit, handleInputChange, input, isValid, isSubmitting, errors, state, dispatch};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment