Created
May 13, 2019 18:47
-
-
Save cryptiklemur/a47244a0585e6532fc101518599cec3a to your computer and use it in GitHub Desktop.
This file contains 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
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; | |
} | |
} |
This file contains 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
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> | |
); | |
}; |
This file contains 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 {Length} from 'class-validator'; | |
class Form extends AbstractFields<Form> { | |
@Length(2, 32) | |
public username: string = ''; | |
public constructor() { | |
super(Form); | |
} | |
} |
This file contains 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 {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