-
Notifications
You must be signed in to change notification settings - Fork 59
Form Validation #239
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
Form Validation #239
Changes from all commits
dc63ad5
064721b
ea247d9
e14b286
cd37cda
35c5a96
8cb5686
cade3eb
d58ee57
3908261
2d56f42
fb18f75
6902c2b
f623214
cf194ec
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,43 +1,46 @@ | ||
| import React, { Component } from 'react' | ||
| import React from 'react' | ||
|
|
||
| import LinkButton from './link-button' | ||
| import StackedPaneDisplay from './stacked-pane-display' | ||
|
|
||
| /** | ||
| * This component handles the existing account display. | ||
| */ | ||
| class ExistingAccountDisplay extends Component { | ||
| render () { | ||
| const { onCancel, onComplete, panes } = this.props | ||
| const paneSequence = [ | ||
| { | ||
| pane: () => <p><LinkButton to='/savedtrips'>Edit my trips</LinkButton></p>, | ||
| title: 'My trips' | ||
| }, | ||
| { | ||
| pane: panes.terms, | ||
| props: { disableCheckTerms: true }, | ||
| title: 'Terms' | ||
| }, | ||
| { | ||
| pane: panes.notifications, | ||
| title: 'Notifications' | ||
| }, | ||
| { | ||
| pane: panes.locations, | ||
| title: 'My locations' | ||
| } | ||
| ] | ||
| const ExistingAccountDisplay = props => { | ||
| // The props include Formik props that provide access to the current user data | ||
| // and to its own blur/change/submit event handlers that automate the state. | ||
| // We forward the props to each pane so that their individual controls | ||
| // can be wired to be managed by Formik. | ||
| const { onCancel, panes } = props | ||
| const paneSequence = [ | ||
| { | ||
| pane: () => <p><LinkButton to='/savedtrips'>Edit my trips</LinkButton></p>, | ||
| title: 'My trips' | ||
| }, | ||
| { | ||
| pane: panes.terms, | ||
| props: { ...props, disableCheckTerms: true }, | ||
| title: 'Terms' | ||
| }, | ||
| { | ||
| pane: panes.notifications, | ||
| props, | ||
| title: 'Notifications' | ||
| }, | ||
| { | ||
| pane: panes.locations, | ||
| props, | ||
| title: 'My locations' | ||
| } | ||
| ] | ||
|
|
||
| return ( | ||
| <StackedPaneDisplay | ||
| onCancel={onCancel} | ||
| onComplete={onComplete} | ||
| paneSequence={paneSequence} | ||
| title='My Account' | ||
| /> | ||
| ) | ||
| } | ||
| return ( | ||
| <StackedPaneDisplay | ||
| onCancel={onCancel} | ||
| paneSequence={paneSequence} | ||
| title='My Account' | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| export default ExistingAccountDisplay | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| import clone from 'lodash/cloneDeep' | ||
| import { Field, FieldArray } from 'formik' | ||
|
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. I'm just realizing that the amount of files in the
Collaborator
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. Good point. Perhaps this? In this PR or a separate PR? user/
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. Might as well do a separate PR. For the |
||
| import memoize from 'lodash.memoize' | ||
| import PropTypes from 'prop-types' | ||
| import React, { Component } from 'react' | ||
| import { | ||
| Button, | ||
| ControlLabel, | ||
| FormControl, | ||
| FormGroup, | ||
|
|
@@ -37,135 +37,112 @@ const NewLocationFormControl = styled(FormControl)` | |
| ` | ||
|
|
||
| // Helper filter functions. | ||
| const isHome = loc => loc.type === 'home' | ||
| const isWork = loc => loc.type === 'work' | ||
| const notHomeOrWork = loc => loc.type !== 'home' && loc.type !== 'work' | ||
| export const isHome = loc => loc.type === 'home' | ||
| export const isWork = loc => loc.type === 'work' | ||
|
|
||
| /** | ||
| * User's saved locations editor. | ||
| * Helper function that adds a new address to the Formik state | ||
| * using the Formik-provided arrayHelpers object. | ||
| */ | ||
| class FavoriteLocationsPane extends Component { | ||
| static propTypes = { | ||
| onUserDataChange: PropTypes.func.isRequired, | ||
| userData: PropTypes.object.isRequired | ||
| function addNewAddress (arrayHelpers, e) { | ||
| const value = (e.target.value || '').trim() | ||
| if (value.length > 0) { | ||
| arrayHelpers.push({ | ||
| address: value, | ||
| icon: 'map-marker', | ||
| type: 'custom' | ||
| }) | ||
|
|
||
| // Empty the input box value so the user can enter their next location. | ||
| e.target.value = '' | ||
| } | ||
| } | ||
|
|
||
| _handleAddNewLocation = e => { | ||
| const value = e.target.value || '' | ||
| if (value.trim().length > 0) { | ||
| const { userData, onUserDataChange } = this.props | ||
| // FIXME: remove assigning [] when null. | ||
| const { savedLocations = [] } = userData | ||
|
|
||
| // Create a copy of savedLocations and add the new location to the copied array. | ||
| const newLocations = clone(savedLocations) | ||
| newLocations.push({ | ||
| address: value.trim(), | ||
| icon: 'map-marker', | ||
| type: 'custom' | ||
| }) | ||
|
|
||
| // Event onChange will trigger after this and before rerender, | ||
| // so DO empty the input box value so the user can enter their next location. | ||
| e.target.value = null | ||
|
|
||
| onUserDataChange({ savedLocations: newLocations }) | ||
| /** | ||
| * User's saved locations editor. | ||
| * TODO: Discuss and improve handling of location details (type, coordinates...). | ||
| */ | ||
| class FavoriteLocationsPane extends Component { | ||
| _handleNewAddressKeyDown = memoize( | ||
| arrayHelpers => e => { | ||
| if (e.keyCode === 13) { | ||
binh-dam-ibigroup marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // On the user pressing enter (keyCode 13) on the new location input, | ||
| // add new address to user's savedLocations... | ||
| addNewAddress(arrayHelpers, e) | ||
|
|
||
| // ... but don't submit the form. | ||
| e.preventDefault() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| _handleAddressChange = memoize( | ||
| location => e => { | ||
| const { userData, onUserDataChange } = this.props | ||
| // FIXME: remove assigning [] when null. | ||
| const { savedLocations = [] } = userData | ||
| const value = e.target.value | ||
| const isValueEmpty = !value || value === '' | ||
| const nonEmptyLocation = isValueEmpty ? null : location | ||
|
|
||
| // Update location address, ohterwise it stalls the input box. | ||
| location.address = value | ||
|
|
||
| // Create a new array for savedLocations. | ||
| let newLocations = [] | ||
|
|
||
| // Add home/work as first entries to the new state only if | ||
| // - user edited home/work to non-empty, or | ||
| // - user edited another location and home/work is in savedLocations. | ||
| const homeLocation = (isHome(location) && nonEmptyLocation) || savedLocations.find(isHome) | ||
| if (homeLocation) newLocations.push(homeLocation) | ||
|
|
||
| const workLocation = (isWork(location) && nonEmptyLocation) || savedLocations.find(isWork) | ||
| if (workLocation) newLocations.push(workLocation) | ||
|
|
||
| // Add the rest if it is not home or work | ||
| // and if the new address of this one is not null or empty. | ||
| newLocations = newLocations.concat(savedLocations | ||
| .filter(notHomeOrWork) | ||
| .filter(loc => loc !== location || !isValueEmpty) | ||
| ) | ||
| ) | ||
|
|
||
| onUserDataChange({ savedLocations: newLocations }) | ||
| _handleNewAddressBlur = memoize( | ||
| arrayHelpers => e => { | ||
| addNewAddress(arrayHelpers, e) | ||
| } | ||
| ) | ||
|
|
||
| render () { | ||
| const { userData } = this.props | ||
| // FIXME: remove assigning [] when null. | ||
| const { savedLocations = [] } = userData | ||
|
|
||
| // Build an 'effective' list of locations for display, | ||
| // where at least one 'home' and one 'work', are always present even if blank. | ||
| // In theory there could be multiple home or work locations. | ||
| // Just pick the first one. | ||
| const homeLocation = savedLocations.find(isHome) || { | ||
| address: null, | ||
| icon: 'home', | ||
| type: 'home' | ||
| } | ||
| const workLocation = savedLocations.find(isWork) || { | ||
| address: null, | ||
| icon: 'briefcase', | ||
| type: 'work' | ||
| } | ||
|
|
||
| const effectiveLocations = [ | ||
| homeLocation, | ||
| workLocation, | ||
| ...savedLocations.filter(notHomeOrWork) | ||
| ] | ||
| const { values: userData } = this.props | ||
| const { savedLocations } = userData | ||
| const homeLocation = savedLocations.find(isHome) | ||
| const workLocation = savedLocations.find(isWork) | ||
|
|
||
| return ( | ||
| <div> | ||
| <ControlLabel>Add the places you frequent often to save time planning trips:</ControlLabel> | ||
|
|
||
| {effectiveLocations.map((loc, index) => ( | ||
| <FormGroup key={index}> | ||
| <InputGroup> | ||
| <StyledAddon title={loc.type}> | ||
| <FontAwesome name={loc.icon} /> | ||
| </StyledAddon> | ||
| <FormControl | ||
| onChange={this._handleAddressChange(loc)} | ||
| placeholder={`Add ${loc.type}`} | ||
| type='text' | ||
| value={loc.address} /> | ||
| </InputGroup> | ||
| </FormGroup> | ||
| ))} | ||
|
|
||
| {/* For adding a location. */} | ||
| <FormGroup> | ||
| <InputGroup> | ||
| <NewLocationAddon> | ||
| <FontAwesome name='plus' /> | ||
| </NewLocationAddon> | ||
| <NewLocationFormControl | ||
| onBlur={this._handleAddNewLocation} | ||
| placeholder='Add another place' | ||
| type='text' | ||
| /> | ||
| </InputGroup> | ||
| </FormGroup> | ||
| <FieldArray | ||
| name='savedLocations' | ||
| render={arrayHelpers => ( | ||
| <> | ||
| {savedLocations.map((loc, index) => { | ||
| const isHomeOrWork = loc === homeLocation || loc === workLocation | ||
| return ( | ||
| <FormGroup key={index}> | ||
| <InputGroup> | ||
| <StyledAddon title={loc.type}> | ||
| <FontAwesome name={loc.icon} /> | ||
| </StyledAddon> | ||
| <Field as={FormControl} | ||
| name={`savedLocations.${index}.address`} | ||
| placeholder={isHomeOrWork ? `Add ${loc.type}` : 'Enter a favorite address'} | ||
| // onBlur, onChange, and value are passed automatically. | ||
| /> | ||
| {!isHomeOrWork && ( | ||
| <InputGroup.Button> | ||
| <Button | ||
| aria-label='Delete this location' | ||
| // Do not memoize this call, as this component's index will change. | ||
| onClick={() => arrayHelpers.remove(index)} | ||
| title='Delete this location' | ||
| > | ||
| <FontAwesome name='times' /> | ||
| </Button> | ||
| </InputGroup.Button> | ||
| )} | ||
| </InputGroup> | ||
| </FormGroup> | ||
| ) | ||
| })} | ||
|
|
||
| {/* For adding a new location. */} | ||
| <FormGroup> | ||
| <InputGroup> | ||
| <NewLocationAddon> | ||
| <FontAwesome name='plus' /> | ||
| </NewLocationAddon> | ||
| <NewLocationFormControl | ||
| onBlur={this._handleNewAddressBlur(arrayHelpers)} | ||
| onKeyDown={this._handleNewAddressKeyDown(arrayHelpers)} | ||
| placeholder='Add another place' | ||
| type='text' | ||
| /> | ||
| </InputGroup> | ||
| </FormGroup> | ||
| </> | ||
| )} | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.