Skip to content

Commit a129468

Browse files
authored
1486 fix address search in desktop menu (#1497)
* added resetBoundaries and called it from onGeocoderResult to reset Boundaries input during an Address Search * added comments for address search and boundaries event handlers * initial debounce * moved debounce options to settings and added comments * removed console.log * removed unused code * refactored debounce to call wrapper with values defined in settings * initial map debounce * initial resetAddressSearch * renamed updateCouncilsFilter to dispatchUpdateCouncilsFilter in CouncilSelector * initial debouncedHandleDelete * initial toggleBoundaires to close boundaries selector box by dispatching click event to #boundaries * initial refactor to move SelectorBox local state to redux store * restored SelectorBox back to original since it is a common component and should remain reusable. Created a copy of SelectorBox called BoundariesSection that utilizes ui.boundaries.isOpen redux state instead of a local expanded state which is what SelectorBox uses. Using a redux state variable exposes the state of our BoundariesSection component to all other components, which is needed to perform event dispatches to toggle it open and closed from other components accordingly * boundaries section now closes from address search. closing the boundaries section from CouncilSelector when selecting a neighborhood council no longer utilizes a click event dispatch but instead makes use of redux closeBoundaries dispatch to set ui.boundaries.isOpen to false * reset address search input when a previously selected neighborhood council gets unselected from the council selector of the boundaries section * added code to reset address search input when clicking on a neighborhood district on map * added code to clear address search input and collapse boundaries section when zooming out of map. Also collapse boundaries section when clicking into a neightborhood district on the map * renamed dispatchUpdateCouncilsFilter to dispatchUpdateNcId in CouncilSelector to be consistent with other parts of codebase * added comments * added comments * initial fix to sync boundaries and address search * added comments * removed concole.log * added BoundariesSection component * refactor * implemented custom event handling in MapSearch to be able to clear the address search input from any component. this fixed a bug. searching A, selecting boundary B and searching A again now works. * added call to resetAddressSearch from map onClick to reset the address search input during zoom out when clicking outside of a zoomed in nc * refactored early return even though nothing wrong. do not want to have others think it is ok to do this in react class components. removed TODO comment and added optional chaining to addressSearchIsEmpty * added comment * removed commented code * updated comments and used webpack alias @components to refer to BoundariesSection in CouncilSelector/index.js instead of relative path * removed commented code
1 parent 09c8f23 commit a129468

File tree

12 files changed

+481
-71
lines changed

12 files changed

+481
-71
lines changed

client/components/Map/Map.jsx

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ import { withStyles } from '@material-ui/core/styles';
77
import mapboxgl from 'mapbox-gl';
88
import FilterMenu from '@components/main/Desktop/FilterMenu';
99
// import LocationDetail from './LocationDetail';
10-
1110
import { REQUEST_TYPES } from '@components/common/CONSTANTS';
12-
import { getNcByLngLat, setSelectedNcId } from '@reducers/data';
11+
import { getNcByLngLat } from '@reducers/data';
1312
import {
1413
updateNcId,
1514
updateSelectedCouncils,
1615
updateUnselectedCouncils,
1716
} from '@reducers/filters';
18-
17+
import { closeBoundaries } from '@reducers/ui';
1918
import {
2019
INITIAL_BOUNDS,
2120
INITIAL_LOCATION,
@@ -46,6 +45,10 @@ import MapSearch from './controls/MapSearch';
4645

4746
import RequestDetail from './RequestDetail';
4847

48+
import { debounce } from '@utils';
49+
50+
import settings from '@settings'
51+
4952
const styles = theme => ({
5053
root: {
5154
position: 'absolute',
@@ -148,7 +151,7 @@ class Map extends React.Component {
148151
if (this.isSubscribed) {
149152
this.initLayers(true);
150153

151-
map.on('click', this.onClick);
154+
map.on('click', this.debouncedOnClick);
152155

153156
map.once('idle', e => {
154157
this.setState({ mapReady: true });
@@ -235,7 +238,12 @@ class Map extends React.Component {
235238
}
236239

237240

238-
const { dispatchUpdateNcId,dispatchUpdateSelectedCouncils,dispatchUpdateUnselectedCouncils, councils, ncBoundaries } = this.props;
241+
const {
242+
dispatchUpdateNcId,
243+
dispatchUpdateSelectedCouncils,
244+
dispatchUpdateUnselectedCouncils,
245+
councils,
246+
ncBoundaries } = this.props;
239247

240248
if(this.initialState.councilId && councils?.length > 0 && !(this.hasSetInitialNCView) && ncBoundaries){
241249
try{
@@ -323,6 +331,8 @@ class Map extends React.Component {
323331
};
324332

325333
reset = () => {
334+
const { dispatchUpdateNcId } = this.props
335+
326336
this.zoomOut();
327337
this.addressLayer.clearMarker();
328338
this.ncLayer.clearSelectedRegion();
@@ -336,14 +346,46 @@ class Map extends React.Component {
336346
selectedNc: null,
337347
});
338348

349+
// Set councilId in reducers/filters back to null
350+
dispatchUpdateNcId(null)
351+
339352
this.map.once('zoomend', () => {
340353
this.setState({
341354
filterGeo: null,
342355
canReset: true,
343-
});
356+
});
344357
});
345358
};
346359

360+
resetBoundaries = () => {
361+
const {
362+
dispatchUpdateNcId,
363+
dispatchUpdateSelectedCouncils,
364+
dispatchUpdateUnselectedCouncils,
365+
councils } = this.props;
366+
367+
// Reset the selected NcId back to null.
368+
dispatchUpdateNcId(null);
369+
370+
// Reset councilSelector.
371+
dispatchUpdateSelectedCouncils([])
372+
dispatchUpdateUnselectedCouncils(councils)
373+
}
374+
375+
addressSearchIsEmpty = () => {
376+
const addressSearchInput = document.querySelector('#geocoder input')
377+
return !Boolean(addressSearchInput?.value?.trim())
378+
}
379+
380+
resetAddressSearch = () => {
381+
if(!this.addressSearchIsEmpty()){
382+
// Dispatch custom event to MapSearch to trigger geocoder.clear() to clear Address Search input
383+
const geocoderElement = document.getElementById('geocoder')
384+
const resetEvent = new Event(settings.map.eventName.reset)
385+
geocoderElement.dispatchEvent(resetEvent)
386+
}
387+
}
388+
347389
onClick = e => {
348390

349391
const hoverables = [
@@ -362,6 +404,7 @@ class Map extends React.Component {
362404
dispatchUpdateNcId,
363405
dispatchUpdateSelectedCouncils,
364406
dispatchUpdateUnselectedCouncils,
407+
dispatchCloseBoundaries,
365408
councils } = this.props;
366409

367410
for (let i = 0; i < features.length; i++) {
@@ -371,14 +414,16 @@ class Map extends React.Component {
371414
(this.props.selectedNcId !== null)
372415
&& (feature.properties.council_id && this.props.selectedNcId !== feature.properties.council_id)
373416
){
374-
// Since click is for another district, zoom out and reset map.
417+
// Since click is for another district
375418

376-
// Reset the selected NcId back to null.
377-
dispatchUpdateNcId(null);
419+
// Reset boundaries selection
420+
this.resetBoundaries()
378421

379-
// Reset councilSelector.
380-
dispatchUpdateSelectedCouncils([])
381-
dispatchUpdateUnselectedCouncils(councils)
422+
// Collapse boundaries section
423+
dispatchCloseBoundaries()
424+
425+
// Reset Address Search input field
426+
this.resetAddressSearch()
382427

383428
// Reset Map.
384429
this.reset()
@@ -390,6 +435,8 @@ class Map extends React.Component {
390435
switch (feature.layer.id) {
391436
case 'nc-fills':
392437
this.setState({ address: null });
438+
this.resetAddressSearch(); // Clear address search input
439+
dispatchCloseBoundaries(); // Collapse boundaries section
393440
const selectedCouncilId = Number(feature.properties.council_id)
394441
const newSelectedCouncil = councils.find(({ councilId }) => councilId === selectedCouncilId);
395442
const newSelected = [newSelectedCouncil];
@@ -412,27 +459,61 @@ class Map extends React.Component {
412459
}
413460
};
414461

462+
debouncedOnClick = debounce(this.onClick)
463+
415464
onChangeSearchTab = tab => {
416465
this.setState({ geoFilterType: tab });
417466
this.reset();
418467
};
419-
468+
469+
// Address Search event handler
470+
// An Address Search will triger the onGeocoderResult event
420471
onGeocoderResult = ({ result }) => {
421-
const { dispatchGetNcByLngLat, dispatchUpdateNcId } = this.props;
472+
const {
473+
dispatchGetNcByLngLat,
474+
dispatchUpdateNcId,
475+
dispatchCloseBoundaries
476+
} = this.props;
477+
478+
// Reset boundaries input
479+
this.resetBoundaries()
480+
481+
// Collapse boundaries section
482+
dispatchCloseBoundaries()
483+
484+
// Reset map & zoom out
485+
this.reset()
486+
422487
if (result.properties.type === GEO_FILTER_TYPES.nc) {
423488
this.setState({ address: null });
424489
dispatchUpdateNcId(result.id);
425-
} else {
490+
}
491+
else { // When result.properties.type does not equal "District"
426492
const address = result.place_name
427493
.split(',')
428494
.slice(0, -2)
429495
.join(', ');
430496

497+
// what does dispatchGetNcByLngLat() do?
498+
//
499+
// dispatchGetNcByLngLat calls a sagas in redux/sagas/data.js:
500+
// yield takeLatest(types.GET_NC_BY_LNG_LAT, getNcByLngLat);
501+
// which will:
502+
// call(fetchNcByLngLat, action.payload);
503+
// on success: getNcByLngLatSuccess(data) to set value for state.selectedNcId
504+
// on error: getNcByLngLatFailure(e) to set value for state.error object
505+
//
506+
// fetchNcByLngLat above makes an API call to:
507+
// `${BASE_URL}/geojson/geocode?latitude=${latitude}&longitude=${longitude}`
508+
//
509+
// and returns the data
431510
dispatchGetNcByLngLat({ longitude: result.center[0], latitude: result.center[1] });
432511

433512
this.setState({
434513
address: address,
435514
});
515+
516+
// Add that cute House Icon on the map
436517
return this.addressLayer.addMarker([result.center[0], result.center[1]]);
437518
}
438519
};
@@ -607,7 +688,10 @@ class Map extends React.Component {
607688
onReset={this.reset}
608689
canReset={!!filterGeo && canReset}
609690
/>
610-
<FilterMenu resetMap={this.reset} />
691+
<FilterMenu
692+
resetMap={this.reset}
693+
resetAddressSearch={this.resetAddressSearch}
694+
/>
611695
{/* {
612696
(selectedNc || address) && <LocationDetail address={address} nc={selectedNc} />
613697
} */}
@@ -655,6 +739,7 @@ const mapDispatchToProps = dispatch => ({
655739
dispatchUpdateNcId: id => dispatch(updateNcId(id)),
656740
dispatchUpdateSelectedCouncils: councils => dispatch(updateSelectedCouncils(councils)),
657741
dispatchUpdateUnselectedCouncils: councils => dispatch(updateUnselectedCouncils(councils)),
742+
dispatchCloseBoundaries: () => dispatch(closeBoundaries()),
658743
});
659744

660745
// We need to specify forwardRef to allow refs on connected components.

client/components/Map/controls/MapSearch.jsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import React from 'react';
44
import PropTypes from 'prop-types';
5-
import clx from 'classnames';
65
import { withStyles } from '@material-ui/core/styles'
76
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
87
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
9-
import Button from '@material-ui/core/Button';
108
import { GEO_FILTER_TYPES } from '../constants';
9+
import settings from '@settings'
1110

1211
const TABS = Object.values(GEO_FILTER_TYPES);
1312

@@ -59,6 +58,22 @@ const styles = theme => ({
5958
});
6059

6160
class MapSearch extends React.Component {
61+
// Array to keep track of event listeners for memory management
62+
listeners = []
63+
64+
// addListener attaches a new event `listener` to `element` with `eventName` type of event
65+
// and adds it to the listeners array
66+
addListener(element, eventName, listener) {
67+
element.addEventListener(eventName, listener);
68+
this.listeners.push(listener);
69+
}
70+
71+
// removeListeners removes and frees all memory associated with `eventName` type of event
72+
// in the listeners array
73+
removeListeners(element, eventName) {
74+
this.listeners.forEach(listener => element.removeEventListener(eventName, listener));
75+
}
76+
6277
componentDidMount() {
6378
const { map } = this.props;
6479

@@ -93,17 +108,29 @@ class MapSearch extends React.Component {
93108
},
94109
});
95110

111+
// This event fires upon an Address Search submission
96112
this.geocoder.on('result', ({ result }) => {
97113
this.props.onGeocoderResult({ result });
98114

99115
// This clears the address from the Address input field.
100116
// this.geocoder.clear();
101117
});
102118

103-
document.getElementById('geocoder').appendChild(this.geocoder.onAdd(map));
119+
const geocoderElement = document.getElementById('geocoder')
120+
geocoderElement.appendChild(this.geocoder.onAdd(map));
121+
122+
// Add a custom event listener to clear the Address Search Input field
123+
this.addListener(geocoderElement, settings.map.eventName.reset, ()=>this.geocoder.clear() )
124+
104125
// this.setTab(GEO_FILTER_TYPES.address);
105126
}
106127

128+
componentWillUnmount() {
129+
// Free memory and remove all event listeners
130+
const geocoderElement = document.getElementById('geocoder')
131+
removeListeners(geocoderElement, settings.map.eventName.reset)
132+
}
133+
107134
setTab = tab => {
108135
this.props.onChangeTab(tab);
109136
this.geocoder.clear();

0 commit comments

Comments
 (0)