Skip to content

Commit

Permalink
Merge pull request #41 from JoviDeCroock/types/generics
Browse files Browse the repository at this point in the history
(feature) - Generic Types
  • Loading branch information
JoviDeCroock authored Jun 24, 2019
2 parents bda1b61 + e06e868 commit 89b17aa
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 133 deletions.
145 changes: 67 additions & 78 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import { deriveInitial } from './helpers/deriveInitial';
import useState from './helpers/useState';
import { Errors, InitialValues, Touched } from './types';

export interface FormOptions {
export interface FormOptions<T> {
enableReinitialize?: boolean;
initialValues?: InitialValues;
mapPropsToValues?: (props: object) => InitialValues;
onError?: (error: object, setFormError: (error: any) => void) => void;
onSuccess?: (result?: any) => void;
onSubmit: (values: object, props: object) => Promise<any> | any;
onSubmit: (values: Partial<T>, props: object) => Promise<any> | any;
shouldSubmitWhenInvalid?: boolean;
validate?: (values: object) => object;
validate?: (values: Partial<T>) => object;
validateOnBlur?: boolean;
validateOnChange?: boolean;
}

const EMPTY_OBJ = {};

const OptionsContainer = ({
const OptionsContainer = <Values extends object>({
enableReinitialize,
initialValues: formInitialValues, // TODO: deprecate
mapPropsToValues,
Expand All @@ -30,31 +30,23 @@ const OptionsContainer = ({
shouldSubmitWhenInvalid,
validateOnBlur,
validateOnChange,
}: FormOptions) => {
}: FormOptions<Values>) => {
const initialValues = formInitialValues || EMPTY_OBJ;
let isDirty = false;

return function FormOuterWrapper(Component: any) {
return function FormOuterWrapper(Component: React.ComponentType<any> | React.FC<any>) {
return function FormWrapper(props: { [property: string]: any }) {
const passDownProps = React.useMemo(() =>
enableReinitialize ? Object.values(props) : [], [enableReinitialize && props]);

const {
0: values,
1: setFieldValue,
2: setValuesState,
} = useState(() => mapPropsToValues ? mapPropsToValues(props) : initialValues);

const {
0: touched,
1: touch,
2: setTouchedState,
} = useState(EMPTY_OBJ);

const {
0: formErrors,
2: setErrorState,
} = useState(EMPTY_OBJ);
const passDownProps = React.useMemo(() => (enableReinitialize ? Object.values(props) : []), [
enableReinitialize && props,
]);

const { 0: values, 1: setFieldValue, 2: setValuesState } = useState(() =>
mapPropsToValues ? mapPropsToValues(props) : initialValues
);

const { 0: touched, 1: touch, 2: setTouchedState } = useState(EMPTY_OBJ);

const { 0: formErrors, 2: setErrorState } = useState(EMPTY_OBJ);

const { 0: isSubmitting, 1: setSubmitting } = React.useState(false);
const { 0: formError, 1: setFormError } = React.useState();
Expand All @@ -74,25 +66,28 @@ const OptionsContainer = ({
setErrorState(EMPTY_OBJ);
}, [...passDownProps]);

const handleSubmit = React.useCallback(async (event?: React.FormEvent<HTMLFormElement>) => {
if (event && event.preventDefault) event.preventDefault();

const errors = validateForm();
setTouchedState(deriveInitial(errors, true));
if (!shouldSubmitWhenInvalid && Object.keys(errors).length > 0) {
return setSubmitting(false);
}

return new Promise(resolve => resolve(onSubmit(values, props)))
.then((result: any) => {
setSubmitting(false);
if (onSuccess) onSuccess(result);
})
.catch((e: any) => {
setSubmitting(false);
if (onError) onError(e, setFormError);
});
}, [values]);
const handleSubmit = React.useCallback(
async (event?: React.FormEvent<HTMLFormElement>) => {
if (event && event.preventDefault) event.preventDefault();

const errors = validateForm();
setTouchedState(deriveInitial(errors, true));
if (!shouldSubmitWhenInvalid && Object.keys(errors).length > 0) {
return setSubmitting(false);
}

return new Promise(resolve => resolve(onSubmit(values, props)))
.then((result: any) => {
setSubmitting(false);
if (onSuccess) onSuccess(result);
})
.catch((e: any) => {
setSubmitting(false);
if (onError) onError(e, setFormError);
});
},
[values]
);

React.useEffect(() => {
if (isSubmitting) handleSubmit();
Expand Down Expand Up @@ -126,42 +121,36 @@ const OptionsContainer = ({
setSubmitting(() => true);
}, []);

const providerValue = React.useMemo(() => ({
errors: formErrors as Errors,
formError,
isDirty,
setFieldTouched,
setFieldValue: onChange,
touched: touched as Touched,
validate: validateForm,
values,
}), [
formErrors,
formError,
isDirty,
setFieldTouched,
onChange,
touched,
validateForm,
values,
]);
const providerValue = React.useMemo(
() => ({
errors: formErrors as Errors,
formError,
isDirty,
setFieldTouched,
setFieldValue: onChange,
touched: touched as Touched,
validate: validateForm,
values,
}),
[formErrors, formError, isDirty, setFieldTouched, onChange, touched, validateForm, values]
);

const comp = React.useMemo(() => (
<Component
change={onChange}
formError={formError}
handleSubmit={submitForm}
isSubmitting={isSubmitting}
resetForm={resetForm}
isDirty={isDirty}
{...props}
/>), [...passDownProps, formError, isSubmitting]);

return (
<Provider value={providerValue}>
{comp}
</Provider>
const comp = React.useMemo(
() => (
<Component
change={onChange}
formError={formError}
handleSubmit={submitForm}
isSubmitting={isSubmitting}
resetForm={resetForm}
isDirty={isDirty}
{...props}
/>
),
[...passDownProps, formError, isSubmitting]
);

return <Provider value={providerValue}>{comp}</Provider>;
};
};
};
Expand Down
34 changes: 18 additions & 16 deletions src/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,28 @@ import * as React from 'react';
import { formContext } from './helpers/context';
import { get } from './helpers/operations';

export interface FieldOperations {
export interface FieldOperations<T> {
onBlur: () => void;
onChange: (value: any) => void;
onChange: (value: T) => void;
onFocus: () => void;
setFieldValue: (fieldId: string, value: any) => void;
setFieldValue: (fieldId: string, value: T) => void;
}

export interface FieldInformation {
export interface FieldInformation<T> {
error: string;
touched: boolean;
value: any;
value: T;
}

export default function useField(fieldId: string): [FieldOperations, FieldInformation] {
export default function useField<T = any>(
fieldId: string
): [FieldOperations<T>, FieldInformation<T>] {
// Dev-check
if (process.env.NODE_ENV !== 'production' && (!fieldId || typeof fieldId !== 'string')) {
throw new Error('The Field needs a valid "fieldId" property to function correctly.');
}
// Context
const {
errors,
values,
setFieldValue,
setFieldTouched,
touched,
} = React.useContext(formContext);
const { errors, values, setFieldValue, setFieldTouched, touched } = React.useContext(formContext);

if (process.env.NODE_ENV !== 'production') {
React.useDebugValue(`${fieldId} Value: ${get(values, fieldId)}`);
Expand All @@ -37,9 +33,15 @@ export default function useField(fieldId: string): [FieldOperations, FieldInform

return [
{
onBlur: React.useCallback(() => { setFieldTouched(fieldId, true); }, []),
onChange: React.useCallback((value: any) => { setFieldValue(fieldId, value); }, []),
onFocus: React.useCallback(() => { setFieldTouched(fieldId, false); }, []),
onBlur: React.useCallback(() => {
setFieldTouched(fieldId, true);
}, []),
onChange: React.useCallback((value: T) => {
setFieldValue(fieldId, value);
}, []),
onFocus: React.useCallback(() => {
setFieldTouched(fieldId, false);
}, []),
setFieldValue,
},
{
Expand Down
98 changes: 59 additions & 39 deletions src/useFieldArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import * as React from 'react';
import { formContext } from './helpers/context';
import { get } from './helpers/operations';

export interface FieldOperations {
add: (item: any) => void;
insert: (at: number, element: object) => void;
export interface FieldOperations<T> {
add: (item: T) => void;
insert: (at: number, element: T) => void;
move: (from: number, to: number) => void;
remove: (toDelete: object | number) => void;
replace: (at: number, element: object) => void;
remove: (toDelete: T | number) => void;
replace: (at: number, element: T) => void;
swap: (first: number, second: number) => void;
}

export interface FieldInformation {
export interface FieldInformation<T> {
error: string | null;
value: any;
value: Array<T>;
}

export default function useFieldArray(fieldId: string): [FieldOperations, FieldInformation] {
export default function useFieldArray<T = any>(
fieldId: string
): [FieldOperations<T>, FieldInformation<T>] {
if (process.env.NODE_ENV !== 'production' && (!fieldId || typeof fieldId !== 'string')) {
throw new Error('The FieldArray needs a valid "fieldId" property to function correctly.');
}
Expand All @@ -31,37 +33,55 @@ export default function useFieldArray(fieldId: string): [FieldOperations, FieldI

return [
{
add: React.useCallback((element: any) => {
setFieldValue(fieldId, [...value, element]);
}, [value]),
insert: React.useCallback((at: number, element: object) => {
const result = [...value];
result.splice(at, 0, element);
setFieldValue(fieldId, result);
}, [value]),
move: React.useCallback((from: number, to: number) => {
const result = [...value];
result.splice(from, 1);
result.splice(to, 0, value[from]);
setFieldValue(fieldId, result);
}, [value]),
remove: React.useCallback((element: object | number) => {
setFieldValue(
fieldId,
value.filter(x => x !== (typeof element === 'number' ? value[element] : element)),
);
}, [value]),
replace: React.useCallback((at: number, element: object) => {
const result = [...value];
result[at] = element;
setFieldValue(fieldId, result);
}, [value]),
swap: React.useCallback((from: number, to: number) => {
const result = [...value];
result[from] = value[to];
result[to] = value[from];
setFieldValue(fieldId, result);
}, [value]),
add: React.useCallback(
(element: T) => {
setFieldValue(fieldId, [...value, element]);
},
[value]
),
insert: React.useCallback(
(at: number, element: T) => {
const result = [...value];
result.splice(at, 0, element);
setFieldValue(fieldId, result);
},
[value]
),
move: React.useCallback(
(from: number, to: number) => {
const result = [...value];
result.splice(from, 1);
result.splice(to, 0, value[from]);
setFieldValue(fieldId, result);
},
[value]
),
remove: React.useCallback(
(element: T | number) => {
setFieldValue(
fieldId,
value.filter(x => x !== (typeof element === 'number' ? value[element] : element))
);
},
[value]
),
replace: React.useCallback(
(at: number, element: T) => {
const result = [...value];
result[at] = element;
setFieldValue(fieldId, result);
},
[value]
),
swap: React.useCallback(
(from: number, to: number) => {
const result = [...value];
result[from] = value[to];
result[to] = value[from];
setFieldValue(fieldId, result);
},
[value]
),
},
{
error: React.useMemo(() => get(errors, fieldId), [errors]),
Expand Down

0 comments on commit 89b17aa

Please sign in to comment.