Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 26 additions & 53 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import lodashIsEqual from 'lodash/isEqual';
import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react';
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import * as ValidationUtils from '@libs/ValidationUtils';
import Visibility from '@libs/Visibility';
import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Form} from '@src/types/form';
import type {Network} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
Expand Down Expand Up @@ -41,46 +39,34 @@ function getInitialValueByType(valueType?: ValueTypeKey): InitialDefaultValue {
}
}

type FormProviderOnyxProps = {
/** Contains the form state that must be accessed outside the component */
formState: OnyxEntry<Form>;
type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProps<TFormID> & {
/** Children to render. */
children: ((props: {inputValues: FormOnyxValues<TFormID>}) => ReactNode) | ReactNode;

/** Contains draft values for each input in the form */
draftValues: OnyxEntry<Form>;
/** Callback to validate the form */
validate?: (values: FormOnyxValues<TFormID>) => FormInputErrors<TFormID>;

/** Information about the network */
network: OnyxEntry<Network>;
};

type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProviderOnyxProps &
FormProps<TFormID> & {
/** Children to render. */
children: ((props: {inputValues: FormOnyxValues<TFormID>}) => ReactNode) | ReactNode;

/** Callback to validate the form */
validate?: (values: FormOnyxValues<TFormID>) => FormInputErrors<TFormID>;
/** Should validate function be called when input loose focus */
shouldValidateOnBlur?: boolean;

/** Should validate function be called when input loose focus */
shouldValidateOnBlur?: boolean;
/** Should validate function be called when the value of the input is changed */
shouldValidateOnChange?: boolean;

/** Should validate function be called when the value of the input is changed */
shouldValidateOnChange?: boolean;
/** Whether to remove invisible characters from strings before validation and submission */
shouldTrimValues?: boolean;

/** Whether to remove invisible characters from strings before validation and submission */
shouldTrimValues?: boolean;
/** Styles that will be applied to the submit button only */
submitButtonStyles?: StyleProp<ViewStyle>;

/** Styles that will be applied to the submit button only */
submitButtonStyles?: StyleProp<ViewStyle>;
/** Whether to apply flex to the submit button */
submitFlexEnabled?: boolean;

/** Whether to apply flex to the submit button */
submitFlexEnabled?: boolean;
/** Whether button is disabled */
isSubmitDisabled?: boolean;

/** Whether button is disabled */
isSubmitDisabled?: boolean;

/** Whether HTML is allowed in form inputs */
allowHTML?: boolean;
};
/** Whether HTML is allowed in form inputs */
allowHTML?: boolean;
};

function FormProvider(
{
Expand All @@ -89,17 +75,17 @@ function FormProvider(
shouldValidateOnBlur = true,
shouldValidateOnChange = true,
children,
formState,
network,
enabledWhenOffline = false,
draftValues,
onSubmit,
shouldTrimValues = true,
allowHTML = false,
...rest
}: FormProviderProps,
forwardedRef: ForwardedRef<FormRef>,
) {
const [network] = useOnyx(ONYXKEYS.NETWORK);
const [formState] = useOnyx<OnyxFormKey, Form>(`${formID}`);
const [draftValues] = useOnyx<OnyxFormDraftKey, Form>(`${formID}Draft`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like getting the draftValues using the onyx hook led to the following issue because useOnyx data load on component mount is not instant:

We fixed this by leveraging useOnyx loading metadata and updating the draft value state as soon as the onyx data becomes available.

const {preferredLocale, translate} = useLocalize();
const inputRefs = useRef<InputRefs>({});
const touchedInputs = useRef<Record<string, boolean>>({});
Expand Down Expand Up @@ -404,19 +390,6 @@ function FormProvider(

FormProvider.displayName = 'Form';

export default withOnyx<FormProviderProps, FormProviderOnyxProps>({
network: {
key: ONYXKEYS.NETWORK,
},
// withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any
formState: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
key: ({formID}) => formID as any,
},
draftValues: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
key: (props) => `${props.formID}Draft` as any,
},
})(forwardRef(FormProvider)) as <TFormID extends OnyxFormKey>(props: Omit<FormProviderProps<TFormID> & RefAttributes<FormRef>, keyof FormProviderOnyxProps>) => ReactNode;
export default forwardRef(FormProvider) as <TFormID extends OnyxFormKey>(props: FormProviderProps<TFormID> & RefAttributes<FormRef>) => ReactNode;

export type {FormProviderProps};
20 changes: 17 additions & 3 deletions src/stories/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {Meta, StoryFn} from '@storybook/react';
import React, {useState} from 'react';
import type {ComponentType} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import AddressSearch from '@components/AddressSearch';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
import DatePicker from '@components/DatePicker';
Expand All @@ -18,8 +19,10 @@ import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';
import {defaultStyles} from '@src/styles';
import type {Form} from '@src/types/form';
import type {Network} from '@src/types/onyx';

type FormStory = StoryFn<FormProviderProps>;
type FormStory = StoryFn<FormProviderProps & FormProviderOnyxProps>;

type StorybookFormValues = {
routingNumber?: string;
Expand All @@ -32,6 +35,17 @@ type StorybookFormValues = {
checkbox?: boolean;
};

type FormProviderOnyxProps = {
/** Contains the form state that must be accessed outside the component */
formState: OnyxEntry<Form>;

/** Contains draft values for each input in the form */
draftValues: OnyxEntry<Form>;

/** Information about the network */
network: OnyxEntry<Network>;
};

type StorybookFormErrors = Partial<Record<keyof StorybookFormValues, string>>;

const STORYBOOK_FORM_ID = 'TestForm' as keyof OnyxFormValuesMapping;
Expand All @@ -50,7 +64,7 @@ const story: Meta<typeof FormProvider> = {
},
};

function Template(props: FormProviderProps) {
function Template(props: FormProviderProps & FormProviderOnyxProps) {
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
NetworkConnection.setOfflineStatus(false);
FormActions.setIsLoading(props.formID, !!props.formState?.isLoading);
Expand Down Expand Up @@ -162,7 +176,7 @@ function Template(props: FormProviderProps) {
/**
* Story to exhibit the native event handlers for TextInput in the Form Component
*/
function WithNativeEventHandler(props: FormProviderProps) {
function WithNativeEventHandler(props: FormProviderProps & FormProviderOnyxProps) {
const [log, setLog] = useState('');

// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
Expand Down