Skip to content

Commit

Permalink
New: Allow the <input> element to be controlled (fixes #21) (#23)
Browse files Browse the repository at this point in the history
* New: Allow the <input> element to be controlled (fixes #21)

This commit implements functionality that allows the <input> element
rendered by the MUI <TextField> component to be controlled externally.
This is done by passing the value of the <input> element as the
'inputValue' prop on the <Downshift> component we render.

This commit satisfies the work of: #21
	modified:   README.md
	modified:   demo/Demo.jsx
	copied:     demo/Demo.jsx -> demo/DemoBasic.jsx
	new file:   demo/DemoControlledInput.jsx
	new file:   demo/rootReducer.js
	modified:   package.json
	modified:   src/MUIPlacesAutocomplete.jsx
	modified:   test/test.jsx
	modified:   yarn.lock

* New: Allow the <input> element to be controlled, part deux (fixes #21)

PR #22 sought to introduce new functionality that would allow the
<input> element rendered by the MUI <TextField> component to be
controlled externally. After some initial feedback and talking to Matt
about it some it was obvious that I went about implementing this
functionality incorrectly in commit
e27d465. After re-reading the docs for
Downshift and MUI <TextField> this commit re-implements the
functionality in a more consistent fashion with how Downshift/MUI are
documented/expected to be used.
	modified:   README.md
	modified:   demo/DemoControlledInput.jsx
	modified:   src/MUIPlacesAutocomplete.jsx
	modified:   test/test.jsx
  • Loading branch information
Giners authored Feb 2, 2018
1 parent 27336be commit 6d6727d
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 44 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
[![Travis CI](https://img.shields.io/travis/Giners/mui-places-autocomplete/master.svg)](https://travis-ci.org/Giners/mui-places-autocomplete/builds)

# Features
* Easy to use input for searching for places
* Easy-to-use component for searching for places
* Place suggestions displayed in realtime
* Input state can be controlled externally
* Google Material Design styling provided by next version of Material-UI (v1)
* Safe to render on the server (SSR)
* Integrates with other 3rd party libraries (e.g. [Redux Form](#advancedUsage))
* Thoroughly tested

# Installation
Expand Down Expand Up @@ -71,6 +73,11 @@ class Example extends React.Component {
export default Example
```

<a name="advancedUsage"></a>
### Advanced Usage

* [DemoControlledInput.jsx](https://github.com/Giners/mui-places-autocomplete/blob/master/demo/DemoControlledInput.jsx) - Example that shows how to control the `<input>` element as well as integrate with Redux Form.

<a name="setup"></a>
### Setup
This component relies on some basic setup before usage. It makes use of services provided by Google. To properly make use of the services you will need to do three things:
Expand All @@ -96,6 +103,7 @@ This component also has testing which makes use of the Places library in the Goo
| :--- | :--- | :---: | :--- |
| [`onSuggestionSelected`](#onSuggestionSelected) | Function || Callback that provides the selected suggestion. |
| [`renderTarget`](#renderTarget) | Function || Renders the components/elements that you would like to have the list of suggestions popover. |
| [`textFieldProps`](#textFieldProps) | Object | | Props that will be spread onto a `<TextField>` MUI component that is responsible for rendering the `<input>` element. If you would like to [control the state of the `<input>` element](#textFieldPropsValueProp) externally you must set the `value` key on the object passed to `textFieldProps`. |

<a name="onSuggestionSelected"></a>
#### onSuggestionSelected (required)
Expand All @@ -111,6 +119,25 @@ function onSuggestionSelected(suggestion)

This function is invoked during rendering. It ought to return the components/elements that you want the list of suggestions to render (pop) over.

<a name="textFieldProps"></a>
#### textFieldProps

A MUI [`<TextField>`](https://material-ui-next.com/api/text-field/) component is used to render the `<input>` element. It can be customized to meet your needs by supplying an object to the `textFieldProps` prop. All properties on the object supplied to the `textFieldProps` prop will be spread onto the `<TextField>` component. You can read more about the props that the `<TextField>` component accepts here: [`<TextField>` API documentation](https://material-ui-next.com/api/text-field/)

<a name="textFieldPropsValueProp"></a>
##### `textFieldProps.value` `<input>` control prop

To help meet your needs the state of the `<input>` element can be controlled externally. It is also useful if you would like to integrate `<MUIPlacesAutocomplete>` with other 3rd party libraries such as [Redux Form](https://redux-form.com). To control the state of the `<input>` element you must set the `value` property on the object passed to the `textFieldProps` prop.

```javascript
// 'getState()' can return state from a React component, your app (e.g via Redux), some other source, etc.
const { inputValue } = getState()

<MUIPlacesAutocomplete textFieldProps={{ value: inputValue }} />
```

If you would like to have consistency between the controlled `<input>` elements state as well as any suggestions that are selected then you need to update the controlled state of the `<input>` element when a [suggestion is selected](#onSuggestionSelected). There is an example of how to do this in the [advanced usage section](#advancedUsage).

# Feedback
This was my first open-source project that I undertook while I was teaching myself full-stack development (JS (ES6)/HTML/CSS, Node, Express, NoSQL (DynamoDB), GraphQL, React, Redux, Material-UI, etc.). I'm very interested in taking feedback to either improve my skills (i.e. correct errors :)) or to make this component more useful in general/for your use case. Please feel free to provide feedback by opening an issue or messaging me.

Expand Down
99 changes: 71 additions & 28 deletions demo/Demo.jsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,88 @@
import React from 'react'
import Snackbar from 'material-ui/Snackbar'
import MUIPlacesAutocomplete from './../dist'
// ESLint rule config for demo file
/* eslint import/no-extraneous-dependencies: 0 */
// Our demo files showcase how to integrates with other 3rd party libraries often. The 3rd party
// library dependencies are added to the 'devDependencies' of 'package.json' so disable the
// 'import/no-extraneous-dependencies' rule in this demo file so we don't have to hear about them
// not being in 'dependencies' all the time.
import React, { createElement } from 'react'
import PropTypes from 'prop-types'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { withStyles } from 'material-ui/styles'
import Grid from 'material-ui/Grid'
import { MenuItem } from 'material-ui/Menu'
import Select from 'material-ui/Select'
import Typography from 'material-ui/Typography'

import rootReducer from './rootReducer'
import DemoBasic from './DemoBasic'
import DemoControlledInput from './DemoControlledInput'

// Map of demos that one can select to view
const demos = {
[DemoBasic.name]: { description: DemoBasic.description, component: DemoBasic },
[DemoControlledInput.name]: {
description: DemoControlledInput.description,
component: DemoControlledInput,
},
}

const store = createStore(rootReducer)

const demoStyles = {
container: {
marginTop: 32,
},
}

class Demo extends React.Component {
constructor() {
super()

this.state = { open: false, suggestion: null }

this.onClose = this.onClose.bind(this)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}
this.state = { selectedDemo: DemoBasic }

onClose() {
this.setState({ open: false, suggestion: null })
this.onChange = this.onChange.bind(this)
}

onSuggestionSelected(suggestion) {
// Add your business logic here. In this case we simply set our state to show our <Snackbar>.
this.setState({ open: true, suggestion })
onChange(event) {
this.setState({ selectedDemo: demos[event.target.value].component })
}

render() {
const { open, suggestion } = this.state
const { selectedDemo } = this.state
const { classes: { container } } = this.props

return (
<div>
<MUIPlacesAutocomplete
onSuggestionSelected={this.onSuggestionSelected}
renderTarget={() => (<div />)}
/>
<Snackbar
onRequestClose={this.onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
autoHideDuration={5000}
open={open}
message={suggestion ? (<span>Selected suggestion: {suggestion.description}</span>) : ''}
/>
</div>
<Provider store={store}>
<div>
<Grid container className={container}>
<Grid item xs={3} />
<Grid item xs={6}>
<Typography type="display1" align="center">Select a demo</Typography>
<Select
fullWidth
value={selectedDemo.name}
onChange={this.onChange}
>
{Object.entries(demos).map(kvp =>
<MenuItem key={kvp[0]} value={kvp[0]}>{kvp[1].description}</MenuItem>)}
</Select>
</Grid>
<Grid item xs={3} />
</Grid>
<div className={container}>
{createElement(selectedDemo)}
</div>
</div>
</Provider>
)
}
}

export default Demo
Demo.propTypes = {
classes: PropTypes.shape({
container: PropTypes.string,
}).isRequired,
}

export default withStyles(demoStyles)(Demo)
48 changes: 48 additions & 0 deletions demo/DemoBasic.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'
import Snackbar from 'material-ui/Snackbar'
import MUIPlacesAutocomplete from './../dist'

class DemoBasic extends React.Component {
constructor() {
super()

this.state = { open: false, suggestion: null }

this.onClose = this.onClose.bind(this)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}

onClose() {
this.setState({ open: false })
}

onSuggestionSelected(suggestion) {
// Add your business logic here. In this case we simply set our state to show our <Snackbar>.
this.setState({ open: true, suggestion })
}

render() {
const { open, suggestion } = this.state

return (
<div>
<MUIPlacesAutocomplete
onSuggestionSelected={this.onSuggestionSelected}
renderTarget={() => (<div />)}
/>
<Snackbar
onRequestClose={this.onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
autoHideDuration={5000}
open={open}
message={suggestion ? (<span>Selected suggestion: {suggestion.description}</span>) : ''}
style={{ width: '70vw' }}
/>
</div>
)
}
}

DemoBasic.description = 'Basic usage'

export default DemoBasic
122 changes: 122 additions & 0 deletions demo/DemoControlledInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// ESLint rule config for demo file
/* eslint import/no-extraneous-dependencies: 0 */
// Our demo files showcase how to integrates with other 3rd party libraries often. The 3rd party
// library dependencies are added to the 'devDependencies' of 'package.json' so disable the
// 'import/no-extraneous-dependencies/ rule in this demo file so we don't have to hear about them
// not being in 'dependencies' all the time.
import React from 'react'
import PropTypes from 'prop-types'
import { Field, reduxForm } from 'redux-form'
import Button from 'material-ui/Button'
import Snackbar from 'material-ui/Snackbar'
import MUIPlacesAutocomplete from './../dist'

// Stateless function that we pass to the 'component' prop of the <Field> to render
// <MUIPlacesAutocomplete>. By passing the props that <Field> passes our stateless function as the
// 'textFieldProps' on <MUIPlacesAutocomplete> we are essentially stating that the <Field> would
// like to control the state of the resulting <input> element that gets rendered. This is due to the
// addition of the 'value' property that gets spread onto the object passed to the 'textFieldProps'
// prop.
//
// It is important to define this stateless function outside of the method that renders the actual
// <Field> to avoid causing it to re-render. For more info please refer to the following docs:
// https://redux-form.com/7.2.0/docs/api/field.md/#2-a-stateless-function
const renderMUIPlacesAutocomplete = ({ onSuggestionSelected, ...other }) => (
<MUIPlacesAutocomplete
onSuggestionSelected={onSuggestionSelected}
renderTarget={() => (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: 16,
}}
>
<Button raised color="primary" type="submit">Submit</Button>
</div>
)}
textFieldProps={{ ...other }}
/>
)

renderMUIPlacesAutocomplete.propTypes = {
onSuggestionSelected: PropTypes.func.isRequired,
input: PropTypes.object.isRequired,
}

const DemoControlledInput = (props) => {
const { change, handleSubmit } = props

// Since we are controlling the state of the <input> element via Redux Form we want to ensure that
// the <input> elements state is consistent with any suggestions a user may select. To do so we
// dispatch an action to Redux Form to update the <Field> with a name of 'demoField'. For more
// info see: https://redux-form.com/7.2.1/docs/api/actioncreators.md/
const onSuggestionSelected = (suggestion) => {
change('demoField', suggestion.description)
}

return (
<form onSubmit={handleSubmit}>
<Field
fullWidth
name="demoField"
autoFocus={false}
placeholder="Search for a place"
onSuggestionSelected={onSuggestionSelected}
component={renderMUIPlacesAutocomplete}
/>
</form>
)
}

DemoControlledInput.propTypes = {
// Injected by Redux Form
change: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
}

const ConnectedDemoControlledInput = reduxForm({
form: 'DemoControlledInput',
})(DemoControlledInput)

class DemoControlledInputContainer extends React.Component {
constructor() {
super()

this.state = { open: false, formValues: { } }

this.onClose = this.onClose.bind(this)
this.onSubmit = this.onSubmit.bind(this)
}

onClose() {
this.setState({ open: false })
}

onSubmit(formValues) {
this.setState({ open: true, formValues })
}

render() {
const { open, formValues } = this.state

return (
<div>
<ConnectedDemoControlledInput onSubmit={this.onSubmit} />
<Snackbar
onRequestClose={this.onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
autoHideDuration={5000}
open={open}
message={`You submitted: ${JSON.stringify(formValues, null, 2)}`}
style={{ width: '70vw' }}
/>
</div>
)
}
}

DemoControlledInputContainer.description = '"Controlled" input state via Redux Form'

export default DemoControlledInputContainer
17 changes: 17 additions & 0 deletions demo/rootReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ESLint rule config for demo file
/* eslint import/no-extraneous-dependencies: 0 */
// Our demo files showcase how to integrates with other 3rd party libraries often. The 3rd party
// library dependencies are added to the 'devDependencies' of 'package.json' so disable the
// 'import/no-extraneous-dependencies/ rule in this demo file so we don't have to hear about them
// not being in 'dependencies' all the time.
import { combineReducers } from 'redux'
import { reducer as FormReducer } from 'redux-form'

// If any demos need reducers they ought to add them in here. Having a separate file for our
// reducers seems like a bit much, but at the same time having them in the main <Demo> component
// seems wrong as well...
const rootReducer = combineReducers({
form: FormReducer,
})

export default rootReducer
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
"raf": "^3.4.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-form": "^7.2.1",
"semantic-release": "^8.2.0",
"webpack": "^3.7.1",
"webpack-dev-server": "^2.9.1",
Expand Down
Loading

0 comments on commit 6d6727d

Please sign in to comment.