Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NoQA] From docs refactor #28627

Merged
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
64 changes: 43 additions & 21 deletions contributingGuides/FORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ This document lists specific guidelines for using our Form component and general

## General Form UI/UX

### Inputs
Any form input needs to be wrapped in [InputWrapper](https://github.com/Expensify/App/blob/029d009731dcd3c44cd1321672b9672ef0d3d7d9/src/components/Form/InputWrapper.js) and passed as `InputComponent` property additionally it's necessary po pass an unique `inputID`. All other props of the input can be passed as `InputWrapper` props.
```jsx
<InputWrapper
// `InputWrapper` required props
InputComponent={TextInput}
inputID="uniqueTextInputID"
// `TextInput` specific props
placeholder="Text input placeholder"
label="Text input label"
shouldSaveDraft
/>
```

### Labels, Placeholders, & Hints

Labels are required for each input and should clearly mark the field. Optional text may appear below a field when a hint, suggestion, or context feels necessary. If validation fails on such a field, its error should clearly explain why without relying on the hint. Inline errors should always replace the microcopy hints. Placeholders should not be used as it’s customary for labels to appear inside form fields and animate them above the field when focused.
Expand All @@ -13,7 +27,8 @@ Labels are required for each input and should clearly mark the field. Optional t
Labels and hints are enabled by passing the appropriate props to each input:

```jsx
<TextInput
<InputWrapper
InputComponent={TextInput}
label="Value"
hint="Hint text goes here"
/>
Expand All @@ -24,7 +39,8 @@ Labels and hints are enabled by passing the appropriate props to each input:
If a field has a character limit we should give that field a max limit. This is done by passing the maxLength prop to TextInput.

```jsx
<TextInput
<InputWrapper
InputComponent={TextInput}
maxLength={20}
/>
```
Expand All @@ -42,7 +58,8 @@ We should always set people up for success on native platforms by enabling the b
We have a couple of keyboard types [defined](https://github.com/Expensify/App/blob/572caa9e7cf32a2d64fe0e93d171bb05a1dfb217/src/CONST.js#L357-L360) and should be used like so:

```jsx
<TextInput
<InputWrapper
InputComponent={TextInput}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
/>
```
Expand All @@ -56,7 +73,8 @@ As a best practice we should avoid asking for information we can get via other m
Browsers use the name prop to autofill information into the input. Here's a [reference](https://developers.google.com/web/fundamentals/design-and-ux/input/forms#recommended_input_name_and_autocomplete_attribute_values) for available values for the name prop.

```jsx
<TextInput
<InputWrapper
InputComponent={TextInput}
name="fname"
/>
```
Expand Down Expand Up @@ -91,7 +109,7 @@ To give a slightly more detailed example of how this would work with phone numbe
Form inputs will NOT store draft values by default. This is to avoid accidentally storing any sensitive information like passwords, SSN or bank account information. We need to explicitly tell each form input to save draft values by passing the shouldSaveDraft prop to the input. Saving draft values is highly desirable and we should always try to save draft values. This way when a user continues a given flow they can easily pick up right where they left off if they accidentally exited a flow. Inputs with saved draft values [will be cleared when a user logs out](https://github.com/Expensify/App/blob/aa1f0f34eeba5d761657168255a1ae9aebdbd95e/src/libs/actions/SignInRedirect.js#L52) (like most data). Additionally, we should clear draft data once the form is successfully submitted by calling `Onyx.set(ONYXKEY.FORM_ID, null)` in the onSubmit callback passed to Form.

```jsx
<TextInput
<InputWrapper
shouldSaveDraft
/>
```
Expand Down Expand Up @@ -178,9 +196,9 @@ Submit buttons shall not be disabled or blocked from being pressed in most cases

The only time we won’t allow a user to press the submit button is when we have submitted the form and are waiting for a response (e.g. from the API). In this case we will show a loading indicator and additional taps on the submit button will have no effect. This is handled by the Form component and will also ensure that a form cannot be submitted multiple times.

## Using Form.js
## Using Form

The example below shows how to use [Form.js](https://github.com/Expensify/App/blob/c5a84e5b4c0b8536eed2214298a565e5237a27ca/src/components/Form.js) in our app. You can also refer to [Form.stories.js](https://github.com/Expensify/App/blob/c5a84e5b4c0b8536eed2214298a565e5237a27ca/src/stories/Form.stories.js) for more examples.
The example below shows how to use [FormProvider](https://github.com/Expensify/App/blob/029d009731dcd3c44cd1321672b9672ef0d3d7d9/src/components/Form/FormProvider.js) and [InputWrapper](https://github.com/Expensify/App/blob/029d009731dcd3c44cd1321672b9672ef0d3d7d9/src/components/Form/InputWrapper.js) in our app. You can also refer to [Form.stories.js](https://github.com/Expensify/App/blob/c5a84e5b4c0b8536eed2214298a565e5237a27ca/src/stories/Form.stories.js) for more examples.

```jsx
function validate(values) {
Expand All @@ -201,43 +219,47 @@ function onSubmit(values) {
}, 1000);
}

