-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Enable again all address fields and allow the user to manually fill them / Move form errors to component state #6200
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
Changes from all commits
e6ec489
7e76fb1
99dda79
716818a
a276592
484df9e
9c5adba
81cc28f
c15798e
6e4410f
c5d4d75
4e4abc6
167fc01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import React, {PureComponent} from 'react'; | ||
| import _ from 'underscore'; | ||
| import React, {useEffect, useState, useRef} from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import {LogBox} from 'react-native'; | ||
| import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; | ||
|
|
@@ -8,7 +8,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize'; | |
| import styles from '../styles/styles'; | ||
| import ExpensiTextInput from './ExpensiTextInput'; | ||
| import Log from '../libs/Log'; | ||
| import {getAddressComponent, isAddressValidForVBA} from '../libs/GooglePlacesUtils'; | ||
| import {getAddressComponent} from '../libs/GooglePlacesUtils'; | ||
|
|
||
| // The error that's being thrown below will be ignored until we fork the | ||
| // react-native-google-places-autocomplete repo and replace the | ||
|
|
@@ -23,134 +23,125 @@ const propTypes = { | |
| value: PropTypes.string, | ||
|
|
||
| /** A callback function when the value of this field has changed */ | ||
| onChangeText: PropTypes.func.isRequired, | ||
| onChange: PropTypes.func.isRequired, | ||
|
|
||
| /** Customize the ExpensiTextInput container */ | ||
| containerStyles: PropTypes.arrayOf(PropTypes.object), | ||
|
|
||
| ...withLocalizePropTypes, | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
| value: '', | ||
| containerStyles: null, | ||
| }; | ||
|
|
||
| // Do not convert to class component! It's been tried before and presents more challenges than it's worth. | ||
| // Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400 | ||
| // Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839 | ||
| const AddressSearch = (props) => { | ||
| const googlePlacesRef = useRef(); | ||
| const [displayListViewBorder, setDisplayListViewBorder] = useState(false); | ||
| useEffect(() => { | ||
| if (!googlePlacesRef.current) { | ||
| return; | ||
| } | ||
| class AddressSearch extends PureComponent { | ||
| constructor(props) { | ||
| super(props); | ||
|
|
||
| googlePlacesRef.current.setAddressText(props.value); | ||
| }, []); | ||
| this.state = { | ||
| skippedFirstOnChangeText: false, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having the ref was more intuitive than this and would recommend we go back to that if possible.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion, I don't think it makes it more intuitive, but I'm ok with changing it. If I change it to ref, this would mean that this component should be a functional component again, right? |
||
| displayListViewBorder: false, | ||
| }; | ||
| } | ||
|
|
||
| const saveLocationDetails = (details) => { | ||
| saveLocationDetails(details) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs a doc |
||
| const addressComponents = details.address_components; | ||
| if (isAddressValidForVBA(addressComponents)) { | ||
marcaaron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (addressComponents) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (!addressComponents) {
return;
} |
||
| // Gather the values from the Google details | ||
| const streetNumber = getAddressComponent(addressComponents, 'street_number', 'long_name'); | ||
| const streetName = getAddressComponent(addressComponents, 'route', 'long_name'); | ||
| let city = getAddressComponent(addressComponents, 'locality', 'long_name'); | ||
| if (!city) { | ||
| city = getAddressComponent(addressComponents, 'sublocality', 'long_name'); | ||
| Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: city}); | ||
| const streetNumber = getAddressComponent(addressComponents, 'street_number', 'long_name') || ''; | ||
| const streetName = getAddressComponent(addressComponents, 'route', 'long_name') || ''; | ||
| const addressStreet = `${streetNumber} ${streetName}`.trim(); | ||
| let addressCity = getAddressComponent(addressComponents, 'locality', 'long_name'); | ||
| if (!addressCity) { | ||
| addressCity = getAddressComponent(addressComponents, 'sublocality', 'long_name'); | ||
| Log.hmmm('[AddressSearch] Replacing missing locality with sublocality: ', {address: details.formatted_address, sublocality: addressCity}); | ||
| } | ||
| const state = getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); | ||
| const zipCode = getAddressComponent(addressComponents, 'postal_code', 'long_name'); | ||
|
|
||
| // Trigger text change events for each of the individual fields being saved on the server | ||
| props.onChangeText('addressStreet', `${streetNumber} ${streetName}`); | ||
| props.onChangeText('addressCity', city); | ||
| props.onChangeText('addressState', state); | ||
| props.onChangeText('addressZipCode', zipCode); | ||
| } else { | ||
| // Clear the values associated to the address, so our validations catch the problem | ||
| Log.hmmm('[AddressSearch] Search result failed validation: ', { | ||
| address: details.formatted_address, | ||
| address_components: addressComponents, | ||
| place_id: details.place_id, | ||
| }); | ||
| props.onChangeText('addressStreet', null); | ||
| props.onChangeText('addressCity', null); | ||
| props.onChangeText('addressState', null); | ||
| props.onChangeText('addressZipCode', null); | ||
| const addressZipCode = getAddressComponent(addressComponents, 'postal_code', 'long_name'); | ||
| const addressState = getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name'); | ||
|
|
||
| this.props.onChange(_.pick({ | ||
| addressCity, | ||
| addressState, | ||
| addressZipCode, | ||
| addressStreet, | ||
| }, _.identity)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are trying to pass the key and value for all of these properties to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The object passed to Now that you mentioned, the intention is not obvious by looking at the code. I could add a comment to make it more clear, what do you think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a comment would be very helpful in this case, I missed the overwriting part but that makes sense.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB, maybe would be better without the underscore const values = {};
if (addressCity) {
values.addressCity = addressCity;
}
// etc
if (_.size(values) === 0) {
return;
}
this.props.onChange(values); |
||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <GooglePlacesAutocomplete | ||
| ref={googlePlacesRef} | ||
| fetchDetails | ||
| suppressDefaultStyles | ||
| enablePoweredByContainer={false} | ||
| onPress={(data, details) => { | ||
| saveLocationDetails(details); | ||
|
|
||
| // After we select an option, we set displayListViewBorder to false to prevent UI flickering | ||
| setDisplayListViewBorder(false); | ||
| }} | ||
| query={{ | ||
| key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus', | ||
| language: props.preferredLocale, | ||
| types: 'address', | ||
| components: 'country:us', | ||
| }} | ||
| requestUrl={{ | ||
| useOnPlatform: 'web', | ||
| url: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}api?command=Proxy_GooglePlaces&proxyUrl=`, | ||
| }} | ||
| textInputProps={{ | ||
| InputComp: ExpensiTextInput, | ||
| label: props.label, | ||
| containerStyles: props.containerStyles, | ||
| errorText: props.errorText, | ||
| onChangeText: (text) => { | ||
| const isTextValid = !_.isEmpty(text) && _.isEqual(text, props.value); | ||
|
|
||
| // Ensure whether an address is selected already or has address value initialized. | ||
| if (!_.isEmpty(googlePlacesRef.current.getAddressText()) && !isTextValid) { | ||
| saveLocationDetails({}); | ||
| } | ||
|
|
||
| // If the text is empty, we set displayListViewBorder to false to prevent UI flickering | ||
| if (_.isEmpty(text)) { | ||
| setDisplayListViewBorder(false); | ||
| } | ||
| }, | ||
| }} | ||
| styles={{ | ||
| textInputContainer: [styles.flexColumn], | ||
| listView: [ | ||
| !displayListViewBorder && styles.googleListView, | ||
| displayListViewBorder && styles.borderTopRounded, | ||
| displayListViewBorder && styles.borderBottomRounded, | ||
| displayListViewBorder && styles.mt1, | ||
| styles.overflowAuto, | ||
| styles.borderLeft, | ||
| styles.borderRight, | ||
| ], | ||
| row: [ | ||
| styles.pv4, | ||
| styles.ph3, | ||
| styles.overflowAuto, | ||
| ], | ||
| description: [styles.googleSearchText], | ||
| separator: [styles.googleSearchSeparator], | ||
| }} | ||
| onLayout={(event) => { | ||
| // We use the height of the element to determine if we should hide the border of the listView dropdown | ||
| // to prevent a lingering border when there are no address suggestions. | ||
| // The height of the empty element is 2px (1px height for each top and bottom borders) | ||
| setDisplayListViewBorder(event.nativeEvent.layout.height > 2); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||
| } | ||
|
|
||
| render() { | ||
| return ( | ||
| <GooglePlacesAutocomplete | ||
| fetchDetails | ||
|
Comment on lines
+75
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nab: my IDE is complaining about no
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, there is a mandatory prop "placeholder" in If I add Screen.Recording.2021-11-22.at.11.00.36.AM.movWe could get rid of the error by just doing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was going to suggest just doing |
||
| suppressDefaultStyles | ||
| enablePoweredByContainer={false} | ||
| onPress={(data, details) => { | ||
| this.saveLocationDetails(details); | ||
|
|
||
| // After we select an option, we set displayListViewBorder to false to prevent UI flickering | ||
| this.setState({displayListViewBorder: false}); | ||
| }} | ||
| query={{ | ||
| key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus', | ||
| language: this.props.preferredLocale, | ||
| types: 'address', | ||
| components: 'country:us', | ||
| }} | ||
| requestUrl={{ | ||
| useOnPlatform: 'web', | ||
| url: `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}api?command=Proxy_GooglePlaces&proxyUrl=`, | ||
| }} | ||
| textInputProps={{ | ||
| InputComp: ExpensiTextInput, | ||
| label: this.props.label, | ||
| containerStyles: this.props.containerStyles, | ||
| errorText: this.props.errorText, | ||
| value: this.props.value, | ||
| onChangeText: (text) => { | ||
| // Line of code https://github.com/FaridSafi/react-native-google-places-autocomplete/blob/47d7223dd48f85da97e80a0729a985bbbcee353f/GooglePlacesAutocomplete.js#L148 | ||
| // will call onChangeText passing '' after the component renders the first time, clearing its value. Why? who knows, but we have to skip it. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just me, but I think we should either figure out why it happens or just say we are working around a feature of the library.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But ideally we don't do this at all and there is no explanation necessary 😄
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I guess it is not the best comment. The point is, I know why it happens (like what code in the library is creating the problem), but I don't know the reason why they decided to do that (doesn't make sense to me).
This ^ |
||
| if (this.state.skippedFirstOnChangeText) { | ||
| this.props.onChange({addressStreet: text}); | ||
| } else { | ||
| this.setState({skippedFirstOnChangeText: true}); | ||
| } | ||
|
|
||
| // If the text is empty, we set displayListViewBorder to false to prevent UI flickering | ||
| if (_.isEmpty(text)) { | ||
| this.setState({displayListViewBorder: false}); | ||
| } | ||
| }, | ||
| }} | ||
| styles={{ | ||
| textInputContainer: [styles.flexColumn], | ||
| listView: [ | ||
| !this.state.displayListViewBorder && styles.googleListView, | ||
| this.state.displayListViewBorder && styles.borderTopRounded, | ||
| this.state.displayListViewBorder && styles.borderBottomRounded, | ||
| this.state.displayListViewBorder && styles.mt1, | ||
| styles.overflowAuto, | ||
| styles.borderLeft, | ||
| styles.borderRight, | ||
| ], | ||
| row: [ | ||
| styles.pv4, | ||
| styles.ph3, | ||
| styles.overflowAuto, | ||
| ], | ||
| description: [styles.googleSearchText], | ||
| separator: [styles.googleSearchSeparator], | ||
| }} | ||
| onLayout={(event) => { | ||
| // We use the height of the element to determine if we should hide the border of the listView dropdown | ||
| // to prevent a lingering border when there are no address suggestions. | ||
| // The height of the empty element is 2px (1px height for each top and bottom borders) | ||
| this.setState({displayListViewBorder: event.nativeEvent.layout.height > 2}); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| AddressSearch.propTypes = propTypes; | ||
| AddressSearch.defaultProps = defaultProps; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB, personally, I think it's fine to explicitly pass the props rather than implicitly bind to
this. Perhaps it would be better to not spend time improving things we know we are going to soon replace and only change what we need to accomplish the task at hand.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but I changed this because I started using
this.stateandthis.setStateinclearError. Didn't check this properly, but I thought that passingthis.setStatewould already require a binding so I thought it was just better to bindclearError. (#6200 (comment))