I am building a react-leaflet application, and I am trying to separate the zoom control from the map itself. The same question in a vanilla Leaflet context was asked here: Placing controls outside map container with Leaflet?. This is what I'm trying to accomplish within the react-leaflet framework. Here is the general outline of my project:
import UIWindow from './UIWindow'
import Map from './MapRL'
class App extends React.Component {
render () {
return (
<div className="App">
<Map />
<UIWindow />
</div>
)
}
}
export default App;
My map component looks like this:
import React from 'react'
import { Map as LeafletMap, TileLayer } from 'react-leaflet'
class Map extends React.Component {
render () {
return(
<LeafletMap
className="sidebar-map"
center={...}
zoom={...}
zoomControl={false}
id="mapId" >
<TileLayer
url="..."
attribution="...
/>
</LeafletMap>
)
}
}
export default Map;
Then my UIWindow looks like this:
class UIWindow extends React.Component {
render () {
return(
<div className="UIWindow">
<Header />
<ControlLayer />
</div>
)
}
}
And finally, my ControlLayer (where I want my ZoomControl to live) should look something like this:
class ControlLayer extends React.Component {
render () {
return (
<div className="ControlLayer">
<LeftSidebar />
<ZoomControl />
{this.props.infoPage && <InfoPage />}
</div>
)
}
}
Of course with this current code, putting ZoomControl in the ControlLayer throws an error: TypeError: Cannot read property '_zoom' of undefined, with some more detailed writeup of what's wrong, with all the references the Leaflet's internal code regarding the zoom functionality.
(DomUtil.removeClass(this._zoomInButton, className);, etc.)
I expected an error, because the ZoomControl is no longer a child of the <Map /> component, but rather a grandchild of the <Map />'s sibling. I know react-leaflet functions on its context provider and consumer, LeafletProvider and LeafletConsumer. When I try to call on my LeafletConsumer from within my <ControlLayer />, I get nothing back. For example:
<LeafletConsumer>
{context => console.log(context)}
</LeafletConsumer>
This logs an empty object. Clearly my LeafletConsumer from my ControlLayer is not properly hooked into the LeaflerProvider from my <Map />. Do I need to export the context from the Map somehow using LeafletProvider? I am a little new to React Context API, so this is not yet intuitive for me. (Most of the rest of the app will be using React Redux to manage state changes and communication between components. This is how I plan to hook up the contents of the sidebar to the map itself. My sidebar doesn't seem to have any problem with being totally disconnected from the <Map />).
How can I properly hook this ZoomControl up to my Map component?
UPDATE:
I tried capturing the context in my redux store, and then serving it to my externalized ZoomControl. Like so:
<LeafletConsumer>
{ context => {
this.props.captureContext(context)
}}
</LeafletConsumer>
This captures the context as part of my redux store. Then I use this as a value in a new context:
// ControlLayer.js
const MapContext = React.createContext()
<MapContext.Provider value={this.props.mapContext}>
<LeftSidebar />
<MapContext.Consumer>
{context => {
if (context){
console.log(ZoomControl);
}
}}
</MapContext.Consumer>
</MapContext.Provider>
Where this.props.mapContext is brought in from my redux matchStateToProps, and its exactly the context captured by the captureContext function.
Still, this is not working. My react dev tools show that the MapContent.Consumer is giving the exact same values as react-leaflet's inherent '' gives when the ZoomControl is within the Map component. But I still get the same error message. Very frustrated over here.
Here is the same approach without hooks:
the Provider should look like this:
class Provider extends Component {
state = { map: null };
setMap = map => {
this.setState({ map });
};
render() {
return (
<Context.Provider value={{ map: this.state.map, setMap: this.setMap }}>
{this.props.children}
</Context.Provider>
);
}
}
Leaflet component will be:
class Leaflet extends Component {
mapRef = createRef(null);
componentDidMount() {
const map = this.mapRef.current.leafletElement;
this.props.setMap(map);
}
render() {
return (
<Map
style={{ width: "80vw", height: "60vh" }}
ref={this.mapRef}
center={[50.63, 13.047]}
zoom={13}
zoomControl={false}
minZoom={3}
maxZoom={18}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?"
/>
</Map>
);
}
}
and now to access the setMap function to the compoenntDidMount you need to do the following:
export default props => (
<Context.Consumer>
{({ setMap }) => <Leaflet {...props} setMap={setMap} />}
</Context.Consumer>
);
For the rest take a look here: Demo
I am not sure how to achieve that using your approach where react-leaflet's wrapper ZoomControl is not a child of Map wrapper when you try to place it outside the Map wrapper.
However, for a small control like the ZoomControl, an easy solution would be to create a custom Zoom component, identical to the original, construct it easily using the native css style and after accessing the map element, invoke the zoom in and out methods respectively.
In the below example I use react-context to save the map element after the map loads:
useEffect(() => {
const map = mapRef.current.leafletElement;
setMap(map);
}, [mapRef, setMap]);
and then here use the map reference to make a custom Zoom component identical to the native (for css see the demo):
const Zoom = () => {
const { map } = useContext(Context);
const zoomIn = e => {
e.preventDefault();
map.setZoom(map.getZoom() + 1);
};
const zoomOut = e => {
e.preventDefault();
map.setZoom(map.getZoom() - 1);
};
return (
<div className="leaflet-bar">
<a
className="leaflet-control-zoom-in"
href="/"
title="Zoom in"
role="button"
aria-label="Zoom in"
onClick={zoomIn}
>
+
</a>
<a
className="leaflet-control-zoom-out"
href="/"
title="Zoom out"
role="button"
aria-label="Zoom out"
onClick={zoomOut}
>
−
</a>
</div>
);
};
and then place it wherever you like:
const App = () => {
return (
<Provider>
<div style={{ display: "flex", flexDirection: "row" }}>
<Leaflet />
<Zoom />
</div>
</Provider>
);
};
Demo
Related
When I use google-maps-react to integrate maps, map loads correctly and even shows the correct markers but I am having some issues.
1- A styling issue normally map component overlays the content I think because it has position absolute but when I change its position to relative it does not show. I have placed it in a div everything above the div shows but it overlays the content below the div.
2- it is giving errors in the console (I am listing them below)
Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. Move data fetching code or side effects to componentDidUpdate.If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps.
Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the Wrapper component.
My mapComponent:
import React from "react";
import { Map, Marker, GoogleApiWrapper } from "google-maps-react";
export function MapContainer({ google, captains = [] }) {
const mapStyles = {
width: "85%",
height: "100%",
};
return (
<Map
google={google}
// containerStyle={{
// position: "absolute",
// width: "85%",
// height: "40%",
// }}
// style={mapStyles}
containerStyle={mapStyles}
initialCenter={{
lat: captains[0].location.coordinates[1],
lng: captains[0].location.coordinates[0],
}}
zoom={captains.length === 1 ? 18 : 13}
>
{captains.map((captain) => (
<Marker
key={captain._id}
position={{
lat: captain.location.coordinates[1],
lng: captain.location.coordinates[0],
}}
/>
))}
</Map>
);
}
export default GoogleApiWrapper({
apiKey: "#############################",
})(MapContainer);
Here is where I am using it:
import MapContainer from "../components/MapContainer";
function Captains({ captains }) {
return (
<div className=" h-screen overflow-auto">
<div className="pt-24 pl-20 flex flex-col">
<othercontent ..... />
<div className="visible h-1/3 w-10/12 mx-5">
<MapContainer captains={captains} />
</div>
<PaginationTable
.....
/>
</div>
</div>
</>
);
}
I have the following code which has an overlay image on the map.
However, i would like to display markers on top of this overlay image. The markers come from a users array which is constantly being updated. I want to loop through these markers and place them on the map, but keep updating them if they change longitude/latitude.
I have tried some code below, the data is getting passed to the components correctly, but the markers are always staying in the same position. Could anyone tell me what i'm doing wrong?
Below is my main map code
return (
<div className='map'>
<div className='google-map'>
<GoogleMapReact
bootstrapURLKeys={{ key: 'KEYID' }}
defaultCenter={location}
defaultZoom={zoomLevel}>
{users?.map((user, i) => {
return (
<MyGreatPlace
key={i}
latitude={user.latitude}
longitude={user.longitude}
sender={user.sender}
/>
);
})}
<OverlayImage lat={location.lat} lng={location.lng} text={'A'} />
</GoogleMapReact>
</div>
</div>
);
Below is MyGreatPlace.jsx file for the markers:
export default function MyGreatPlace({ i, latitude, longitude, sender }) {
console.log(sender);
return (
<div style={{ width: 60, height: 60, background: 'green' }}>
{latitude} {longitude} {sender}
</div>
);
}
Below is OverlayImage.jsx file for the overlay image:
export default class OverlayImage extends Component {
static propTypes = {
text: PropTypes.string,
};
static defaultProps = {};
shouldComponentUpdate = shouldPureComponentUpdate;
render() {
return <div>{<img src={floorplan} alt='floorplan' />}</div>;
}
}
Do i need to make the MyGreatPlace component the same as the overlay component? I'm not sure how props would work getting the longitude/latitude passed in?
The solution was to change MyGreatPlace file to the same as the overlay component, to look like this:
export default class MyGreatPlace extends Component {
static propTypes = {
text: PropTypes.string,
};
static defaultProps = {};
shouldComponentUpdate = shouldPureComponentUpdate;
render() {
return <div>{this.prop.text}</div>;
}
}
So I'm rendering a bunch of <Marker /> that are giving by google-maps-react
The problem is that it don't seems like google-maps-react likes it when I add a <Link /> that comes from react-router-dom
This is how I put it together :
render() {
return (
<div>
<Map
google={this.props.google}
zoom={14}
styles={this.props.mapStyles}
disableDefaultUI={true}
onClick={this.saveCoords}
>
{this.state.data.map(m => {
return (
<Link to={`/c/contribution/${m.id}`}>
<Marker position={{ lat: m.x, lng: m.y }} title={m.title} />
</Link>
)
})}
</Map>
</div>
)
}
I tried using window.location instead but this reloads the page and I don't want that.
I get this error with the code above, it don't really makes sense for me :
Warning: React does not recognize the `mapCenter` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `mapcenter` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
With this I try to achieve a clickable <Marker/> that will render another element. This specific elemend can be accessed by going to the Route present in the code example.
The route used :
<Route path='/c/contribution/:id' component={Contribution} />
Unfortunately, you cannot wrap markers with google-maps-react like that because of how the map's children are rendered. I didn't look into that too deep, so why exactly it works the way it does is unclear to me.
With google-maps-react I think the way to go is to attach click handlers, but that makes working with react-router a bit more complicated.
For maybe an easier path, you could try the google-map-react package. It works a bit differently, but makes it easy to render almost anything on the map.
You can use the onClick event on the markers for the redirect. Set up a state property for the path you want to redirect to, and if that property is not an empty string , instead of renering your normal component, return a <Redirect/> element.
constructor(props){
super(props)
this.state = {
...
redirectTo:"
};
}
this.setRedirect = (path)=>{
this.setState({
redirectTo:path
}
}
render(){
if(this.state.redirectTo !== "")
return <Redirect to={this.state.redirectTo}/>
else
return (
<div>
<Map
google={this.props.google}
zoom={14}
styles={this.props.mapStyles}
disableDefaultUI={true}
onClick={this.saveCoords}
>
{this.state.data.map(m => {
return (
<Marker onClick={()=>this.setRedirect(`/c/contribution/${m.id}`)} position={{ lat: m.x, lng: m.y }} title={m.title} />
)
})}
</Map>
</div>
)
}
I have a simple React component that renders multiple buttons from an array in my props. I'm applying the ripple on DidMount, however, it's only attaching on the first button, the rest are being ignored. It looks like the attachTo only takes the first element. Is there another way to attach to all the buttons on didmount?
class NavBar extends Component {
constructor(props) {
super(props);
this.state = {
links
};
}
componentDidMount() {
MDCRipple.attachTo(document.querySelector('.mdc-button'));
}
render() {
return (
<section>
{this.state.links.map((link, i) => {
return (
<StyledLink key={i} to={link.url}>
<StyledButton className="mdc-button">
<StyledIcon className="material-icons">{link.icon}</StyledIcon>
<StyledTypography className="mdc-typography--caption">
{link.title}
</StyledTypography>
</StyledButton>
</StyledLink>
);
})}
</section>
);
}
}
Final markup
<a class="sc-iwsKbI bhaIR">
<button class="mdc-button sc-dnqmqq ksXmjj mdc-ripple-upgraded" style="--mdc-ripple-fg-size:57.599999999999994px; --mdc-ripple-fg-scale:2.1766951530355496; --mdc-ripple-fg-translate-start:-7.799999999999997px, 19.200000000000003px; --mdc-ripple-fg-translate-end:3.200000000000003px, 19.200000000000003px;">
...content
</button>
</a>
<a class="sc-iwsKbI bhaIR">
<button class="mdc-button sc-dnqmqq ksXmjj">
...content
</button>
</a>
Updated
I was able to find a way to use the attachTo with each button, but it still seems like there's a better way.
I changed by componentDidMount() to:
componentDidMount() {
this.state.links.forEach((link) => {
MDCRipple.attachTo(document.getElementById(`button-navbar-${link.id}`));
});
}
and then changed my render to
<StyledButton id={`button-navbar-${link.id}`} className="mdc-button">
Is there a way to do this without having to iterate through the array?
The react way to do this is to write component that injects the necessary logic.
class RippleButton extends Component {
const handleRef = elem => MDCRipple.attachTo(elem);
render() {
return (
<StyledButton {...this.props} ref={this.handleRef} />
);
}
}
Then render that component instead of your original StyledButton component and it will call the MDCRipple.attachTo() itself with its ref.
Depending on how the StyledButton is implemented you may need to use another prop to get the ref to the underlying DOM element. You did not provide enough of your code to exactly know this.
Alright, I'm going to do my best to explain how my project is setup so that you can appropriately aid me on my quest to figure out how to approach this configuration.
I have a parent component that is a smart component. Through this component all my data from my store is being accessed.
class DashboardPage extends React.Component {
componentDidMount() {
this.props.getTips();
}
render() {
return (
<div>
<div className="row">
<div className="col-sm-7">
<ContentBox
title="The Scoop"
footerText="Submit a Story"
showSlider
content={<TipOfTheDay tips={this.props.tips} />}
/>
</div>
</div>
</div>
)
}
}
DashboardPage.propTypes = {
getTips: PropTypes.func
}
function mapStateToProps(state, ownProps) {
tips: state.tips
}
function mapDispatchToProps(dispatch) {
return {
getTips: () => { dispatch(tipActions.loadTips());} ## This hits tipActions and runs the `action creator`: loadTips(). Which returns all tips from api.
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
As you can see, I have included two dumb components inside my smart component, <ContentBox/> & <TipOfTheDay/>. On the dashboardPage there are about 7 <ContentBox/> components, each inheriting special a title for the header/footer and also being told whether or not to display the footer through the showSlider boolean. Here is what <ContentBox/> looks like:
import React, {PropTypes} from 'react';
import Footer from './ContentBoxFooter';
const ContentBox = ({title, footerText, showSlider, content}) => {
return (
<div style={styles.root} className="col-sm-12">
<div style={styles.header} className="row">
<h3 style={styles.header.title}>{title}</h3>
<span style={styles.header.arrow} />
</div>
{content}
<Footer footerText={footerText} showSlider={showSlider} />
</div>
);
};
ContentBox.propTypes = {
title: PropTypes.string,
footerText: PropTypes.string,
showSlider: PropTypes.bool,
content: PropTypes.object
};
export default ContentBox;
And here is the footer:
import React, {PropTypes} from 'react';
import styles from './contentBoxStyles';
import Previous from './svg/Previous';
import Next from './svg/Next';
import Add from './svg/Add';
import consts from '../../styles/consts';
const ContentBoxFooter = ({footerText, showSlider}) => {
if (footerText != undefined) {
return (
<div style={styles.footer} className="row">
{
showSlider ?
<div>
<Previous fillColor={consts.orange} height="20px" width="20px"/>
<span style={styles.bar}>|</span>
<Next fillColor={consts.orange} width="20px" height="20px"/>
</div> : <div style={styles.emptyArrow} />
}
<div style={styles.footer.link}>
<span style={styles.footer.link.text}>{footerText}</span>
<Add fillColor={consts.orange} height="24px" width="24px" />
</div>
</div>
);
} else {
return(null);
}
};
ContentBoxFooter.propTypes = {
footerText: PropTypes.string,
showSlider: PropTypes.bool
};
export default ContentBoxFooter;
Few! So here is where I need to add the onClick functionality. This functionality needs to be added to the <Previous/> & <Next/> component that is an SVG. What I am attempting to do is create a slider for the tips that I have pulled in. Obviously there will be <Footer/> components that will need the same functionality, but controlling different data other than the tips. Because I am new to React & Redux, I am not sure how I can perform this and not just do it, but do it in the 'Redux` way.
How do I get these two svg components that are nested within other dumb components that are dumb components, to perform onClick functions for specific data on the page? I hope this made sense. For more clarity, here is what I am doing with the <TipOfTheDay/> component:
const tipOfTheDay = ({tips}) => {
return (
<div style={styles.tipBody} className="row">
{
tips.map(function(tip, key) {
return (
<div key={key} className="myTips">
<h3 style={styles.tipBody.header}>{tip.title}</h3>
<p style={styles.tipBody.content}>{tip.content}</p>
</div>
);
})
}
</div>
);
};
tipOfTheDay.propTypes = {
tips: PropTypes.array.isRequired
};
export default tipOfTheDay;
Thank you for anytime you spend reading/responded/assisting with this question. I am a fairly new developer and this is also new technology to me.
I'm not sure how you've implemented your Next and Previous Components, but since you've using React-Redux, you can create extra Containers to wrap those components and pass in a Redux Action to them, e.g.:
// PreviousComponent.jsx
var Previous React.createClass({
propTypes: {
goToPrevious: React.PropTypes.func,
},
render: function() {
return (
<div onClick={this.props.goToPrevious}>
...
</div>
);
}
};
export default Previous;
//PreviousContainer.jsx
import ReactRedux from 'react-redux';
import Previous from './PreviousComponent';
var mapStateToProps = (state, props) => {
return {};
};
var mapDispatchToProps = (dispatch) => {
return {
goToPrevious: () => {
dispatch(Actions.goToPrevious());
},
}
};
var PreviousContainer = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps
)(Previous);
export default PreviousContainer;
By adding a container wrapper directly around your component, you can connect a redux action for going to the previous image/slide/whatever directly into your React component. Then, when you want to use the action in your ContentBoxFooter, you import the PreviousContainer and place it where you want the Previous component, e.g.:
//ContentBoxFooter.jsx
import PreviousContainer from './PreviousContainer'
const ContentBoxFooter = ({footerText, showSlider}) => {
if (footerText != undefined) {
return (
<div style={styles.footer} className="row">
{
showSlider ?
<div>
/*
* Add the PreviousContainer here where before you were only using your regular Previous component.
*/
<PreviousContainer fillColor={consts.orange} height="20px" width="20px"/>
<span style={styles.bar}>|</span>
<Next fillColor={consts.orange} width="20px" height="20px"/>
</div> : <div style={styles.emptyArrow} />
}
<div style={styles.footer.link}>
<span style={styles.footer.link.text}>{footerText}</span>
<Add fillColor={consts.orange} height="24px" width="24px" />
</div>
</div>
);
} else {
return(null);
}
};
ContentBoxFooter.propTypes = {
footerText: PropTypes.string,
showSlider: PropTypes.bool
};
By wrapping both the Next and Previous components in containers that pass actions into them, you can connect the Redux actions directly into your components without having to pass them from the root component of your application. Also, doing this allows you to isolate where certain actions are called. Your Previous button is probably the only component that would want to call a Previous action, so by placing it in a Container wrapper around the component, you're making sure that the Previous action is only used where it is needed.
Edit:
If you have to deal with multiple actions, it is better to define them at a higher level. In this case, since the ContentBox is the common breaking point, I would define separate Previous actions for each type of content box and pass them into each ContentBox instance:
var DashboardApp = React.createClass({
propTypes: {
TipsPrevious: React.PropTypes.function,
NewsPrevious: React.PropTypes.function,
},
render: function() {
<div>
<ContentBox
previousAction={this.props.TipsPrevious}
contentType='Tips'
/>
<ContentBox
previousAction={this.props.NewsPrevious}
contentType='News'
/>
...
</div>
},
});
Pass the actions down through the child components until you reach the Previous component and then attach the action to an 'onClick' handler on the Previous component.
The idea here behind this is that you want to limit the scope of parameters to the least amount of code possible. For example, if you added a profile component showing your user information on the page, you might want to add a container around that component and pass in the User-related information/actions without passing the information to the rest of your application. By doing this, it makes it easier to focus information where it is needed. It also helps you figure out where some change/action is taking place in your code if you have to fix a bug.
In the example above, if the Dashboard component is your root component, you'll just pass the Redux Actions into through a container wrapping it. However, if your dashboard component is a nested component itself, pass the actions into it through a custom container so that the actions aren't spread to code that don't need to see it:
<AppRoot>
<PageHeader/>
<DashboardContainer />
<PageSidebar />
<PageFooter />
</AppRoot>