Skip to content

Commit

Permalink
Merge pull request #882 from sharetribe/reusable-searchmap-vol2
Browse files Browse the repository at this point in the history
Reusable SearchMap
  • Loading branch information
Gnito authored Aug 3, 2018
2 parents 11f8696 + a130864 commit 0fb56d4
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 99 deletions.
3 changes: 2 additions & 1 deletion .nsprc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"https://nodesecurity.io/advisories/157",
"https://nodesecurity.io/advisories/577",
"https://nodesecurity.io/advisories/654",
"https://nodesecurity.io/advisories/664"
"https://nodesecurity.io/advisories/664",
"https://nodesecurity.io/advisories/678"
]
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ way to update this template, but currently, we follow a pattern:
* Patch (v0.0.**X**): Bug fixes and small changes to components.

---
## Upcoming version 2018-08-XX
* [change] Reusable SearchMap. Fixed the original reverted version. (Includes audit exception 678)
[#882](https://github.com/sharetribe/flex-template-web/pull/882)

## v1.3.1
* [fix] Hotfix: reverting the usage of ReusableMapContainer due to
production build error.
Expand Down
44 changes: 42 additions & 2 deletions src/components/SearchMap/ReusableMapContainer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { node, string } from 'prop-types';
import mapValues from 'lodash/mapValues';
import { IntlProvider } from 'react-intl';
import messages from '../../translations/en.json';
import config from '../../config';

import css from './SearchMap.css';

/**
* ReusableMapContainer makes Google Map usage more effective. This improves:
* - Performance: no need to load dynamic map every time user enters the search page view on SPA.
* - Efficient Google Maps usage: less unnecessary calls to instantiate a dynamic map.
* - Reaction to a bug when removing Google Map instance
* https://issuetracker.google.com/issues/35821412
*/
class ReusableMapContainer extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -41,6 +53,29 @@ class ReusableMapContainer extends React.Component {
}

renderSearchMap() {
// Prepare rendering child (MapWithGoogleMap component) to new location
// We need to add translations (IntlProvider) for map overlay components
//
// NOTICE: Children rendered with ReactDOM.render doesn't have Router access
// You need to provide onClick functions and URLs as props.
const renderChildren = () => {
const isTestEnv = process.env.NODE_ENV === 'test';

// Locale should not affect the tests. We ensure this by providing
// messages with the key as the value of each message.
const testMessages = mapValues(messages, (val, key) => key);
const localeMessages = isTestEnv ? testMessages : messages;

const children = (
<IntlProvider locale={config.locale} messages={localeMessages}>
{this.props.children}
</IntlProvider>
);

// Render children to created element
ReactDOM.render(children, this.el);
};

const targetDomNode = document.getElementById(this.el.id);

// Check if we have already added map somewhere on the DOM
Expand All @@ -52,6 +87,7 @@ class ReusableMapContainer extends React.Component {
// if no mountNode is found, append this outside SPA rendering tree (to document body)
document.body.appendChild(this.el);
}
renderChildren();
} else {
this.el.classList.remove(css.reusableMapHidden);

Expand All @@ -60,10 +96,14 @@ class ReusableMapContainer extends React.Component {
// (but it's not yet moved to correct location of rendering tree).
document.body.removeChild(this.el);
this.mountNode.appendChild(this.el);

// render children and call reattach
renderChildren();
this.props.onReattach();
} else {
renderChildren();
}
}

ReactDOM.render(this.props.children, this.el);
}

render() {
Expand Down
103 changes: 78 additions & 25 deletions src/components/SearchMap/SearchMap.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React, { Component } from 'react';
import { arrayOf, bool, func, number, string, shape } from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withGoogleMap, GoogleMap } from 'react-google-maps';
import classNames from 'classnames';
import groupBy from 'lodash/groupBy';
import isEqual from 'lodash/isEqual';
import reduce from 'lodash/reduce';
import routeConfiguration from '../../routeConfiguration';
import { createResourceLocatorString } from '../../util/routes';
import { createSlug } from '../../util/urlHelpers';
import { types as sdkTypes } from '../../util/sdkLoader';
import { propTypes } from '../../util/types';
import { obfuscatedCoordinates } from '../../util/maps';
import { googleBoundsToSDKBounds } from '../../util/googleMaps';
import { SearchMapInfoCard, SearchMapPriceLabel, SearchMapGroupLabel } from '../../components';
import config from '../../config';

import ReusableMapContainer from './ReusableMapContainer';
import css from './SearchMap.css';

