A react native form hook based on formik. shamelessly taken from Jarred (jaredpalmer/formik#1046)
Last active
September 10, 2019 06:47
-
-
Save Evanion/a8d69ae963fed2924f0b4ef394f61515 to your computer and use it in GitHub Desktop.
React-native form hook
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 React, { useCallback } from 'react'; | |
import { TextInput, View, CheckBox, Button, Text } from 'react-native'; | |
import { useForm } from './form'; | |
export const MyComponent = () => { | |
const onSubmit = useCallback(async val => { | |
console.log('form submitted', val); | |
}, []); | |
const validate = useCallback((val: any) => { | |
console.log('validate', val); | |
const error = {}; | |
if (!val.firstName) { | |
error.firstName = 'First name is required'; | |
} | |
if (!val.age) { | |
error.age = 'Age is required'; | |
} | |
if (!val.eula) { | |
error.eula = 'You need to accept the EULA'; | |
} | |
return error; | |
}, []); | |
const { field, handleSubmit, handleReset, errors, values } = useForm({ | |
validate, | |
validateOnChange: false, | |
onSubmit, | |
}); | |
const fields = { | |
firstName: field('firstName'), | |
age: field('age'), | |
eula: field('eula', 'checkbox'), | |
}; | |
return ( | |
<View> | |
<TextInput | |
style={{ borderColor: '#cccccc', borderWidth: 1 }} | |
{...fields.firstName} | |
/> | |
<TextInput | |
style={{ borderColor: '#cccccc', borderWidth: 1 }} | |
{...fields.age} | |
/> | |
<View | |
style={{ | |
display: 'flex', | |
flexDirection: 'row', | |
alignItems: 'center', | |
}} | |
> | |
<CheckBox {...fields.eula} /> | |
<Text>Accept the EULA</Text> | |
</View> | |
<Button onPress={handleSubmit} title="Send" /> | |
<Button onPress={handleReset} title="Reset" /> | |
<Text>{JSON.stringify(values)}</Text> | |
<Text>{JSON.stringify(errors)}</Text> | |
</View> | |
); | |
}; |
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 { useState, useCallback } from 'react'; | |
import { setNestedObjectValues } from './helpers'; | |
export const FieldType = { | |
checkbox: 'checkbox' as 'checkbox', | |
radio: 'radio' as 'radio', | |
picker: 'picker' as 'picker', | |
text: 'text' as 'text', | |
}; | |
export type FieldType = typeof FieldType[keyof typeof FieldType]; | |
interface FieldProps { | |
onBlur: () => void; | |
} | |
interface TextFieldProps extends FieldProps { | |
value: string; | |
onChangeText: (value: string) => void; | |
} | |
interface BooleanFieldProps extends FieldProps { | |
value: boolean; | |
onValueChange: (value: boolean) => void; | |
} | |
interface PickerFieldProps extends FieldProps { | |
selectedValue: string; | |
onValueChange: (value: string) => void; | |
} | |
type FormValues<V> = V; | |
type OnSubmitFn<V> = | |
| ((values: FormValues<V>) => void) | |
| ((values: FormValues<V>) => Promise<any>); | |
interface Config<V> { | |
initialValues?: FormValues<V>; | |
onSubmit: OnSubmitFn<V>; | |
validateOnBlur?: boolean; | |
validateOnChange?: boolean; | |
resetOnSubmit?: boolean; | |
validate?: (values: FormValues<V>) => any; | |
} | |
export function useForm<V = {}>({ | |
// @ts-ignore | |
initialValues = {}, | |
onSubmit, | |
validateOnBlur = true, | |
validateOnChange = true, | |
resetOnSubmit = true, | |
validate, | |
}: Config<V>) { | |
const [values, updateValues] = useState<V>(initialValues); | |
const [errors, updateErrors] = useState({}); | |
const [touched, updateTouched] = useState({}); | |
const [submitAttemptCount, updateSubmitAttemptCount] = useState(0); | |
const [isSubmitting, updateIsSubmitting] = useState(false); | |
const [isValidating, updateIsValidating] = useState(false); | |
/** | |
* Performs the validation action. | |
*/ | |
const validateForm = useCallback( | |
(vals = values) => { | |
updateIsValidating(true); | |
return Promise.resolve(validate ? validate(vals) : {}).then(e => { | |
updateErrors(e); | |
updateIsValidating(false); | |
}); | |
}, | |
[validate, values], | |
); | |
/** | |
* handles what should happen when the value of the field changes. | |
*/ | |
const onChange = useCallback( | |
(name: string) => (value: any) => { | |
console.log('onChange called', value); | |
updateValues((prevValues: FormValues<V>) => { | |
const newValues = { | |
...prevValues, | |
[name]: value, | |
}; | |
if (validateOnChange && validate) { | |
validateForm(newValues); | |
} | |
return newValues; | |
}); | |
}, | |
[], | |
); | |
/** | |
* actions to be performed when the field blurs | |
*/ | |
const onBlur = useCallback( | |
(name: string) => () => { | |
updateTouched(prevTouched => { | |
const newTouched = { ...prevTouched, [name]: true }; | |
if (validateOnBlur && validate) { | |
validateForm(values); | |
} | |
return newTouched; | |
}); | |
}, | |
[values], | |
); | |
/** | |
* returns an oject with the field props. | |
*/ | |
function field(name: string, type: 'checkbox'): BooleanFieldProps; | |
function field(name: string, type: 'radio'): BooleanFieldProps; | |
function field(name: string, type: 'picker'): PickerFieldProps; | |
function field(name: string): TextFieldProps; | |
function field(name: string, type?: FieldType) { | |
const props = { | |
value: values[name], | |
onBlur: onBlur(name), | |
}; | |
switch (type) { | |
case FieldType.picker: | |
return { | |
onValueChange: onChange(name), | |
selectedValue: values[name], | |
}; | |
case FieldType.checkbox: | |
case FieldType.radio: | |
return { | |
...props, | |
onValueChange: (value: boolean) => { | |
onChange(name); | |
validateForm(values); | |
}, | |
}; | |
default: | |
return { | |
...props, | |
onChangeText: onChange(name), | |
}; | |
} | |
} | |
/** | |
* resets the form to it's initial state | |
*/ | |
const handleReset = () => { | |
updateValues(initialValues); | |
updateErrors({}); | |
updateTouched(setNestedObjectValues(initialValues, false)); | |
updateIsSubmitting(false); | |
updateSubmitAttemptCount(0); | |
}; | |
/** | |
* Submits the form and calls the onSubmit function. | |
*/ | |
const handleSubmit = useCallback(async () => { | |
updateTouched(setNestedObjectValues(values, true)); | |
updateIsSubmitting(true); | |
updateSubmitAttemptCount(count => count++); | |
try { | |
await validateForm(); | |
const error = await onSubmit(values); | |
if (error) { | |
updateErrors(error); | |
} else if (resetOnSubmit) { | |
handleReset(); | |
} | |
updateIsSubmitting(false); | |
} catch (error) { | |
updateErrors(error); | |
updateIsSubmitting(false); | |
} | |
}, [values, onSubmit]); | |
return { | |
values, | |
updateValues, | |
errors, | |
updateErrors, | |
touched, | |
updateTouched, | |
submitAttemptCount, | |
updateSubmitAttemptCount, | |
isSubmitting, | |
updateIsSubmitting, | |
isValidating, | |
validateOnChange, | |
validateOnBlur, | |
field, | |
handleSubmit, | |
handleReset, | |
}; | |
} |
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 const isObject = (obj: any) => obj !== null && typeof obj === 'object'; | |
/** | |
* Recursively a set the same value for all keys and arrays nested object, cloning | |
*/ | |
export function setNestedObjectValues( | |
object = {}, | |
value: any, | |
visited = new WeakMap(), | |
response = {}, | |
) { | |
for (const k of Object.keys(object)) { | |
const val = object[k]; | |
if (isObject(val)) { | |
if (!visited.get(val)) { | |
visited.set(val, true); | |
// In order to keep array values consistent for both dot path and | |
// bracket syntax, we need to check if this is an array so that | |
// this will output { friends: [true] } and not { friends: { "0": true } } | |
response[k] = Array.isArray(val) ? [] : {}; | |
setNestedObjectValues(val, value, visited, response[k]); | |
} | |
} else { | |
response[k] = value; | |
} | |
} | |
return response; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
solved the value typedef in last weeks update