From 1d2c816a11c99ba902f2330c1c5403acc1afb8bc Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 3 Aug 2018 00:25:00 +0300 Subject: [PATCH] ReusableMapContainer: pass-in routing related function through props --- .../SearchMap/ReusableMapContainer.js | 44 +++++++++++++- src/components/SearchMap/SearchMap.js | 57 ++++++++++++++++-- .../SearchMapInfoCard/SearchMapInfoCard.js | 60 ++++++------------- 3 files changed, 111 insertions(+), 50 deletions(-) diff --git a/src/components/SearchMap/ReusableMapContainer.js b/src/components/SearchMap/ReusableMapContainer.js index d9a4872c..b00376e7 100644 --- a/src/components/SearchMap/ReusableMapContainer.js +++ b/src/components/SearchMap/ReusableMapContainer.js @@ -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); @@ -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 = ( + + {this.props.children} + + ); + + // 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 @@ -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); @@ -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() { diff --git a/src/components/SearchMap/SearchMap.js b/src/components/SearchMap/SearchMap.js index 12037822..8f69a164 100644 --- a/src/components/SearchMap/SearchMap.js +++ b/src/components/SearchMap/SearchMap.js @@ -1,10 +1,14 @@ 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'; @@ -98,11 +102,13 @@ const MapWithGoogleMap = withGoogleMap(props => { infoCardOpen, isOpenOnModal, listings, - onCloseAsModal, onIdle, onListingClicked, + onListingInfoCardClicked, + createURLToListing, onMapLoad, zoom, + mapComponentRefreshToken, } = props; const listingArraysInLocations = reducedToArray(groupedByCoordinates(listings)); @@ -127,6 +133,7 @@ const MapWithGoogleMap = withGoogleMap(props => { className={LABEL_HANDLE} listing={listing} onListingClicked={onListingClicked} + mapComponentRefreshToken={mapComponentRefreshToken} /> ); } @@ -137,6 +144,7 @@ const MapWithGoogleMap = withGoogleMap(props => { className={LABEL_HANDLE} listings={listingArr} onListingClicked={onListingClicked} + mapComponentRefreshToken={mapComponentRefreshToken} /> ); }); @@ -145,9 +153,11 @@ const MapWithGoogleMap = withGoogleMap(props => { const openedCard = infoCardOpen ? ( ) : null; @@ -192,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); @@ -215,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); @@ -259,13 +293,18 @@ 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 ? ( - + } mapElement={
} @@ -273,8 +312,10 @@ export class SearchMapComponent extends Component { isOpenOnModal={isOpenOnModal} listings={listings} activeListingId={activeListingId} - infoCardOpen={this.state.infoCardOpen} + infoCardOpen={infoCardOpen} onListingClicked={this.onListingClicked} + onListingInfoCardClicked={this.onListingInfoCardClicked} + createURLToListing={this.createURLToListing} onMapLoad={this.onMapLoadHandler} onIdle={() => { if (this.googleMap) { @@ -287,6 +328,7 @@ export class SearchMapComponent extends Component { } }} zoom={zoom} + mapComponentRefreshToken={this.mapReattachmentCount} /> ) : ( @@ -329,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; diff --git a/src/components/SearchMapInfoCard/SearchMapInfoCard.js b/src/components/SearchMapInfoCard/SearchMapInfoCard.js index ede8af10..1ae2a945 100644 --- a/src/components/SearchMapInfoCard/SearchMapInfoCard.js +++ b/src/components/SearchMapInfoCard/SearchMapInfoCard.js @@ -1,18 +1,14 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import { arrayOf, bool, func, string } from 'prop-types'; import { OverlayView } from 'react-google-maps'; import { OVERLAY_VIEW } from 'react-google-maps/lib/constants'; import { compose } from 'redux'; -import { withRouter } from 'react-router-dom'; import { injectIntl, intlShape } from 'react-intl'; import classNames from 'classnames'; import config from '../../config'; -import routeConfiguration from '../../routeConfiguration'; import { propTypes } from '../../util/types'; import { formatMoney } from '../../util/currency'; import { ensureListing } from '../../util/data'; -import { createResourceLocatorString } from '../../util/routes'; -import { createSlug } from '../../util/urlHelpers'; import { ResponsiveImage } from '../../components'; import css from './SearchMapInfoCard.css'; @@ -37,22 +33,14 @@ const getPixelPositionOffset = (width, height) => { return { x: -1 * (width / 2), y: -1 * (height + 3) }; }; -const createURL = (routes, history, listing) => { - const id = listing.id.uuid; - const slug = createSlug(listing.attributes.title); - const pathParams = { id, slug }; - return createResourceLocatorString('ListingPage', routes, pathParams, {}); -}; - // ListingCard is the listing info without overlayview or carousel controls const ListingCard = props => { - const { className, clickHandler, history, intl, isInCarousel, listing } = props; + const { className, clickHandler, intl, isInCarousel, listing, urlToListing } = props; const { title, price } = listing.attributes; const formattedPrice = price && price.currency === config.currency ? formatMoney(intl, price) : price.currency; const firstImage = listing.images && listing.images.length > 0 ? listing.images[0] : null; - const urlToListing = createURL(routeConfiguration(), history, listing); // listing card anchor needs sometimes inherited border radius. const classes = classNames( @@ -69,8 +57,8 @@ const ListingCard = props => { href={urlToListing} onClick={e => { e.preventDefault(); - // Handle click callbacks and use internal router - clickHandler(urlToListing); + // Use clickHandler from props to call internal router + clickHandler(listing); }} >
@@ -202,22 +181,17 @@ class SearchMapInfoCard extends Component { SearchMapInfoCard.defaultProps = { className: null, rootClassName: null, - onClickCallback: null, }; SearchMapInfoCard.propTypes = { className: string, rootClassName: string, listings: arrayOf(propTypes.listing).isRequired, - onClickCallback: func, - - // from withRouter - history: shape({ - push: func.isRequired, - }).isRequired, + onListingInfoCardClicked: func.isRequired, + createURLToListing: func.isRequired, // from injectIntl intl: intlShape.isRequired, }; -export default compose(withRouter, injectIntl)(SearchMapInfoCard); +export default compose(injectIntl)(SearchMapInfoCard);