Skip to content

Instantly share code, notes, and snippets.

@Evanion
Last active September 10, 2019 06:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Evanion/a8d69ae963fed2924f0b4ef394f61515 to your computer and use it in GitHub Desktop.
Save Evanion/a8d69ae963fed2924f0b4ef394f61515 to your computer and use it in GitHub Desktop.
React-native form hook
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>
);
};
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,
};
}
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;
}
@Evanion
Copy link
Author

Evanion commented Sep 4, 2019

There is an issue with the types, in that FormValue only allows strings.
I need to figure out how to do the definitions so that it will allow boolean when the field is a checkbox or radio button.

The code will still work .. it's just the linting that is complaining.

@Evanion
Copy link
Author

Evanion commented Sep 10, 2019

solved the value typedef in last weeks update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment