Skip to content

Commit 57762fa

Browse files
committed
feat(geocoding): use mapzen's autocomplete service
also, cache autocomplete results so queries to Mapzen don’t occur when user hits backspace
1 parent fbc4217 commit 57762fa

8 files changed

Lines changed: 983 additions & 403 deletions

File tree

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
.DS_Store
2+
mock-autocomplete-result.json
3+
yarn.lock

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ Geocoder that uses react-select
77

88
## Usage
99

10-
Check the `propTypes` for the options. For `boundary` see [isomorphic-mapzen-search](https://github.com/conveyal/isomorphic-mapzen-search#searchapikey-text-options) and for `focusPoint` use any [lonlat](https://github.com/conveyal/lonlat) compatible value.
10+
Check the `propTypes` for the options. For `boundary` see [isomorphic-mapzen-search](https://github.com/conveyal/isomorphic-mapzen-search#autocomplete) and for `focusPoint` use any [lonlat](https://github.com/conveyal/lonlat) compatible value.
1111

1212
```js
1313
import Geocoder from 'react-select-geocoder'
14+
1415
render(
1516
<Geocoder
1617
apiKey={process.env.MAPZEN_KEY}

__snapshots__/index.test.js.snap

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
13
exports[`Geocoder should render 1`] = `
24
<Geocoder
35
apiKey="test"
@@ -6,7 +8,8 @@ exports[`Geocoder should render 1`] = `
68
findingLocationText="Locating you..."
79
reverseSearch={[Function]}
810
search={[Function]}
9-
useLocationText="Use Current Location">
11+
useLocationText="Use Current Location"
12+
>
1013
<Async
1114
apiKey="test"
1215
autoBlur={true}
@@ -27,9 +30,10 @@ exports[`Geocoder should render 1`] = `
2730
search={[Function]}
2831
searchPromptText="Type to search"
2932
useLocationText="Use Current Location"
30-
value={null}>
33+
value={null}
34+
>
3135
<Select
32-
addLabelText="Add \"{label}\"?"
36+
addLabelText="Add \\"{label}\\"?"
3337
apiKey="test"
3438
arrowRenderer={[Function]}
3539
autoBlur={true}
@@ -85,21 +89,26 @@ exports[`Geocoder should render 1`] = `
8589
useLocationText="Use Current Location"
8690
value={null}
8791
valueComponent={[Function]}
88-
valueKey="value">
92+
valueKey="value"
93+
>
8994
<div
90-
className="Select Select--single is-searchable">
95+
className="Select Select--single is-searchable"
96+
>
9197
<div
9298
className="Select-control"
9399
onKeyDown={[Function]}
94100
onMouseDown={[Function]}
95101
onTouchEnd={[Function]}
96102
onTouchMove={[Function]}
97-
onTouchStart={[Function]}>
103+
onTouchStart={[Function]}
104+
>
98105
<span
99106
className="Select-multi-value-wrapper"
100-
id="react-select-2--value">
107+
id="react-select-2--value"
108+
>
101109
<div
102-
className="Select-placeholder">
110+
className="Select-placeholder"
111+
>
103112
Select...
104113
</div>
105114
<AutosizeInput
@@ -114,14 +123,16 @@ exports[`Geocoder should render 1`] = `
114123
onFocus={[Function]}
115124
required={false}
116125
role="combobox"
117-
value="">
126+
value=""
127+
>
118128
<div
119129
className="Select-input"
120130
style={
121131
Object {
122132
"display": "inline-block",
123133
}
124-
}>
134+
}
135+
>
125136
<input
126137
aria-activedescendant="react-select-2--value"
127138
aria-expanded="false"
@@ -138,7 +149,8 @@ exports[`Geocoder should render 1`] = `
138149
"width": "5px",
139150
}
140151
}
141-
value="" />
152+
value=""
153+
/>
142154
<div
143155
style={
144156
Object {
@@ -150,16 +162,19 @@ exports[`Geocoder should render 1`] = `
150162
"visibility": "hidden",
151163
"whiteSpace": "pre",
152164
}
153-
} />
165+
}
166+
/>
154167
</div>
155168
</AutosizeInput>
156169
</span>
157170
<span
158171
className="Select-arrow-zone"
159-
onMouseDown={[Function]}>
172+
onMouseDown={[Function]}
173+
>
160174
<span
161175
className="Select-arrow"
162-
onMouseDown={[Function]} />
176+
onMouseDown={[Function]}
177+
/>
163178
</span>
164179
</div>
165180
</div>

index.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {search as mapzenSearch, reverse as reverseSearch} from 'isomorphic-mapzen-search'
1+
import stableStringify from 'json-stable-stringify'
2+
import {autocomplete as mapzenAutocomplete, reverse as reverseSearch} from 'isomorphic-mapzen-search'
23
import throttle from 'lodash.throttle'
34
import React, {Component, PropTypes} from 'react'
45
import {shallowEqual} from 'react-pure-render'
@@ -29,10 +30,12 @@ class Geocoder extends Component {
2930
featureToValue: (feature) => `${feature.properties.label}-${feature.geometry.coordinates.join(',')}`,
3031
findingLocationText: 'Locating you...',
3132
reverseSearch,
32-
search: mapzenSearch,
33+
search: mapzenAutocomplete,
3334
useLocationText: 'Use Current Location'
3435
}
3536

37+
autocompleteCache = {}
38+
3639
options = {}
3740

3841
state = {
@@ -88,16 +91,26 @@ class Geocoder extends Component {
8891
})
8992
}
9093
} else {
91-
search({
94+
const autocompleteQuery = {
9295
apiKey,
9396
boundary,
9497
focusPoint,
9598
text: input
96-
}).then((geojson) => {
99+
}
100+
const autocompleteQueryKey = stableStringify(autocompleteQuery)
101+
102+
// check if autocomplete query has been made before
103+
const cacheValue = this.autocompleteCache[autocompleteQueryKey]
104+
if (cacheValue) {
105+
return callback(null, cacheValue)
106+
}
107+
108+
search(autocompleteQuery).then((geojson) => {
97109
const options = geojson && geojson.features
98110
? geojson.features.map(this.featureToOption)
99111
: []
100112
this.cacheOptions(options)
113+
this.autocompleteCache[autocompleteQueryKey] = {options}
101114
callback(null, {options})
102115
}).catch((error) => {
103116
callback(error)

index.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
import {mount} from 'enzyme'
44
import {mountToJson} from 'enzyme-to-json'
5+
import nock from 'nock'
56
import React from 'react'
67

78
import Geocoder from './index'
9+
const mockAutocompleteResult = require('./mock-autocomplete-result.json')
810

911
// add a div to jsdom for enzyme to mount to
1012
const div = document.createElement('div')
1113
div.id = 'test'
1214
document.body.appendChild(div)
1315

16+
function timeoutPromise (ms) {
17+
return new Promise((resolve, reject) => {
18+
setTimeout(resolve, ms)
19+
})
20+
}
21+
1422
describe('Geocoder', () => {
1523
it('should render', () => {
1624
const tree = mount(
@@ -22,4 +30,30 @@ describe('Geocoder', () => {
2230
})
2331
expect(mountToJson(tree)).toMatchSnapshot()
2432
})
33+
34+
it('should process input change', async () => {
35+
nock('https://search.mapzen.com/')
36+
.get(/v1\/autocomplete/)
37+
.reply(200, mockAutocompleteResult)
38+
39+
const tree = mount(
40+
<Geocoder
41+
apiKey='test'
42+
/>
43+
, {
44+
attachTo: document.getElementById('test')
45+
})
46+
47+
let calculatedOptions
48+
49+
tree.find('Async').props().loadOptions('123 main', (error, result) => {
50+
expect(error).toBeFalsy()
51+
calculatedOptions = result
52+
})
53+
54+
// wait for query to mapzen
55+
await timeoutPromise(1000)
56+
57+
expect(calculatedOptions.options).toHaveLength(10)
58+
})
2559
})

0 commit comments

Comments
 (0)