const LABEL_HANDLE = 'SearchMapLabel';
Expand Down Expand Up @@ -97,11 +102,13 @@ const MapWithGoogleMap = withGoogleMap(props => {
infoCardOpen,
isOpenOnModal,
listings,
onCloseAsModal,
onIdle,
onListingClicked,
onListingInfoCardClicked,
createURLToListing,
onMapLoad,
zoom,
mapComponentRefreshToken,
} = props;

const listingArraysInLocations = reducedToArray(groupedByCoordinates(listings));
Expand All @@ -126,6 +133,7 @@ const MapWithGoogleMap = withGoogleMap(props => {
className={LABEL_HANDLE}
listing={listing}
onListingClicked={onListingClicked}
mapComponentRefreshToken={mapComponentRefreshToken}
/>
);
}
Expand All @@ -136,6 +144,7 @@ const MapWithGoogleMap = withGoogleMap(props => {
className={LABEL_HANDLE}
listings={listingArr}
onListingClicked={onListingClicked}
mapComponentRefreshToken={mapComponentRefreshToken}
/>
);
});
Expand All @@ -144,9 +153,11 @@ const MapWithGoogleMap = withGoogleMap(props => {
const openedCard = infoCardOpen ? (
<SearchMapInfoCard
key={listingsArray[0].id.uuid}
mapComponentRefreshToken={mapComponentRefreshToken}
className={INFO_CARD_HANDLE}
listings={listingsArray}
onClickCallback={onCloseAsModal}
onListingInfoCardClicked={onListingInfoCardClicked}
createURLToListing={createURLToListing}
/>
) : null;

Expand Down Expand Up @@ -191,7 +202,11 @@ export class SearchMapComponent extends Component {

this.listings = [];
this.googleMap = null;
this.mapReattachmentCount = 0;
this.state = { infoCardOpen: null };

this.createURLToListing = this.createURLToListing.bind(this);
this.onListingInfoCardClicked = this.onListingInfoCardClicked.bind(this);
this.onListingClicked = this.onListingClicked.bind(this);
this.onMapClicked = this.onMapClicked.bind(this);
this.onMapLoadHandler = this.onMapLoadHandler.bind(this);
Expand All @@ -214,10 +229,30 @@ export class SearchMapComponent extends Component {
this.listings = [];
}

createURLToListing(listing) {
const routes = routeConfiguration();

const id = listing.id.uuid;
const slug = createSlug(listing.attributes.title);
const pathParams = { id, slug };

return createResourceLocatorString('ListingPage', routes, pathParams, {});
}

onListingClicked(listings) {
this.setState({ infoCardOpen: listings });
}

onListingInfoCardClicked(listing) {
if (this.props.onCloseAsModal) {
this.props.onCloseAsModal();
}

// To avoid full page refresh we need to use internal router
const history = this.props.history;
history.push(this.createURLToListing(listing));
}

onMapClicked(e) {
// Close open listing popup / infobox, unless the click is attached to a price label
const labelClicked = hasParentWithClassName(e.nativeEvent.target, LABEL_HANDLE);
Expand All @@ -240,6 +275,7 @@ export class SearchMapComponent extends Component {
const {
className,
rootClassName,
reusableContainerClassName,
center,
isOpenOnModal,
listings: originalListings,
Expand All @@ -257,34 +293,44 @@ export class SearchMapComponent extends Component {
const listings = coordinatesConfig.fuzzy
? withCoordinatesObfuscated(listingsWithLocation)
: listingsWithLocation;
const infoCardOpen = this.state.infoCardOpen;

const isMapsLibLoaded = typeof window !== 'undefined' && window.google && window.google.maps;

const forceUpdateHandler = () => {
this.mapReattachmentCount += 1;
};

// container element listens clicks so that opened SearchMapInfoCard can be closed
/* eslint-disable jsx-a11y/no-static-element-interactions */
return isMapsLibLoaded ? (
<MapWithGoogleMap
containerElement={<div className={classes} onClick={this.onMapClicked} />}
mapElement={<div className={mapClasses} />}
center={center}
isOpenOnModal={isOpenOnModal}
listings={listings}
activeListingId={activeListingId}
infoCardOpen={this.state.infoCardOpen}
onListingClicked={this.onListingClicked}
onMapLoad={this.onMapLoadHandler}
onIdle={() => {
if (this.googleMap) {
onIdle(this.googleMap);
}
}}
onCloseAsModal={() => {
if (onCloseAsModal) {
onCloseAsModal();
}
}}
zoom={zoom}
/>
<ReusableMapContainer className={reusableContainerClassName} onReattach={forceUpdateHandler}>
<MapWithGoogleMap
containerElement={<div className={classes} onClick={this.onMapClicked} />}
mapElement={<div className={mapClasses} />}
center={center}
isOpenOnModal={isOpenOnModal}
listings={listings}
activeListingId={activeListingId}
infoCardOpen={infoCardOpen}
onListingClicked={this.onListingClicked}
onListingInfoCardClicked={this.onListingInfoCardClicked}
createURLToListing={this.createURLToListing}
onMapLoad={this.onMapLoadHandler}
onIdle={() => {
if (this.googleMap) {
onIdle(this.googleMap);
}
}}
onCloseAsModal={() => {
if (onCloseAsModal) {
onCloseAsModal();
}
}}
zoom={zoom}
mapComponentRefreshToken={this.mapReattachmentCount}
/>
</ReusableMapContainer>
) : (
<div className={classes} />
);
Expand All @@ -296,6 +342,7 @@ SearchMapComponent.defaultProps = {
className: null,
rootClassName: null,
mapRootClassName: null,
reusableContainerClassName: null,
bounds: null,
center: new sdkTypes.LatLng(0, 0),
activeListingId: null,
Expand All @@ -311,6 +358,7 @@ SearchMapComponent.propTypes = {
className: string,
rootClassName: string,
mapRootClassName: string,
reusableContainerClassName: string,
bounds: propTypes.latlngBounds,
center: propTypes.latlng,
activeListingId: propTypes.uuid,
Expand All @@ -323,8 +371,13 @@ SearchMapComponent.propTypes = {
coordinatesConfig: shape({
fuzzy: bool.isRequired,
}),

// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
};

const SearchMap = SearchMapComponent;
const SearchMap = withRouter(SearchMapComponent);

export default SearchMap;
Loading

0 comments on commit 0fb56d4

Please sign in to comment.