I have two components that are both children of the same parent component that both render a list of places - a map that loads the places as markers on the map and then a grid of place listings with a filter menu. What I want to do is communicate the filter click from the place listing component to the map component to filter the markers. To accomplish this I have a function in the parent component called handlePlaceFilter() that I am passing into the place listing child component as a prop.
I am able to trigger a console log statement from the parent component after a filter click on the child and can pass up to the parent a filtered list of places - but I can't get it to re-render either component with the filtered list of places.
Here's the parent component containing both children and the handlePlaceFilter() function:
import React from 'react';
import Header from './Header';
import MapContainer from './MapContainer';
import _ from 'lodash';
import Places from './Places';
const Cosmic = require('cosmicjs')();
export default class PlaceIndex extends React.Component {
constructor (props) {
super(props);
this.handlePlaceFilter = this.handlePlaceFilter.bind(this);
this.state = {
destination: '',
destinations: '',
places: '',
globals: '',
}
}
async componentDidMount() {
const bucket = Cosmic.bucket({
slug: 'where-she-goes',
read_key: '',
write_key: ''
});
try {
let result = await bucket.getBucket()
this.setState (() => {
return {
destination: _.find(result.bucket.objects, { slug: this.props.match.params.slug }),
destinations: _.filter(result.bucket.objects, {type_slug: 'destinations'}),
places: _.filter(result.bucket.objects, {type_slug: 'places'}),
globals: _.filter(result.bucket.objects, {type_slug: 'globals'})
}
});
} catch (err) {
console.log(err)
}
}
handlePlaceFilter (places) {
console.log("Place filter clicked!")
console.log(places)
this.setState (() => {
return {
places: places
}
});
}
render() {
if (!this.state.places || !this.state.destination)
return <p>Loading...</p>
// compile list of destination plus children
let placeDestinations = new Array();
placeDestinations.push(this.state.destination.slug);
this.state.destination.metadata.child_destinations &&
this.state.destination.metadata.child_destinations.map(destination => {
placeDestinations.push(destination.slug)
destination.metadata.child_destinations &&
destination.metadata.child_destinations.map(child_destination => {
placeDestinations.push(child_destination.slug)
})
})
console.log("Destination List")
console.log(placeDestinations)
// filter places by destination list
let places = this.state.places.filter(function(place) {
return placeDestinations.includes(place.metadata.destination.slug);
})
console.log("Places")
console.log(places)
let destinationCenter = {
latitude: this.state.destination.metadata.latitude,
longitude: this.state.destination.metadata.longitude
}
return (
<div>
<Header
destinations={this.state.destinations}
globals={this.state.globals}
/>
<div className="places-title text-center">
<h2>All Places in {this.state.destination.title}</h2>
</div>
<MapContainer
places={places}
center={destinationCenter}
/>
<Places
places={places}
handlePlaceFilter={this.handlePlaceFilter}
/>
</div>
);
}
}
Here's the child component for the Place Listings:
import React from 'react'
import _ from 'lodash'
export default class Places extends React.Component {
constructor (props) {
super(props);
this.showHotels = this.showHotels.bind(this);
this.showAll = this.showAll.bind(this);
this.showRestaurants = this.showRestaurants.bind(this);
let places = _.flatMap(this.props.places, this.props.places.metadata);
var allplaces = new Array();
var hotels = new Array();
var restaurants = new Array();
var sights = new Array();
places &&
places.map(place => {
allplaces.push(place)
if (place.metadata.place_type == 'Hotel') {
hotels.push(place)
}
if (place.metadata.place_type == 'Restaurant') {
restaurants.push(place)
}
if (place.metadata.place_type == 'Sight') {
sights.push(place)
}
})
// Limit # of places in each array to customize for page contect
if (this.props.limit) {
(allplaces.length > 0) ? (allplaces.length = this.props.limit) : allplaces;
(hotels.length > 0) ? (hotels.length = this.props.limit) : hotels;
(restaurants.length > 0) ? (restaurants.length = this.props.limit) : restaurants;
(sights.length > 0) ? (sights.length = this.props.limit) : sights;
}
this.state = {
current: "All",
places: allplaces,
hotels: hotels,
restaurants: restaurants,
sights: sights,
allplaces: allplaces
}
}
showAll (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.allplaces);
this.setState (() => {
return {
current: "All",
places: this.state.allplaces
}
});
}
showHotels (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.hotels);
this.setState (() => {
return {
current: "Hotels",
places: this.state.hotels
}
});
}
showRestaurants (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.restaurants);
this.setState (() => {
return {
current: "Restaurants",
places: this.state.restaurants
}
});
}
showSights (e) {
e.preventDefault();
this.props.handlePlaceFilter(this.state.sights);
this.setState (() => {
return {
current: "Sights",
places: this.state.sights
}
});
}
render () {
if (this.state.allplaces.length > 0) {
return (
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="blogFilter text-center text-uppercase">
<ul className="list-inline">
<li>{(this.state.current == "All") ? All : <a href="#" onClick={this.showAll}>All</a>}</li>
<li>{(this.state.hotels.length > 0) ? ((this.state.current == "Hotels") ? <a href="#" className="current" onClick={this.showHotels}>Hotels</a> : <a href="#" onClick={this.showHotels}>Hotels</a>) : <span></span>}</li>
<li>{(this.state.restaurants.length > 0) ? ((this.state.current == "Restaurants") ? <a href="#" className="current" onClick={this.showRestaurants}>Restaurants</a> : <a href="#" onClick={this.showRestaurants}>Restaurants</a>) : <span></span>}</li>
<li>{(this.state.sights.length > 0) ? ((this.state.current == "Sights") ? <a href="#" className="current" onClick={this.showSights}>Sights</a> : <a href="#" onClick={this.showSights}>Sights</a>) : <span></span>}</li>
</ul>
</div>
<div className="row">
<div className="blogContainer">
{
this.state.places &&
this.state.places.map(place => {
console.log("Places")
console.log(place)
return (
<div className="col-sm-3 design">
<article className="portfolio portfolio-2 post-grid">
<div className="post-thumb">
<a href={`/place/${place.slug}`}><img src={place.metadata.hero.imgix_url} alt="" /></a>
<div className="post-thumb-overlay text-center">
<div className="text-uppercase text-center">
<i className="fa fa-link"></i>
</i>
</div>
</div>
</div>
<div className="post-content">
<header className="entry-header text-center text-uppercase">
<h6><a href={`/place/${place.slug}`}>{place.metadata.place_type}</a></h6>
<h2 className="entry-title">{place.title}</h2>
</header>
</div>
</article>
</div>
)
})
}
</div>
</div>
</div>
</div>
</div>
)
} else {
return (
<div></div>
)
}
}
}
And here's the child component for the Map:
import React, { Component } from 'react';
import {Map, InfoWindow, Marker, GoogleApiWrapper} from 'google-maps-react';
const mapStyles = {
width: '100%',
height: '300px'
};
let geocoder;
export class MapContainer extends Component {
constructor (props) {
super(props);
this.onMarkerClick = this.onMarkerClick.bind(this);
this.displayMarkers = this.displayMarkers.bind(this);
let addresses = new Array();
this.props.places &&
this.props.places.map(place => {
addresses.push(place.metadata.address)
})
this.state = {
lat: this.props.center.latitude,
lng: this.props.center.longitude,
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
places: [],
addresses: addresses
}
}
componentDidMount () {
this.plotPoints()
}
plotPoints () {
let locations = this.getPoints(geocoder)
let places = new Array()
Promise.all(locations)
.then((returnVals) => {
returnVals.forEach((latLng) => {
let place = {
latitude: latLng[0],
longitude: latLng[1]
}
places.push(place)
})
console.log("Places to Plot:")
console.log(places[0].latitude)
// places now populated
this.setState(() => {
return {
lat: places[0].latitude,
lng: places[0].longitude,
places: places
}
});
console.log("Center Lat")
console.log(this.state.lat)
console.log(this.state.lng)
});
}
getPoints(geocoder) {
let locationData = [];
for (let i = 0; i < this.state.addresses.length; i++) {
locationData.push(this.findLatLang(this.state.addresses[i], geocoder))
}
return locationData // array of promises
}
findLatLang(address, geocoder) {
return new Promise(function(resolve, reject) {
geocoder.geocode({
'address': address
}, function(results, status) {
if (status === 'OK') {
console.log(results);
resolve([results[0].geometry.location.lat(), results[0].geometry.location.lng()]);
} else {
reject(new Error('Couldnt\'t find the location ' + address));
}
})
})
}
displayMarkers (stores) {
return stores.map((place, index) => {
return <Marker key={index} id={index} position={{
lat: place.latitude,
lng: place.longitude
}}
onClick={() => console.log("You clicked me!")} />
})
}
onMarkerClick (props, marker, e) {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
});
};
render() {
geocoder = new this.props.google.maps.Geocoder();
console.log("Place Array")
console.log(this.state.places)
return (
<div className="container place-map">
<div className="row">
<div className="col-md-12">
<Map
google={this.props.google}
zoom={8}
style={mapStyles}
initialCenter={{
lat: this.state.lat,
lng: this.state.lng
}}
>
{this.displayMarkers(this.state.places)}
<InfoWindow
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
>
<div>Your Location Here!</div>
</InfoWindow>
</Map>
</div>
</div>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: 'AIzaSyCOJDrZ_DXmHzbzSXv74mULU3aMu3rNrQc'
})(MapContainer);
In your Child component, you are retrieving/setting places value in constructor. After which any change in props of Parent component wont notify to Child component that state value is updated, unless you add getDerivedStateFromProps(props, state).
In this method, you will receive new props and update state from newly received props.
After updating state here (with setState, your child component's render method will execute)
For component to rerender and display the changes the state needs to update. Right now youbare update the state with initial props. When the props changes the state of your child component won't change as you are using only initial props. So what you can do is use a lifecycle hook componentWillReceiveProps and inside that update your state with new props.
Code can be something like this:
componentWillReceiveProps(nextProps){
if(this.state.lat !== nextProps.center.latitude){
this.setState({ lat: nexrProps.center.latitude});
}
}
You can do likewise for rest of the variables too.
This way whenever your props changes your state will also change thus forcing your component to rerender and reflect the changes.
Hope it helps!!
Related
I'm trying to get some map over some data and render it as a card style component.
The data structure is as follows within my user array (all fake data)
and this is the code...
import React from "react";
import OverlayContent from "./OverlayContent";
import { onCountryClick } from "../Scene3D/AppSignals";
import Portal from "./Portal";
import "./style.scss";
class Overlay extends React.Component {
constructor(props) {
super(props);
this.state = { overlay: false, users: [], country: [] };
this.openOverlay = this.openOverlay.bind(this);
this.closeOverlay = this.closeOverlay.bind(this);
}
openOverlay() {
this.setState({ overlay: true });
}
closeOverlay() {
this.setState({ overlay: false });
}
onCountryClick = (country, users) => {
this.openOverlay();
this.setState({ users: [users], country });
};
componentDidMount() {
this.onCountrySignal = onCountryClick.add(this.onCountryClick);
}
componentWillUnmount() {
this.onCountrySignal.detach();
}
render() {
const country = this.state.country;
const users = this.state.users;
console.log(users);
return (
<div className="btn-container">
{this.state.overlay && (
<Portal>
<div>
<h1>{country}</h1>
{users &&
users.map(user => (
<div className="user_container">
<h1 key={user.id} className="user_name">
{user.favouriteQuote}
</h1>
</div>
))}
<button className="btn" onClick={this.closeOverlay}>
Close
</button>
</div>
</Portal>
)}
</div>
);
}
}
export default Overlay;
When mapping on this user array it doesn't appear to give me access to the actual user data I need. Is it because I have an array within an array?
onCountryClick = (country, users) => {
this.openOverlay();
this.setState({ users: [users], country });
};
Is users an array? If so remove the brackets here or map through the first element. users[0].map()
I am learning react, and is trying to plot a map with mapbox. I am stuck with how to render elements seperatly.
In the render part, there is a <div/> element and a component <CityWeather/> wrapped. div is a reference to map. The <Cityweather /> is an info box which is supposed to display weather information based on lat and long. The app.js file render method is
render(){
console.log(this.state);
return(
<section>
<div className="map-container" ref={x => { this.mapContainer = x;}}/>
<CityWeather lat={this.state.lat} lng={this.state.lng} />
</section>
);
The componentDidMount() is
componentDidMount() {
this.getLocation();
mapboxgl.accessToken = "";
const { lng, lat, zoom } = this.state;
const map = new mapboxgl.Map({
container: this.mapContainer,
style: "mapbox://styles/mapbox/streets-v11",
center: [lng, lat],
zoom: zoom
});
map.on("moveend", () => {
const { lng, lat } = map.getCenter();
this.setState({
lng: lng.toFixed(4),
lat: lat.toFixed(4),
zoom: map.getZoom().toFixed(2)
});
});
}
The <CityWeather /> component
class CityWeather extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
}
componentDidMount() {
console.log(this.props); // this logs only 1 time when the page loads
fetch("api?lat=" + this.props.lat +"&lon=" +this.props.lng + "&appid=")
.then(res => res.json())
.then(data => {this.setState({ name: data.name });
}); // get name from lat and long and store it in state
}
render() {
return (
<div className="city-weather">
<p>
City | <span className="city">
{this.state.name}</span>
</p>
</div>
);
}
}
On each event the console is logging the updated latitude, longitude and zoom. Also the <CityWeather/> is also rendered for Ist time. After that the component is not rendered on state change.
You have loaded weather data in componentDidMount. It will only run the first time that the component is rendered, Not on every state change.
This is the correct code:
class CityWeather extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
}
componentDidMount() {
this.load();
}
load(){
console.log(this.props); // this logs only 1 time when the page loads
fetch("api?lat=" + this.props.lat + "&lon=" + this.props.lng + "&appid=")
.then(res => res.json())
.then(data => {
this.setState({ name: data.name });
}); // get name from lat and long and store it in state
}
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.lat !== prevProps.lat || this.props.lng !== prevProps.lng) {
this.load();
}
}
render() {
return (
<div className="city-weather">
<p>
City | <span className="city">
{this.state.name}</span>
</p>
</div>
);
}
}
Did you add a log in the listener? I think the method was not called because you did not move. you can use timer and some mock data to test the render.
I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.
Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.
App.js
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';
class App extends Component {
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false
}
getCities = (e) => {
e.preventDefault();
const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);
const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');
if (allowedCountries.test(countryName)) {
axios
.get(countryUrl)
.then( response => {
const country = response.data.results.find(el => el.name === countryName);
return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
})
.then( response => {
const cities = response.data.results.map(record => {
return { name: record.city };
});
cities.forEach(city => {
axios
.get(wikiUrl + city.name)
.then( response => {
let id;
for (let key in response.data.query.pages) {
id = key;
}
const description = response.data.query.pages[id].extract;
this.setState(prevState => ({
cities: [...prevState.cities, {city: `${city.name}`, description}],
infoMessage: prevState.infoMessage = ''
}))
})
})
})
.catch(error => {
console.log('oopsie, something went wrong', error)
})
} else {
this.setState(prevState => ({
infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
cities: [...prevState.cities = []]
}))
}
}
descriptionTogglerHandler = () => {
this.setState((prevState) => {
return { visible: !prevState.visible};
});
};
render () {
return (
<div className="App">
<ErrorMessage error={this.state.infoMessage}/>
<div className="form-wrapper">
<CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
</div>
{this.state.cities.map(({ city, description }) => (
<CityOutput
key={city}
city={city}
description={description}
show={this.state.visible}
descriptionToggler={this.descriptionTogglerHandler} />
))}
</div>
);
}
}
export default App;
CityOutput.js
import React, { Component } from 'react';
import './CityOutput.css';
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show } = this.props;
let descriptionClasses = 'output-record description'
if (show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
export default CityOutput;
Put the visible key and the toggle function in the CityOutput instead of having it in the parent
import React, { Component } from "react";
import "./CityOutput.css";
class CityOutput extends Component {
state = {
visible: true
};
descriptionTogglerHandler = () => {
this.setState({ visible: !this.state.visible });
};
render() {
const { city, description } = this.props;
let descriptionClasses = "output-record description";
if (this.state.visible) {
descriptionClasses = "output-record description open";
}
return (
<div className="output">
<div className="output-record">
<b>City:</b> {city}
</div>
<button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
);
}
}
export default CityOutput;
There are two ways of how I would approach this,
The first one is setting in your state a key property and check and compare that key with the child keys like:
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false.
currKey: 0
}
descriptionTogglerHandler = (key) => {
this.setState((prevState) => {
return { currKey: key, visible: !prevState.visible};
});
};
// then in your child component
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
let descriptionClasses = 'output-record description'
if (show && elKey === currKey) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={() => descriptionToggler(elKey)}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
The other way is to set an isolated state for every child component
class CityOutput extends Component {
constructor(props) {
this.state = {
show: false
}
}
function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
render() {
const { city, descriptionToggler, description } = this.props;
let descriptionClasses = 'output-record description'
if (this.state.show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
I hope this helps ;)
I am trying to update my parent compenent's state according to child's component input value and render components again.
I have App, Map and ListPlaces components.
Map component shows the map and markers and takes markers from App component as a prop.
ListPlaces component has own state(searchQuery) and a method(updateQuery). Basically it uses regexp and filter the results according to the input and show filtered values in the list element. It's fine, however, I need much more than this. What I want to do here? According to input value changes, I want to update my markers (state in App component) and re-render my map component again to show only related markers on the map. Idea is basic, but I couldn't implement it.
When I try to change my markers which is actually array, it would be 0. I will share my all code but I need to say that I think the problem probably is about onChange method in the input element (ListPlaces)
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
places: [],
markers: [],
markerID: -1,
newmarkers: []
};
this.changeMarkers = this.changeMarkers.bind(this);
}
componentDidMount() {
fetch(
"api_url"
)
.then(response => response.json())
.then(data => {
this.setState({
places: data.response.venues,
markers: data.response.venues
});
})
.catch(error => {
console.log("Someting went wrong ", error);
});
}
openInfo = (e, id) => {
this.setState({
markerID: id
});
};
closeInfo = () => {
this.setState({
markerID: -1
});
};
changeMarkers = newValue => {
const newmarkers = this.state.places.filter(
place => place.name === newValue
);
this.setState({
markers: newmarkers
});
};
toggleListPlaces = () => {
const nav = document.getElementById("nav-toggle");
const body = document.getElementsByTagName("body")[0];
nav.classList.toggle("active");
if (body.classList.contains("show-nav")) {
body.classList.remove("show-nav");
} else {
// If sidebar is hidden:
body.classList.add("show-nav");
}
};
render() {
return (
<div className="App">
<Map
role="application"
places={this.state.places}
markers={this.state.markers}
openInfoHandler={this.openInfo}
closeInfoHandler={this.closeInfo}
markerID={this.state.markerID}
googleMapURL="map_url"
loadingElement={<div style={{ height: "100%" }} />}
containerElement={<div style={{ height: "100%" }} />}
mapElement={<div style={{ height: "100%" }} />}
/>
<ListPlaces
toggleListHandler={this.toggleListPlaces}
locations={this.state.places}
openInfoHandler={this.openInfo}
changeMarkersHandler={this.changeMarkers}
/>
</div>
);
}
}
export default App;
ListPlaces
class ListPlaces extends Component {
state = {
searchQuery: ""
};
updateQuery = query => {
this.setState({ searchQuery: query});
};
render() {
const { toggleListHandler, locations, openInfoHandler, changeMarkersHandler} = this.props;
let showLocations;
if (this.state.searchQuery) {
const match = new RegExp(escapeRegExp(this.state.searchQuery), "i");
showLocations = locations.filter(location =>match.test(location.name));
} else {
showLocations = locations;
}
return (
<div>
<aside>
<h2>Restaurants</h2>
<nav>
<div className="search-area">
<input
className="search-input"
type="text"
placeholder="Search Restaurant"
value={this.state.searchQuery}
onChange={e => {this.updateQuery(e.target.value); changeMarkersHandler(e.target.value)}}
/>
</div>
<ul>
{showLocations.map(location => {
return (
<li
key={location.id}
onClick={e =>
openInfoHandler(e, location.id)
}
>
{location.name}
</li>
);
})}
</ul>
</nav>
<p>Information provided by Foursquare</p>
</aside>
<a
onClick={toggleListHandler}
id="nav-toggle"
className="position"
>
<span />
</a>
</div>
);
}
}
export default ListPlaces;
Map
const Map = withScriptjs(withGoogleMap((props) =>
<GoogleMap
defaultZoom={14}
defaultCenter={{ lat: 48.2854790, lng: -143.1407394 }}
>
{props.markers.map(restaurant => {
let marker = (
<Marker
id={restaurant.id}
key={restaurant.id}
name={restaurant.name}
position={{lat: restaurant.location.lat, lng: restaurant.location.lng}}
address={restaurant.location.address}
defaultAnimation={window.google.maps.Animation.DROP} // Should be 1 or 2 according to Stackoverflow
onClick={e => {props.openInfoHandler(e, restaurant.id)}}
animation = {props.markerID === restaurant.id && window.google.maps.Animation.BOUNCE}
>
{props.markerID === restaurant.id && (
<InfoWindow onCloseClick={e => {props.closeInfoHandler()}}>
<div className="info-window">
<p className="restaurant-name">{restaurant.name}</p>
<p className="restaurant-address">{restaurant.location.address}</p>
</div>
</InfoWindow>
)}
</Marker>
);
return marker;
})}
</GoogleMap>
))
export default Map;
you're on the right track - you need to lift up the filter value/filter operation into the common parent
App.js
class App extends React.Component {
onChangeFilter = (newFilter) => {
this.setState({ filter: newFilter })
}
render () {
const { filter, places } = this.state
const mPlaces = places.filter(/* filter places here */)
return (
<div className="App">
<ListPlaces
places={ mPlaces }
onChangeFilter={ this.onChangeFilter }
...
/>
<Map
locations={ mPlaces }
...
/>
</div>
)
}
}
ListPlaces.js
class ListPlaces extends React.Component {
updateQuery = (query) => {
this.props.onChangeFilter(query)
}
...
}
this is a common pattern in React - so common it has its own page in the React docs.
What I am trying to do:
Be able to click a marker and have it show the InfoWindow and then be able to click the marker again to close it or just close the pop up window manually. Also want to be able to open multiple markers at once.
What works:
I can click to open InfoWindow and click again to close the window. If I close one marker then I can open another one.
What doesn't work:
If one marker is already open, and I try to click another to open it I get:
Target container is not a DOM element
The above error occurred in the <InfoWindow> component:
This happens only when I click another marker if another marker is already open. I will post the code below. I have seperated out the MapComponent into a separate file.
MapComponent:
import React from 'react';
import { compose, withProps } from 'recompose';
import { withScriptjs, withGoogleMap, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
import config from '../config';
import { MarkerInfo } from '../components/view';
export const MapComponent = compose(
withProps({
googleMapURL: `https://maps.googleapis.com/maps/api/js?key=${config.GOOGLE_API_KEY}&v=3.exp&libraries=geometry,drawing,places`,
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `500px` }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap defaultZoom={8} center={props.center}>
{props.isMarkerShown &&
props.center && (
<Marker position={props.center} title={"User's Location"} onClick={props.onHomeMarkerClick}>
<InfoWindow>
<div>User's Location</div>
</InfoWindow>
</Marker>
)}
{props.markers.map((marker, i) => {
const onClick = () => props.onMarkerClick(marker);
const onCloseClick = () => props.onCloseClick(marker);
return (
<Marker key={i} position={marker.position} title={marker.title} onClick={onClick}>
{marker.showInfo && (
<InfoWindow onCloseClick={onCloseClick}>
<div>
<MarkerInfo marker={marker} />
</div>
</InfoWindow>
)}
</Marker>
);
})}
</GoogleMap>
));
React Component Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import geolib from 'geolib';
import { geolocated } from 'react-geolocated';
import axios from 'axios';
import actions from '../../actions';
import { MapComponent, Geocode } from '../../utils';
class GhostMap extends Component {
constructor(props) {
super(props);
this.state = {
isMarkerShown: true,
currentLocation: null,
radius: 100,
markers: []
};
this.handleMarkerClick = this.handleMarkerClick.bind(this);
this.handleCloseClick = this.handleCloseClick.bind(this);
this.handleHomeMarkerClick = this.handleHomeMarkerClick.bind(this);
this.createMarkersWithinRadius = this.createMarkersWithinRadius.bind(this);
this.updateRadius = this.updateRadius.bind(this);
}
componentDidMount() {
if (this.props.posts.all) {
return;
}
console.log(this.props);
this.props
.fetchPosts()
.then(data => {
return data;
})
.catch(err => {
console.log(err);
});
}
componentWillReceiveProps(props) {
if (props.coords) {
this.setState({
currentLocation: {
lat: props.coords.latitude,
lng: props.coords.longitude
}
});
this.createMarkersWithinRadius(
props.posts.all,
this.state.radius,
props.coords.latitude,
props.coords.longitude
)
.then(data => {
this.setState({
markers: data
});
return data;
})
.catch(err => {
console.log(err);
});
} else {
axios
.get('http://ip-api.com/json')
.then(response => {
this.setState({
currentLocation: {
lat: response.data.lat,
lng: response.data.lon
}
});
return this.createMarkersWithinRadius(
props.posts.all,
this.state.radius,
response.data.lat,
response.data.lon
);
})
.then(data => {
this.setState({
markers: data
});
})
.catch(err => {
console.log(err);
});
}
}
createMarkersWithinRadius(posts, radius, lat, lng) {
return new Promise((resolve, reject) => {
const markers = [];
const currentLocation = {
latitude: lat,
longitude: lng
};
posts.map(post => {
let postGeolocation = {};
Geocode(post.address, post.city, post.state, post.zipCode)
.then(response => {
postGeolocation.lat = response.lat;
postGeolocation.lng = response.lng;
const distanceArr = geolib.orderByDistance(currentLocation, [postGeolocation]);
const miles = (distanceArr[0].distance / 1609.34).toFixed(2);
if (miles <= radius) {
markers.push({
id: post.id,
position: postGeolocation,
title: post.title,
description: post.text,
image: post.image,
showInfo: false
});
}
resolve(markers);
})
.catch(err => {
reject(err);
});
});
});
}
handleMarkerClick(targetMarker) {
this.setState({
markers: this.state.markers.map(
marker =>
marker.id === targetMarker.id ? { ...marker, showInfo: !marker.showInfo } : marker
)
});
}
handleCloseClick(targetMarker) {
this.setState({
markers: this.state.markers.map(marker => {
if (marker._id === targetMarker._id) {
return {
...marker,
showInfo: false
};
}
return marker;
})
});
}
handleHomeMarkerClick() {
this.setState({ isMarkerShown: false });
}
updateRadius(event) {
const radius = event.target.value;
const posts = this.props.posts.all;
const { lat, lng } = this.state.currentLocation;
if (typeof radius !== 'number') {
alert('Please put in a number');
return;
}
if (this.props.posts.all && this.state.currentLocation) {
this.createMarkersWithinRadius(this.props.posts.all, radius, lat, lng);
}
}
render() {
if (this.state.markers.length === 0) {
return <div>Loading...</div>;
}
return (
<div className="row">
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="text">Radius</label>
<input
onChange={this.updateRadius}
className="form-control"
id="radius"
type="Number"
placeholder="Radius"
/>
</div>
</div>
<div className="col-sm-12">
<MapComponent
onMarkerClick={this.handleMarkerClick}
isMarkerShown={this.state.isMarkerShown}
onHomeMarkerClick={this.handleHomeMarkerClick}
center={this.state.currentLocation}
markers={this.state.markers}
onCloseClick={this.handleCloseClick}
/>
</div>
</div>
);
}
}
const stateToProps = state => {
return {
posts: state.post
};
};
const dispatchToProps = dispatch => {
return {
fetchPosts: () => dispatch(actions.fetchPosts())
};
};
const loadData = store => {
return store.dispatch(actions.fetchPosts());
};
export default {
loadData: loadData,
component: connect(stateToProps, dispatchToProps)(
geolocated({
positionOptions: {
enableHighAccuracy: false
},
userDecisionTimeout: 5000
})(GhostMap)
)
};