-
Notifications
You must be signed in to change notification settings - Fork 3.5k
[No QA] Create Form and FormActions #6412
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
Merged
Merged
Changes from all commits
Commits
Show all changes
73 commits
Select commit
Hold shift + click to select a range
f5c7bef
create form and actions
luacmartins 5d25971
refactor BankAccountStep and ExpensiTextInput
luacmartins 5fdb65a
add onsubmit and other handlers
luacmartins 6ac6d0e
save progress
luacmartins d89b170
fix props usage in ExpensifyButton and ExpensiTextInput
luacmartins 5847a35
read static property for wrapped component
luacmartins 561de29
add serverError scroll
luacmartins 29dbaf0
merge main
luacmartins 0abd923
move serverError to errors
luacmartins a29c1fb
Merge branch 'main' into cmartins-form
luacmartins bbc0d73
update usage of ExpensifyButton and ExpensifyText
luacmartins 5f7c3cc
update alerts
luacmartins 31a9087
refactor submit to set alerts
luacmartins c8269e2
update expensiformsubmit prop types
luacmartins 0eb5cc4
remove default values from state in expensiform
luacmartins daeb792
remove createRef, rename static property, add server error handling
luacmartins e804028
rename submit component
luacmartins dd1c04a
add EXPENSIFORM_SUBMIT_INPUT static propery
luacmartins 047a2c5
fix tests
luacmartins 1615b8d
move alert and submit component
luacmartins d5ac517
revert changes
luacmartins 9441dc1
finish reverting changes
luacmartins 38f3279
rm navigation
luacmartins d66295f
create ExpensiFormActions
luacmartins 6d643be
refactor ExpensiForm
luacmartins d409e29
remove ExpensiFormFormAlertWithSubmitButton
luacmartins 4ac4b56
fix style
luacmartins bc78349
rename ExpensiForm
luacmartins 489056b
rename submitButtonText and comments
luacmartins 700e7de
Merge branch 'main' into cmartins-form
luacmartins c9a7c1d
rename formName to formID in FormActions
luacmartins a8074ef
rename onSubmit to submit, rename key to inputID and remove getErrorT…
luacmartins 1f87b76
add class method docs
luacmartins 58c278d
update comments and add child onBlur handler
luacmartins 78a8256
remove weird import
luacmartins bfd6926
move submit and add propTypes to TextInput
luacmartins b9fc378
refactor TextInput to use Forms interface
luacmartins 4791aa8
fix TextInput propType for inputID
luacmartins 8aaf7c6
update error message for inputID prop
luacmartins eca774a
create Form story
luacmartins d6d94a7
fix styles
luacmartins 1bd951c
fix value and defaultValue in BaseTextInput
luacmartins 5753b18
expose value to input ref in native
luacmartins 4339f6e
move native ref value to Form component
luacmartins de3dcf1
remove console logs
luacmartins 2afa829
remove changes to BankAccountStep, add more Form stories and fix vali…
luacmartins 1d0ce6f
fix submit class method
luacmartins 0295840
add validate and submit logic to stories
luacmartins 45d6aaf
fix form style
luacmartins c3cf1a4
add comment to stories
luacmartins ff5e498
remove podfile.lock changes
luacmartins 6b860bc
rename previous Form component to SignInPageForm
luacmartins 0c7e37f
fix typo in comments and story args
luacmartins 253f5d5
add docs to setTouchedInput, rm _.keys usage and child onChange, fix …
luacmartins ee401db
crate getInputIDPropTypes util function
luacmartins 5763970
fix defaultValue logic, add draftValue docs and examples to validate …
luacmartins 06b9559
add validation for validationErrors
luacmartins 51af475
move childrenWrapperWithProps out of render
luacmartins 8d073e4
fix error with childrenWrapperWithProps declaration
luacmartins f4202d1
move native ref.value to TextInput
luacmartins 4c31d29
fix js style
luacmartins cf7a5fb
disable param reassign lint rule
luacmartins 73a44ee
fix style
luacmartins 9685fbf
revert previous changes
luacmartins 91b0de0
rm assigning input value in render
luacmartins 98ab01e
fix onyx race condition on submit
luacmartins 032683a
revert race condition changes
luacmartins 76abec7
Merge branch 'main' into cmartins-form
luacmartins 3de3c37
remove eslint comment
luacmartins fd38d13
fix onSubmit handler
luacmartins 25fed0e
add js docs, rm onBlur handler, rm touched input check in onChange, a…
luacmartins ecf7677
rm getValues, add inputValues, update references to inputRefs.value
luacmartins 4c48227
fix jslint
luacmartins File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import React from 'react'; | ||
| import {ScrollView, View} from 'react-native'; | ||
| import PropTypes from 'prop-types'; | ||
| import _ from 'underscore'; | ||
| import {withOnyx} from 'react-native-onyx'; | ||
| import compose from '../libs/compose'; | ||
| import withLocalize, {withLocalizePropTypes} from './withLocalize'; | ||
| import * as FormActions from '../libs/actions/FormActions'; | ||
| import styles from '../styles/styles'; | ||
| import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; | ||
|
|
||
| const propTypes = { | ||
| /** A unique Onyx key identifying the form */ | ||
| formID: PropTypes.string.isRequired, | ||
|
|
||
| /** Text to be displayed in the submit button */ | ||
| submitButtonText: PropTypes.string.isRequired, | ||
|
|
||
| /** Callback to validate the form */ | ||
| validate: PropTypes.func.isRequired, | ||
|
|
||
| /** Callback to submit the form */ | ||
| onSubmit: PropTypes.func.isRequired, | ||
|
|
||
| children: PropTypes.node.isRequired, | ||
|
|
||
| /* Onyx Props */ | ||
|
|
||
| /** Contains the form state that must be accessed outside of the component */ | ||
| formState: PropTypes.shape({ | ||
|
|
||
| /** Controls the loading state of the form */ | ||
| isSubmitting: PropTypes.bool, | ||
|
|
||
| /** Server side error message */ | ||
| serverErrorMessage: PropTypes.string, | ||
| }), | ||
|
|
||
| /** Contains draft values for each input in the form */ | ||
| // eslint-disable-next-line react/forbid-prop-types | ||
| draftValues: PropTypes.object, | ||
|
|
||
| ...withLocalizePropTypes, | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
| formState: { | ||
| isSubmitting: false, | ||
| serverErrorMessage: '', | ||
| }, | ||
| draftValues: {}, | ||
| }; | ||
|
|
||
| class Form extends React.Component { | ||
| constructor(props) { | ||
| super(props); | ||
|
|
||
| this.state = { | ||
| errors: {}, | ||
| }; | ||
|
|
||
| this.inputRefs = {}; | ||
| this.inputValues = {}; | ||
| this.touchedInputs = {}; | ||
|
|
||
| this.setTouchedInput = this.setTouchedInput.bind(this); | ||
| this.validate = this.validate.bind(this); | ||
| this.submit = this.submit.bind(this); | ||
| } | ||
|
|
||
| /** | ||
| * @param {String} inputID - The inputID of the input being touched | ||
| */ | ||
| setTouchedInput(inputID) { | ||
| this.touchedInputs[inputID] = true; | ||
| } | ||
|
|
||
| submit() { | ||
| // Return early if the form is already submitting to avoid duplicate submission | ||
| if (this.props.formState.isSubmitting) { | ||
luacmartins marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
|
|
||
| // Touches all form inputs so we can validate the entire form | ||
| _.each(this.inputRefs, (inputRef, inputID) => ( | ||
| this.touchedInputs[inputID] = true | ||
| )); | ||
|
|
||
| // Validate form and return early if any errors are found | ||
| if (!_.isEmpty(this.validate(this.inputValues))) { | ||
| return; | ||
| } | ||
|
|
||
| // Set loading state and call submit handler | ||
| FormActions.setIsSubmitting(this.props.formID, true); | ||
| this.props.onSubmit(this.inputValues); | ||
| } | ||
|
|
||
| /** | ||
| * @param {Object} values - An object containing the value of each inputID, e.g. {inputID1: value1, inputID2: value2} | ||
| * @returns {Object} - An object containing the errors for each inputID, e.g. {inputID1: error1, inputID2: error2} | ||
| */ | ||
| validate(values) { | ||
| FormActions.setServerErrorMessage(this.props.formID, ''); | ||
| const validationErrors = this.props.validate(values); | ||
luacmartins marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (!_.isObject(validationErrors)) { | ||
| throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); | ||
| } | ||
|
|
||
| const errors = _.pick(validationErrors, (inputValue, inputID) => ( | ||
| Boolean(this.touchedInputs[inputID]) | ||
| )); | ||
| this.setState({errors}); | ||
| return errors; | ||
| } | ||
|
|
||
| /** | ||
| * Loops over Form's children and automatically supplies Form props to them | ||
| * | ||
| * @param {Array} children - An array containing all Form children | ||
| * @returns {React.Component} | ||
| */ | ||
| childrenWrapperWithProps(children) { | ||
luacmartins marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return React.Children.map(children, (child) => { | ||
| // Just render the child if it is not a valid React element, e.g. text within a <Text> component | ||
| if (!React.isValidElement(child)) { | ||
| return child; | ||
| } | ||
|
|
||
| // Depth first traversal of the render tree as the input element is likely to be the last node | ||
| if (child.props.children) { | ||
| return React.cloneElement(child, { | ||
| children: this.childrenWrapperWithProps(child.props.children), | ||
| }); | ||
| } | ||
|
|
||
| // We check if the child has the isFormInput prop. | ||
| // We don't want to pass form props to non form components, e.g. View, Text, etc | ||
| if (!child.props.isFormInput) { | ||
| return child; | ||
| } | ||
|
|
||
| // We clone the child passing down all form props | ||
| const inputID = child.props.inputID; | ||
| const defaultValue = this.props.draftValues[inputID] || child.props.defaultValue; | ||
| this.inputValues[inputID] = defaultValue; | ||
|
|
||
| return React.cloneElement(child, { | ||
| ref: node => this.inputRefs[inputID] = node, | ||
| defaultValue, | ||
| errorText: this.state.errors[inputID] || '', | ||
| onBlur: () => { | ||
| this.setTouchedInput(inputID); | ||
| this.validate(this.inputValues); | ||
| }, | ||
| onChange: (value) => { | ||
luacmartins marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.inputValues[inputID] = value; | ||
| if (child.props.shouldSaveDraft) { | ||
| FormActions.setDraftValues(this.props.formID, {[inputID]: value}); | ||
| } | ||
| this.validate(this.inputValues); | ||
| }, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| return ( | ||
| <> | ||
| <ScrollView | ||
| style={[styles.w100, styles.flex1]} | ||
| contentContainerStyle={styles.flexGrow1} | ||
| keyboardShouldPersistTaps="handled" | ||
| > | ||
| <View style={[this.props.style]}> | ||
| {this.childrenWrapperWithProps(this.props.children)} | ||
| <FormAlertWithSubmitButton | ||
| buttonText={this.props.submitButtonText} | ||
| isAlertVisible={_.size(this.state.errors) > 0 || Boolean(this.props.formState.serverErrorMessage)} | ||
| isLoading={this.props.formState.isSubmitting} | ||
| message={this.props.formState.serverErrorMessage} | ||
| onSubmit={this.submit} | ||
| onFixTheErrorsLinkPressed={() => { | ||
luacmartins marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.inputRefs[_.first(_.keys(this.state.errors))].focus(); | ||
| }} | ||
| containerStyles={[styles.mh0, styles.mt5]} | ||
| /> | ||
| </View> | ||
| </ScrollView> | ||
| </> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Form.propTypes = propTypes; | ||
| Form.defaultProps = defaultProps; | ||
|
|
||
| export default compose( | ||
| withLocalize, | ||
| withOnyx({ | ||
| formState: { | ||
| key: props => props.formID, | ||
| }, | ||
| draftValues: { | ||
| key: props => `${props.formID}DraftValues`, | ||
| }, | ||
| }), | ||
| )(Form); | ||
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * Gets the prop type for inputID | ||
| * | ||
| * @param {Object} props - props passed to the input component | ||
| * @returns {Object} returns an Error object if isFormInput is supplied but inputID is falsey or not a string | ||
| */ | ||
| function getInputIDPropTypes(props) { | ||
| if (!props.isFormInput) { | ||
| return; | ||
| } | ||
| if (!props.inputID) { | ||
| return new Error('InputID is required if isFormInput prop is supplied.'); | ||
| } | ||
| if (typeof props.inputID !== 'string') { | ||
| return new Error(`Invalid prop type ${typeof props.inputID} supplied to inputID. Expecting string.`); | ||
| } | ||
| } | ||
|
|
||
| export { | ||
| // eslint-disable-next-line import/prefer-default-export | ||
| getInputIDPropTypes, | ||
| }; |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import Onyx from 'react-native-onyx'; | ||
|
|
||
| /** | ||
| * @param {String} formID | ||
| * @param {Boolean} isSubmitting | ||
| */ | ||
| function setIsSubmitting(formID, isSubmitting) { | ||
| Onyx.merge(formID, {isSubmitting}); | ||
| } | ||
|
|
||
| /** | ||
| * @param {String} formID | ||
| * @param {Boolean} serverErrorMessage | ||
| */ | ||
| function setServerErrorMessage(formID, serverErrorMessage) { | ||
| Onyx.merge(formID, {serverErrorMessage}); | ||
| } | ||
|
|
||
| /** | ||
| * @param {String} formID | ||
| * @param {Object} draftValues | ||
| */ | ||
| function setDraftValues(formID, draftValues) { | ||
| Onyx.merge(`${formID}DraftValues`, draftValues); | ||
| } | ||
|
|
||
| export { | ||
| setIsSubmitting, | ||
| setServerErrorMessage, | ||
| setDraftValues, | ||
| }; |
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.