<Form
<FormProvider
formID="testForm"
submitButtonText="Submit"
validate={this.validate}
onSubmit={this.onSubmit}
>
// Wrapping TextInput in a View to show that Form inputs can be nested in other components
// Wrapping InputWrapper in a View to show that Form inputs can be nested in other components
<View>
<TextInput
<InputWrapper
InputComponent={TextInput}
label="Routing number"
inputID="routingNumber"
maxLength={8}
shouldSaveDraft
/>
</View>
<TextInput
<InputWrapper
InputComponent={TextInput}
label="Account number"
inputID="accountNumber"
containerStyles={[styles.mt4]}
/>
</Form>
</FormProvider>
```

`Form.js` also works with inputs nested in a custom component, e.g. [AddressForm](https://github.com/Expensify/App/blob/86579225ff30b21dea507347735259637a2df461/src/pages/ReimbursementAccount/AddressForm.js). The only exception is that the nested component shouldn't be wrapped around any HoC.
`FormProvider` also works with inputs nested in a custom component, e.g. [AddressForm](https://github.com/Expensify/App/blob/86579225ff30b21dea507347735259637a2df461/src/pages/ReimbursementAccount/AddressForm.js). The only exception is that the nested component shouldn't be wrapped around any HoC and all inputs in the component needs to be wrapped with `InputWrapper`.

```jsx
const BankAccountForm = () => (
<>
<View>
<TextInput
<InputWrapper
InputComponent={TextInput}
label="Routing number"
inputID="routingNumber"
maxLength={8}
shouldSaveDraft
/>
</View>
<TextInput
<InputWrapper
InputComponent={TextInput}
label="Account number"
inputID="accountNumber"
containerStyles={[styles.mt4]}
Expand All @@ -246,14 +268,14 @@ const BankAccountForm = () => (
);

// ...
<Form
<FormProvider
formID="testForm"
submitButtonText="Submit"
validate={this.validate}
onSubmit={this.onSubmit}
>
<BankAccountForm />
</Form>
</FormProvider>
```

### Props provided to Form inputs
Expand All @@ -266,7 +288,7 @@ The following prop is available to form inputs:
- value: The value to show for the input.
- onValueChange: A callback that is called when the input's value changes.

Form.js will automatically provide the following props to any input with the inputID prop.
InputWrapper component will automatically provide the following props to any input with the inputID prop.

- ref: A React ref that must be attached to the input.
- value: The input value.
Expand All @@ -287,13 +309,13 @@ An example of this can be seen in the [ACHContractStep](https://github.com/Expen

### Safe Area Padding

Any `Form.js` that has a button will also add safe area padding by default. If the `<Form/>` is inside a `<ScreenWrapper>` we will want to disable the default safe area padding applied there e.g.
Any `FormProvider.js` that has a button will also add safe area padding by default. If the `<FormProvider>` is inside a `<ScreenWrapper>` we will want to disable the default safe area padding applied there e.g.

```js
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
<Form>
<FormProvider>
{...}
</Form>
</FormProvider>
</ScreenWrapper>
```

Expand Down
41 changes: 26 additions & 15 deletions src/stories/Form.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import Picker from '../components/Picker';
import StatePicker from '../components/StatePicker';
import AddressSearch from '../components/AddressSearch';
import DatePicker from '../components/DatePicker';
import Form from '../components/Form';
import * as FormActions from '../libs/actions/FormActions';
import styles from '../styles/styles';
import CheckboxWithLabel from '../components/CheckboxWithLabel';
import Text from '../components/Text';
import NetworkConnection from '../libs/NetworkConnection';
import CONST from '../CONST';
import InputWrapper from '../components/Form/InputWrapper';
import FormProvider from '../components/Form/FormProvider';

/**
* We use the Component Story Format for writing stories. Follow the docs here:
Expand All @@ -20,8 +21,9 @@ import CONST from '../CONST';
*/
const story = {
title: 'Components/Form',
component: Form,
component: FormProvider,
subcomponents: {
InputWrapper,
TextInput,
AddressSearch,
CheckboxWithLabel,
Expand All @@ -40,36 +42,41 @@ function Template(args) {

return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Form {...args}>
<FormProvider {...args}>
<View>
<TextInput
<InputWrapper
InputComponent={TextInput}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
accessibilityLabel="Routing number"
label="Routing number"
inputID="routingNumber"
shouldSaveDraft
/>
</View>
<TextInput
<InputWrapper
InputComponent={TextInput}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
label="Account number"
accessibilityLabel="Account number"
inputID="accountNumber"
containerStyles={[styles.mt4]}
/>
<AddressSearch
<InputWrapper
InputComponent={AddressSearch}
label="Street"
inputID="street"
containerStyles={[styles.mt4]}
hint="No PO box"
/>
<DatePicker
<InputWrapper
InputComponent={DatePicker}
label="Date of birth"
inputID="dob"
containerStyles={[styles.mt4]}
/>
<View>
<Picker
<InputWrapper
InputComponent={Picker}
label="Fruit"
inputID="pickFruit"
containerStyles={[styles.mt4]}
Expand All @@ -90,7 +97,8 @@ function Template(args) {
]}
/>
</View>
<Picker
<InputWrapper
InputComponent={Picker}
label="Another Fruit"
inputID="pickAnotherFruit"
containerStyles={[styles.mt4]}
Expand All @@ -110,17 +118,19 @@ function Template(args) {
]}
/>
<View style={styles.mt4}>
<StatePicker
<InputWrapper
InputComponent={StatePicker}
inputID="state"
shouldSaveDraft
/>
</View>
<CheckboxWithLabel
<InputWrapper
InputComponent={CheckboxWithLabel}
inputID="checkbox"
style={[styles.mb4, styles.mt5]}
LabelComponent={() => <Text>I accept the Expensify Terms of Service</Text>}
/>
</Form>
</FormProvider>
);
}

Expand All @@ -140,8 +150,9 @@ function WithNativeEventHandler(args) {

return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Form {...args}>
<TextInput
<FormProvider {...args}>
<InputWrapper
InputComponent={TextInput}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
accessibilityLabel="Routing number"
label="Routing number"
Expand All @@ -150,7 +161,7 @@ function WithNativeEventHandler(args) {
shouldSaveDraft
/>
<Text>{`Entered routing number: ${log}`}</Text>
</Form>
</FormProvider>
);
}

Expand Down
Loading