Last active
December 22, 2018 18:43
-
-
Save malerba118/cdc61f993504e0c11562846960a666a8 to your computer and use it in GitHub Desktop.
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
| export const useField = ( | |
| name, | |
| form, | |
| { defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {} | |
| ) => { | |
| let [value, setValue] = useState(defaultValue); | |
| let [errors, setErrors] = useState([]); | |
| let [pristine, setPristine] = useState(true); | |
| let [validating, setValidating] = useState(false); | |
| let validateCounter = useRef(0); | |
| const validate = async () => { | |
| let validateIteration = ++validateCounter.current; | |
| setValidating(true); | |
| let formData = form.getFormData(); | |
| let errorMessages = await Promise.all( | |
| validations.map(validation => validation(formData, name)) | |
| ); | |
| errorMessages = errorMessages.filter(errorMsg => !!errorMsg); | |
| if (validateIteration === validateCounter.current) { | |
| // this is the most recent invocation | |
| setErrors(errorMessages); | |
| setValidating(false); | |
| } | |
| let fieldValid = errorMessages.length === 0; | |
| return fieldValid; | |
| }; | |
| useEffect( | |
| () => { | |
| if (pristine) return; // Avoid validate on mount | |
| form.validateFields(fieldsToValidateOnChange); | |
| }, | |
| [value] | |
| ); | |
| let field = { | |
| name, | |
| value, | |
| errors, | |
| setErrors, | |
| pristine, | |
| onChange: e => { | |
| if (pristine) { | |
| setPristine(false); | |
| } | |
| setValue(e.target.value); | |
| }, | |
| validate, | |
| validating | |
| }; | |
| form.addField(field); | |
| return field; | |
| }; | |
| export const useForm = ({ onSubmit }) => { | |
| let [submitted, setSubmitted] = useState(false); | |
| let [submitting, setSubmitting] = useState(false); | |
| let fields = []; | |
| const validateFields = async fieldNames => { | |
| let fieldsToValidate; | |
| if (fieldNames instanceof Array) { | |
| fieldsToValidate = fields.filter(field => | |
| fieldNames.includes(field.name) | |
| ); | |
| } else { | |
| //if fieldNames not provided, validate all fields | |
| fieldsToValidate = fields; | |
| } | |
| let fieldsValid = await Promise.all( | |
| fieldsToValidate.map(field => field.validate()) | |
| ); | |
| let formValid = fieldsValid.every(isValid => isValid === true); | |
| return formValid; | |
| }; | |
| const getFormData = () => { | |
| return fields.reduce((formData, f) => { | |
| formData[f.name] = f.value; | |
| return formData; | |
| }, {}); | |
| }; | |
| return { | |
| onSubmit: async e => { | |
| e.preventDefault(); | |
| setSubmitting(true); | |
| setSubmitted(true); // User has attempted to submit form at least once | |
| let formValid = await validateFields(); | |
| let returnVal = await onSubmit(getFormData(), formValid); | |
| setSubmitting(false); | |
| return returnVal; | |
| }, | |
| isValid: () => fields.every(f => f.errors.length === 0), | |
| addField: field => fields.push(field), | |
| getFormData, | |
| validateFields, | |
| submitted, | |
| submitting | |
| }; | |
| }; | |
| const Field = ({ | |
| label, | |
| name, | |
| value, | |
| onChange, | |
| errors, | |
| setErrors, | |
| pristine, | |
| validating, | |
| validate, | |
| formSubmitted, | |
| ...other | |
| }) => { | |
| let showErrors = (!pristine || formSubmitted) && !!errors.length; | |
| return ( | |
| <FormControl className="field" error={showErrors}> | |
| <InputLabel htmlFor={name}>{label}</InputLabel> | |
| <Input | |
| id={name} | |
| value={value} | |
| onChange={onChange} | |
| onBlur={() => !pristine && validate()} | |
| endAdornment={ | |
| <InputAdornment position="end"> | |
| {validating && <LoadingIcon className="rotate" />} | |
| </InputAdornment> | |
| } | |
| {...other} | |
| /> | |
| <FormHelperText component="div"> | |
| {showErrors && | |
| errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)} | |
| </FormHelperText> | |
| </FormControl> | |
| ); | |
| }; | |
| const App = props => { | |
| const form = useForm({ | |
| onSubmit: async (formData, valid) => { | |
| if (!valid) return; | |
| await timeout(2000); // Simulate network time | |
| if (formData.username.length < 10) { | |
| //Simulate 400 response from server | |
| usernameField.setErrors(["Make a longer username"]); | |
| } else { | |
| //Simulate 201 response from server | |
| window.alert( | |
| `form valid: ${valid}, form data: ${JSON.stringify(formData)}` | |
| ); | |
| } | |
| } | |
| }); | |
| const usernameField = useField("username", form, { | |
| defaultValue: "", | |
| validations: [ | |
| async formData => { | |
| await timeout(2000); | |
| return formData.username.length < 6 && "Username already exists"; | |
| } | |
| ], | |
| fieldsToValidateOnChange: [] | |
| }); | |
| const passwordField = useField("password", form, { | |
| defaultValue: "", | |
| validations: [ | |
| formData => | |
| formData.password.length < 6 && "Password must be at least 6 characters" | |
| ], | |
| fieldsToValidateOnChange: ["password", "confirmPassword"] | |
| }); | |
| const confirmPasswordField = useField("confirmPassword", form, { | |
| defaultValue: "", | |
| validations: [ | |
| formData => | |
| formData.password !== formData.confirmPassword && | |
| "Passwords do not match" | |
| ], | |
| fieldsToValidateOnChange: ["password", "confirmPassword"] | |
| }); | |
| let requiredFields = [usernameField, passwordField, confirmPasswordField]; | |
| return ( | |
| <div id="form-container"> | |
| <form onSubmit={form.onSubmit}> | |
| <Field | |
| {...usernameField} | |
| formSubmitted={form.submitted} | |
| label="Username" | |
| /> | |
| <Field | |
| {...passwordField} | |
| formSubmitted={form.submitted} | |
| label="Password" | |
| type="password" | |
| /> | |
| <Field | |
| {...confirmPasswordField} | |
| formSubmitted={form.submitted} | |
| label="Confirm Password" | |
| type="password" | |
| /> | |
| <Button | |
| type="submit" | |
| disabled={ | |
| !form.isValid() || | |
| form.submitting || | |
| requiredFields.some(f => f.pristine) | |
| } | |
| > | |
| {form.submitting ? "Submitting" : "Submit"} | |
| </Button> | |
| </form> | |
| </div> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment