Life cycle handling of React based PriceLabel was buggy - revert to plain examples of google overlays

This commit is contained in:
Vesa Luusua 2017-07-25 15:10:24 +03:00
parent 4e291af482
commit ce47dc329e
3 changed files with 198 additions and 38 deletions

View file

@ -1,4 +1,91 @@
@import '../../marketplace.css';
.root {
width: 100%;
height: 100%;
}
.labelRoot {
position: relative;
transform: translate( -50%, -31px);
width: auto;
height: auto;
}
.priceLabel {
/**
* Since caret is absolutely positioned,
* label must have relative to be included to the same rendering layer
*/
position: relative;
/* Font */
@apply --marketplaceH5FontStyles;
font-weight: var(--fontWeightSemiBold);
color: var(--matterColor);
letter-spacing: 0.1px;
background-color: var(--matterColorLight);
/* Borders */
border-style: solid;
border-color: var(--matterColorNegative);
border-width: 1px;
border-radius: 4px;
box-shadow: var(--boxShadowPopupLight);
/* Dimensions */
padding: 6px 10px;
margin-top: 0;
margin-bottom: 0;
transition: var(--transitionStyleButton);
&:hover {
color: var(--marketplaceColor);
cursor: pointer;
box-shadow: var(--boxShadowPopup);
}
/* Overwrite dimensions from font styles */
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.caretShadow {
/* Caret / arrow dimensions and position */
width: 6px;
height: 6px;
position: absolute;
bottom: -3px;
left: 50%;
margin-left: -3px;
transform: rotate(45deg);
/* Caret should have same box-shadow as label */
box-shadow: var(--boxShadowPopupLight);
}
.caret {
/* Caret / arrow dimensions and position */
width: 6px;
height: 6px;
position: absolute;
bottom: -3px;
left: 50%;
margin-left: -3px;
transform: rotate(45deg);
/* Caret should have same bg-color and border as label */
background-color: var(--matterColorLight);
border-right-style: solid;
border-right-color: var(--matterColorNegative);
border-right-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--matterColorNegative);
border-bottom-width: 1px;
}

View file

@ -1,9 +1,9 @@
import React, { Component, PropTypes } from 'react';
import { isEqualWith, sortBy } from 'lodash';
import { unmountComponentAtNode } from 'react-dom';
import { isEqualWith } from 'lodash';
import classNames from 'classnames';
import { types as sdkTypes } from '../../util/sdkLoader';
import * as propTypes from '../../util/propTypes';
import { MapPriceMarker } from '../../components';
import css from './SearchMap.css';
@ -17,9 +17,13 @@ const fitMapToBounds = (map, bounds) => {
}
};
// Compare listing arrays (if listings are the same, there's no need to rerender google maps).
const hasSameListings = (prevListings, nextListings) => {
return isEqualWith(sortBy(prevListings, l => l.id.uuid), sortBy(nextListings, l => l.id.uuid));
const createDiv = (className, text) => {
const d = document.createElement(`div`);
d.className = className;
if (text) {
d.appendChild(document.createTextNode(text));
}
return d;
};
class SearchMap extends Component {
@ -27,8 +31,7 @@ class SearchMap extends Component {
super(props);
this.map = null;
this.state = { overlays: [] };
this.addOverlay = this.addOverlay.bind(this);
this.priceLabelOverlays = [];
}
componentDidMount() {
@ -49,10 +52,92 @@ class SearchMap extends Component {
// Disable zooming by scrolling
scrollwheel: false,
};
// Create map
this.map = new window.google.maps.Map(this.el, mapOptions);
// If bounds are given, use it (defaults to center & zoom).
fitMapToBounds(this.map, bounds);
// This is now done according to Google Maps examples
// https://developers.google.com/maps/documentation/javascript/examples/overlay-simple
// TODO It should be possible to do this more React way, but that enhancement must be done later
this.MapPriceOverlayMarker = class extends window.google.maps.OverlayView {
constructor(id, origin, map) {
super();
// Initialize all properties.
this.id = id;
this.origin = origin;
this.map = map;
// Define a property to hold the price label.
// This happens in onAdd method
this.overlayContainer = null;
// Explicitly call setMap on this overlay.
// This will trigger calls to onAdd and draw methods
this.setMap(map);
}
/**
* onAdd is called when the map's panes are ready and the overlay has been
* added to the map.
*/
onAdd() {
const overlayDiv = document.createElement(`div`);
overlayDiv.style.position = `absolute`;
overlayDiv.dataset.overlayId = this.id;
this.overlayContainer = overlayDiv;
// Create price label
// TODO this should be possible with React too,
// but life-cycle management needs some some extra research
const root = createDiv(css.labelRoot);
root.appendChild(createDiv(css.caretShadow));
root.appendChild(createDiv(css.priceLabel, 'asdf'));
root.appendChild(createDiv(css.caret));
this.overlayContainer.appendChild(root);
// Add the element to the "overlayMouseTarget" pane.
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#MapPanes
const panes = this.getPanes();
panes.overlayMouseTarget.appendChild(overlayDiv);
}
draw() {
// We need to retrieve the projection from the overlay, to map pixels to coordinates
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#MapCanvasProjection
const overlayProjection = this.getProjection();
const point = overlayProjection.fromLatLngToDivPixel(this.origin);
if (point) {
this.overlayContainer.style.left = `${point.x}px`;
this.overlayContainer.style.top = `${point.y}px`;
this.overlayContainer.style.height = `0`;
}
window.google.maps.event.addDomListener(this.overlayContainer, 'click', event => {
// TODO: Click handler needs to open info window at some point
// eslint-disable-next-line no-console
console.log(
`OverlayLayer (${event.target.closest('[data-overlay-id]').dataset.overlayId}) clicked.`
);
window.google.maps.event.trigger(self, 'click');
});
}
/**
* The onRemove() method will be called automatically from the API if
* we ever set the overlay's map property to 'null'.
*/
onRemove() {
// Remove container from parent node and tell React about it too
this.overlayContainer.parentNode.removeChild(this.overlayContainer);
unmountComponentAtNode(this.overlayContainer);
this.overlayContainer = null;
}
};
}
componentWillReceiveProps(nextProps) {
@ -60,48 +145,36 @@ class SearchMap extends Component {
fitMapToBounds(this.map, bounds);
if (!hasSameListings(listings, nextProps.listings)) {
const hasSameListings = isEqualWith(listings, this.props.listings);
const hasOverlaysOnMap = this.map && this.priceLabelOverlays.length > 0;
if (!(hasSameListings && hasOverlaysOnMap)) {
// Clear markers from map
this.state.overlays.forEach(o => o.setMap(null));
this.priceLabelOverlays.forEach(o => o.setMap(null));
this.priceLabelOverlays = [];
this.priceLabelOverlays = listings.reverse().map(l => {
const geolocation = l.attributes.geolocation;
const googleLatLng = new window.google.maps.LatLng({
lat: geolocation.lat,
lng: geolocation.lng,
});
return new this.MapPriceOverlayMarker(l.id.uuid, googleLatLng, this.map);
});
}
}
addOverlay(overlay) {
this.setState(prevState => {
// Track map overlays, in case we need to clear them from map
const overlays = prevState.overlays.concat([overlay]);
return { overlays };
});
}
render() {
const { className, rootClassName, listings } = this.props;
const { className, rootClassName } = this.props;
const classes = classNames(rootClassName || css.root, className);
// Add listing markers to map if listings prop is passed
// By reversing the order, we can show the nearest price labels on top of more distant ones.
const currentMarkers = listings
.map(l => {
return (
<MapPriceMarker
key={l.id.uuid}
map={this.map}
listing={l}
onAddOverlay={this.addOverlay}
/>
);
})
.reverse();
return (
<div
className={classes}
ref={el => {
this.el = el;
}}
>
{currentMarkers}
</div>
/>
);
}
}

View file

@ -26,7 +26,7 @@ input,
textarea,
select,
li {
@apply --marketplaceDefaultFontStyles
@apply --marketplaceDefaultFontStyles;
}
p,
@ -38,7 +38,7 @@ pre {
/* TODO */
html {
@apply --marketplaceDefaultFontStyles
@apply --marketplaceDefaultFontStyles;
color: var(--matterColor);
padding: 0;
margin: 0;