├── .travis.yml ├── .npmignore ├── src ├── index.js └── GooglePlaceAutocomplete.js ├── .babelrc ├── test ├── MockAutocompleteService.js ├── .setup.js └── GooglePlaceAutocomplete.spec.js ├── .gitignore ├── webpack.config.js ├── package.json ├── README.md └── .eslintrc /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | webpack.* 4 | .babelrc 5 | .eslintrc 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import GooglePlaceAutocomplete from './GooglePlaceAutocomplete'; 2 | 3 | export default GooglePlaceAutocomplete; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "plugins": ["babel-plugin-add-module-exports", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /test/MockAutocompleteService.js: -------------------------------------------------------------------------------- 1 | export default class MockAutoCompleteService { 2 | getPlacePredictions(request, callback) { 3 | callback([]); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/.setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | 3 | var jsdom = require('jsdom').jsdom; 4 | 5 | var exposedProperties = ['window', 'navigator', 'document']; 6 | 7 | global.document = jsdom(''); 8 | global.window = document.defaultView; 9 | Object.keys(document.defaultView).forEach((property) => { 10 | if (typeof global[property] === 'undefined') { 11 | exposedProperties.push(property); 12 | global[property] = document.defaultView[property]; 13 | } 14 | }); 15 | 16 | global.navigator = { 17 | userAgent: 'node.js' 18 | }; 19 | 20 | documentRef = document; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .DS_Store 30 | 31 | lib 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 3 | var path = require('path'); 4 | var env = require('yargs').argv.env; 5 | 6 | var libraryName = 'material-ui-places'; 7 | 8 | var plugins = [], outputFile; 9 | 10 | if (env.mode === 'build') { 11 | plugins.push(new UglifyJsPlugin({ minimize: true })); 12 | outputFile = libraryName + '.min.js'; 13 | } else { 14 | outputFile = libraryName + '.js'; 15 | } 16 | 17 | var config = { 18 | entry: __dirname + '/src/index.js', 19 | output: { 20 | path: __dirname + '/lib', 21 | filename: outputFile, 22 | library: libraryName, 23 | libraryTarget: 'umd', 24 | umdNamedDefine: true 25 | }, 26 | externals: { 27 | 'react': 'react', 28 | 'material-ui': 'material-ui', 29 | 'prop-types': 'prop-types' 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /(\.jsx|\.js)$/, 35 | use: 'babel-loader', 36 | exclude: /(node_modules|bower_components)/ 37 | }, 38 | { 39 | test: /(\.jsx|\.js)$/, 40 | use: 'eslint-loader', 41 | exclude: /node_modules/ 42 | } 43 | ] 44 | }, 45 | resolve: { 46 | modules: [ 47 | path.resolve('./src') 48 | ], 49 | extensions: ['.js'] 50 | }, 51 | plugins: plugins 52 | }; 53 | 54 | module.exports = config; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-places", 3 | "version": "1.1.7", 4 | "description": "Wrapper on top of the material-ui AutoComplete component that use google place api", 5 | "main": "lib/material-ui-places.min.js", 6 | "scripts": { 7 | "build": "webpack --env.mode=build", 8 | "dev": "webpack --progress --colors --watch --mode=dev", 9 | "test": "mocha ./test/.setup.js ./test/**/*.spec.js" 10 | }, 11 | "devDependencies": { 12 | "babel": "6.23.0", 13 | "babel-cli": "^6.16.0", 14 | "babel-core": "6.23.1", 15 | "babel-eslint": "7.1.1", 16 | "babel-loader": "6.3.2", 17 | "babel-plugin-add-module-exports": "0.2.1", 18 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 19 | "babel-preset-es2015": "6.22.0", 20 | "babel-preset-react": "^6.16.0", 21 | "babel-register": "^6.16.3", 22 | "chai": "3.5.0", 23 | "enzyme": "^2.8.2", 24 | "eslint": "3.16.1", 25 | "eslint-loader": "1.6.3", 26 | "eslint-plugin-react": "^6.4.1", 27 | "jsdom": "^9.6.0", 28 | "material-ui": "^0.17.0", 29 | "mocha": "3.2.0", 30 | "prop-types": "^15.5.10", 31 | "react": "^15.5.4", 32 | "react-dom": "^15.5.4", 33 | "react-tap-event-plugin": "^2.0.1", 34 | "react-test-renderer": "^15.5.4", 35 | "sinon": "^1.17.6", 36 | "webpack": "2.2.1", 37 | "yargs": "6.6.0" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/ydeshayes/googlePlaceAutocomplete.git" 42 | }, 43 | "keywords": [ 44 | "google place", 45 | "place", 46 | "location", 47 | "autoComplete", 48 | "material-ui", 49 | "React", 50 | "ReactJS", 51 | "select", 52 | "google api" 53 | ], 54 | "author": "Yann Deshayes", 55 | "bugs": { 56 | "url": "https://github.com/ydeshayes/googlePlaceAutocomplete/issues" 57 | }, 58 | "homepage": "https://github.com/ydeshayes/googlePlaceAutocomplete" 59 | } 60 | -------------------------------------------------------------------------------- /test/GooglePlaceAutocomplete.spec.js: -------------------------------------------------------------------------------- 1 | /* global expect*/ 2 | 3 | import sinon from 'sinon'; 4 | import React from 'react'; 5 | import { expect } from 'chai'; 6 | import { shallow } from 'enzyme'; 7 | 8 | import AutocompleteService from './MockAutocompleteService'; 9 | import GooglePlaceAutocomplete from '../src'; 10 | 11 | describe('', () => { 12 | before(function () { 13 | global.google = { 14 | maps: { 15 | LatLng: function (lat, lng) { 16 | return { 17 | latitude: parseFloat(lat), 18 | longitude: parseFloat(lng), 19 | 20 | lat: function () { return this.latitude; }, 21 | lng: function () { return this.longitude; } 22 | }; 23 | }, 24 | LatLngBounds: function (ne, sw) { 25 | return { 26 | getSouthWest: function () { return sw; }, 27 | getNorthEast: function () { return ne; } 28 | }; 29 | }, 30 | places: { 31 | AutocompleteService 32 | } 33 | } 34 | }; 35 | }); 36 | 37 | it('Render GooglePlaceAutocomplete', () => { 38 | 39 | const onNewRequest = sinon.spy(); 40 | 41 | const onChange = sinon.spy(); 42 | 43 | let wrapper = shallow(); 48 | }); 49 | 50 | it('Accepts country restrictions', () => { 51 | sinon.spy(AutocompleteService.prototype, 'getPlacePredictions'); 52 | const onNewRequest = sinon.spy(); 53 | 54 | const onChange = sinon.spy(); 55 | 56 | let wrapper = shallow(); 63 | 64 | wrapper.setProps({ searchText: 'test' }); 65 | 66 | expect(AutocompleteService.prototype.getPlacePredictions.calledOnce).to.be.true; 67 | expect(AutocompleteService.prototype.getPlacePredictions.args[0][0]).to.have.property('componentRestrictions'); 68 | expect(AutocompleteService.prototype.getPlacePredictions.args[0][0].componentRestrictions).to.have.property('country').that.includes('France'); 69 | 70 | AutocompleteService.prototype.getPlacePredictions.restore(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ydeshayes/googlePlaceAutocomplete.svg?branch=master)](https://travis-ci.org/ydeshayes/googlePlaceAutocomplete) 2 | # material-ui-places component for ReactJS 3 | 4 | Wrapper on top of the material-ui AutoComplete component that use google place api 5 | 6 | ## Installation 7 | 8 | Add this script to your html page: 9 | ```html 10 | 11 | 12 | 13 | ``` 14 | 15 | Material-ui is required: 16 | 17 | ``` 18 | npm install material-ui 19 | ``` 20 | 21 | ``` 22 | npm install material-ui-places 23 | ``` 24 | 25 | ## Features 26 | 27 | * AutoComplete that auto-load google places 28 | 29 | ## Getting started 30 | 31 | 32 | ```jsx 33 | 39 | ``` 40 | ### Props: 41 | 42 | * [Same as AutoComplete material-ui component](http://www.material-ui.com/#/components/auto-complete) 43 | 44 | * onNewRequest: function -> (selectedData, searchedText, selectedDataIndex) 45 | 46 | * onChange: function -> ({target: {value: searchText}}, dataSource, params) 47 | 48 | * location: {lat: latitude, lng: longitude}, default: ```{lat: 0, lng: 0}``` see [LatLng](https://developers.google.com/maps/documentation/javascript/reference?hl=fr#LatLng) 49 | 50 | * radius: number, default: ```0``` 51 | 52 | * bounds: object, ```{sw: southWest, ne: northEast}``` for [LatLngBounds](https://developers.google.com/maps/documentation/javascript/reference?hl=fr#LatLngBounds) or ```{south: south, east: east, north: north, west: west}``` for [LatLngBoundsLiteral](https://developers.google.com/maps/documentation/javascript/reference?hl=fr#LatLngBoundsLiteral) default: ```undefined``` 53 | 54 | * getRef: function -> (ref) 55 | 56 | * types: Array, ``` 57 | The types of predictions to be returned. Four types are supported: 'establishment' for businesses, 'geocode' for addresses, '(regions)' for administrative regions and '(cities)' for localities. If nothing is specified, all types are returned.```, default ```undefined``` 58 | 59 | * restrictions: ```country: Array|String```, ```{ country: [ 'fr', 'gb'] | 'gb' }``` 60 | Restricts predictions to the specified country (ISO 3166-1 Alpha-2 country code, case insensitive). E.g., us, br, au. You can provide a single one, or an array of up to 5 country code strings. See [ComponentRestrictions](https://developers.google.com/maps/documentation/javascript/reference#ComponentRestrictions) 61 | 62 | ## Development 63 | 64 | * `npm run build` - produces production version 65 | * `npm run dev` - produces development version 66 | * `npm test` - run the tests 67 | -------------------------------------------------------------------------------- /src/GooglePlaceAutocomplete.js: -------------------------------------------------------------------------------- 1 | /* global google*/ 2 | 3 | import React from 'react'; 4 | import { AutoComplete } from 'material-ui'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class GooglePlaceAutocomplete extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.autocompleteService = new google.maps.places.AutocompleteService(); 11 | this.state = { 12 | dataSource: [], 13 | data: [] 14 | }; 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (this.props.searchText !== nextProps.searchText) { 19 | this.onUpdateInput(nextProps.searchText, this.state.dataSource); 20 | this.onInputChange(nextProps.searchText); 21 | } 22 | } 23 | 24 | updateDatasource(data) { 25 | if (!data || !data.length) { 26 | return false; 27 | } 28 | 29 | if (this.state.data) { 30 | this.previousData = { ...this.state.data }; 31 | } 32 | this.setState({ 33 | dataSource: data.map(place => place.description), 34 | data 35 | }); 36 | } 37 | 38 | getBounds() { 39 | if (!this.props.bounds || (!this.props.bounds.ne && !this.props.bounds.south)) { 40 | return undefined; 41 | } 42 | 43 | if (this.props.bounds.ne && this.props.bounds.sw) { 44 | return new google.maps.LatLngBounds(this.props.bounds.sw, this.props.bounds.ne); 45 | } 46 | 47 | return { 48 | ...this.props.bounds 49 | }; 50 | } 51 | 52 | onUpdateInput(searchText, dataSource) { 53 | if (!searchText.length || !this.autocompleteService) { 54 | return false; 55 | } 56 | 57 | let request = { 58 | input: searchText, 59 | location: new google.maps.LatLng(this.props.location.lat, this.props.location.lng), 60 | radius: this.props.radius, 61 | types: this.props.types, 62 | bounds: this.getBounds() 63 | }; 64 | 65 | if (this.props.restrictions) { 66 | request.componentRestrictions = { ...this.props.restrictions }; 67 | } 68 | 69 | this.autocompleteService.getPlacePredictions(request, data => this.updateDatasource(data)); 70 | } 71 | 72 | onNewRequest(searchText, index) { 73 | // The index in dataSource of the list item selected, or -1 if enter is pressed in the TextField 74 | if (index === -1) { 75 | return false; 76 | } 77 | const data = this.previousData || this.state.data; 78 | 79 | this.props.onNewRequest(data[index], searchText, index); 80 | } 81 | 82 | onInputChange(searchText, dataSource, params) { 83 | this.props.onChange({target: {value: searchText}}, dataSource, params); 84 | } 85 | 86 | render() { 87 | const { 88 | location, radius, bounds, types, restrictions, ...autoCompleteProps // eslint-disable-line no-unused-vars 89 | } = this.props; 90 | 91 | return ( 92 | 101 | ); 102 | } 103 | } 104 | 105 | GooglePlaceAutocomplete.propTypes = { 106 | location: PropTypes.object, 107 | radius: PropTypes.number, 108 | onNewRequest: PropTypes.func.isRequired, 109 | onChange: PropTypes.func.isRequired, 110 | getRef: PropTypes.func, 111 | types: PropTypes.arrayOf(PropTypes.string), 112 | bounds: PropTypes.object, 113 | restrictions: PropTypes.shape({ 114 | country: PropTypes.oneOfType([ 115 | PropTypes.string, 116 | PropTypes.arrayOf(PropTypes.string) 117 | ]) 118 | }) 119 | }; 120 | 121 | GooglePlaceAutocomplete.defaultProps = { 122 | location: {lat: 0, lng: 0}, 123 | radius: 0, 124 | filter: AutoComplete.noFilter 125 | }; 126 | 127 | export default GooglePlaceAutocomplete; 128 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "globalReturn": true, 4 | "jsx": true, 5 | "modules": true 6 | }, 7 | 8 | "env": { 9 | "browser": true, 10 | "es6": true, 11 | "node": true 12 | }, 13 | 14 | "globals": { 15 | "document": false, 16 | "escape": false, 17 | "navigator": false, 18 | "unescape": false, 19 | "window": false, 20 | "describe": true, 21 | "before": true, 22 | "it": true, 23 | "expect": true, 24 | "sinon": true 25 | }, 26 | 27 | "parser": "babel-eslint", 28 | 29 | "parserOptions": { 30 | "ecmaVersion": 6, 31 | "sourceType": "module", 32 | "ecmaFeatures": { 33 | "modules": true, 34 | "jsx": true 35 | } 36 | }, 37 | 38 | "plugins": [ 39 | "react" 40 | ], 41 | 42 | "rules": { 43 | "block-scoped-var": 2, 44 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 45 | "camelcase": [2, { "properties": "always" }], 46 | "comma-dangle": [2, "never"], 47 | "comma-spacing": [2, { "before": false, "after": true }], 48 | "comma-style": [2, "last"], 49 | "complexity": 0, 50 | "consistent-this": 0, 51 | "curly": [2, "multi-line"], 52 | "default-case": 0, 53 | "dot-location": [2, "property"], 54 | "dot-notation": 0, 55 | "eol-last": 2, 56 | "eqeqeq": [2, "allow-null"], 57 | "func-names": 0, 58 | "func-style": 0, 59 | "generator-star-spacing": [2, "both"], 60 | "guard-for-in": 0, 61 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 62 | "indent": [2, 2, { "SwitchCase": 1 }], 63 | "react/jsx-uses-vars": 1, 64 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 65 | "keyword-spacing": 2, 66 | "linebreak-style": 0, 67 | "max-depth": 0, 68 | "max-len": [2, 120, 4], 69 | "max-nested-callbacks": 0, 70 | "max-params": 0, 71 | "max-statements": 0, 72 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 73 | "newline-after-var": [2, "always"], 74 | "new-parens": 2, 75 | "no-alert": 0, 76 | "no-array-constructor": 2, 77 | "no-bitwise": 0, 78 | "no-caller": 2, 79 | "no-catch-shadow": 0, 80 | "no-cond-assign": 2, 81 | "no-console": 0, 82 | "no-constant-condition": 0, 83 | "no-continue": 0, 84 | "no-control-regex": 2, 85 | "no-debugger": 2, 86 | "no-delete-var": 2, 87 | "no-div-regex": 0, 88 | "no-dupe-args": 2, 89 | "no-dupe-keys": 2, 90 | "no-duplicate-case": 2, 91 | "no-else-return": 2, 92 | "no-empty": 0, 93 | "no-empty-character-class": 2, 94 | "no-eq-null": 0, 95 | "no-eval": 2, 96 | "no-ex-assign": 2, 97 | "no-extend-native": 2, 98 | "no-extra-bind": 2, 99 | "no-extra-boolean-cast": 2, 100 | "no-extra-parens": 0, 101 | "no-extra-semi": 0, 102 | "no-extra-strict": 0, 103 | "no-fallthrough": 2, 104 | "no-floating-decimal": 2, 105 | "no-func-assign": 2, 106 | "no-implied-eval": 2, 107 | "no-inline-comments": 0, 108 | "no-inner-declarations": [2, "functions"], 109 | "no-invalid-regexp": 2, 110 | "no-irregular-whitespace": 2, 111 | "no-iterator": 2, 112 | "no-label-var": 2, 113 | "no-labels": 2, 114 | "no-lone-blocks": 0, 115 | "no-lonely-if": 0, 116 | "no-loop-func": 0, 117 | "no-mixed-requires": 0, 118 | "no-mixed-spaces-and-tabs": [2, false], 119 | "no-multi-spaces": 2, 120 | "no-multi-str": 2, 121 | "no-multiple-empty-lines": [2, { "max": 1 }], 122 | "no-native-reassign": 2, 123 | "no-negated-in-lhs": 2, 124 | "no-nested-ternary": 0, 125 | "no-new": 2, 126 | "no-new-func": 2, 127 | "no-new-object": 2, 128 | "no-new-require": 2, 129 | "no-new-wrappers": 2, 130 | "no-obj-calls": 2, 131 | "no-octal": 2, 132 | "no-octal-escape": 2, 133 | "no-path-concat": 0, 134 | "no-plusplus": 0, 135 | "no-process-env": 0, 136 | "no-process-exit": 0, 137 | "no-proto": 2, 138 | "no-redeclare": 2, 139 | "no-regex-spaces": 2, 140 | "no-reserved-keys": 0, 141 | "no-restricted-modules": 0, 142 | "no-return-assign": 2, 143 | "no-script-url": 0, 144 | "no-self-compare": 2, 145 | "no-sequences": 2, 146 | "no-shadow": 0, 147 | "no-shadow-restricted-names": 2, 148 | "no-spaced-func": 2, 149 | "no-sparse-arrays": 2, 150 | "no-sync": 0, 151 | "no-ternary": 0, 152 | "no-throw-literal": 2, 153 | "no-trailing-spaces": 2, 154 | "no-undef": 2, 155 | "no-undef-init": 2, 156 | "no-undefined": 0, 157 | "no-underscore-dangle": 0, 158 | "no-unneeded-ternary": 2, 159 | "no-unreachable": 2, 160 | "no-unused-expressions": 0, 161 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 162 | "no-use-before-define": 2, 163 | "no-var": 0, 164 | "no-void": 0, 165 | "no-warning-comments": 0, 166 | "no-with": 2, 167 | "one-var": 0, 168 | "operator-assignment": 0, 169 | "operator-linebreak": [2, "before"], 170 | "padded-blocks": 0, 171 | "quote-props": 0, 172 | "quotes": [2, "single", "avoid-escape"], 173 | "semi": [2, "always"], 174 | "semi-spacing": 0, 175 | "sort-vars": 0, 176 | "space-before-blocks": [2, "always"], 177 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 178 | "space-in-brackets": 0, 179 | "space-in-parens": [2, "never"], 180 | "space-infix-ops": 2, 181 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 182 | "spaced-comment": [2, "always"], 183 | "strict": 0, 184 | "use-isnan": 2, 185 | "valid-jsdoc": 0, 186 | "valid-typeof": 2, 187 | "vars-on-top": 2, 188 | "wrap-iife": [2, "any"], 189 | "wrap-regex": 0, 190 | "yoda": [2, "never"] 191 | } 192 | } 193 | --------------------------------------------------------------------------------