okay so i am using the react-leaflet library to bring a map into my page which i have created with react and nextjs. I have added components from react-leaflet-draw library to allow user to draw features on the map.
My goal is to have a popup open when user finishes drawing a feature. Inside that popup will be a simple form where user can enter "name" and "description" and when clicking "save" a redux action will be dispatched, in its payload name, description and geojson of the drawn feature.
I am able to open a Popup when user finished drawing, fill it with a simple HTML form and, independent from that, also extract the drawn feature as GeoJSON. My Problem is that i am not able to extract the contents of the input fields.
this is the functional component that renders the map:
import 'leaflet/dist/leaflet.css';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css';
import 'leaflet-defaulticon-compatibility';
import 'leaflet-draw/dist/leaflet.draw.css'
import { FeatureGroup, MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import { locationsType } from '../../pages/api/starterSet'
import { EditControl } from 'react-leaflet-draw'
import styles from '../../styles/components/popupForm.module.css'
import L from 'leaflet';
import PopupForm from './PopupForm'
import { useDispatch } from 'react-redux';
import { testing } from '../../reduxState/reduxState'
import ReactDOMServer from 'react-dom/server'
interface leafletMapProps {
locations: locationsType
drawnLayersRef: any
}
const LeafletMap = ({ locations, drawnLayersRef }:leafletMapProps) => {
const dispatch = useDispatch()
//creating button and its event listener that dispatches action
const button = L.DomUtil.create('button');
button.innerHTML = 'Save';
button.addEventListener('click', () => {
console.log(
"eventlistener triggered, input content: ",
document.getElementById('popupFormName')?.innerHTML
)
dispatch(testing())
});
//creating popupcontent out of simple html form and adding button
const container = L.DomUtil.create('div');
container.innerHTML = ReactDOMServer.renderToString(<PopupForm/>);
container.appendChild(button);
//creating custom popup and filling it with custom content
const popup = L.popup();
popup.setContent(container);
return (
<>
<MapContainer center={[52.5200, 13.4050]} zoom={13} scrollWheelZoom={true} style={{height: 400, width: "100%"}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<FeatureGroup ref={drawnLayersRef}>
<EditControl
position="topright"
draw={{
rectangle: false,
circle: true,
circlemarker: false,
polyline: {
showLength: true,
metric: true },
polygon: {
allowIntersection: false, // Restricts shapes to simple polygons
drawError: {
color: 'red', // Color the shape will turn when intersects
message: '<strong>That is a terrible polygon! Draw that again!'
}
}
}}
onCreated={(e) => {
console.log("onCreated!")
console.log("CREATED LAYER", e.layer.toGeoJSON())
e.layer.bindPopup(popup).openPopup();
}}
/>
</FeatureGroup>
</MapContainer>
</>
)
}
export default LeafletMap
and this is the functional component that contains the html form
import styles from '../../styles/components/popupForm.module.css'
const PopupForm = (ref: any) => {
return (
<form className={styles.form}>
<input
id='popupFormName'
name='name'
placeholder='name...'
ref={ref}
className={styles.inputField}
/>
<textarea
id='popupFormDescr'
name="description"
placeholder="description (max 300 characters)"
maxLength={300}
className={styles.inputTextarea}
/>
</form>
)
}
export default PopupForm
I am creating the contents of the popup using the ReactDOM.renderToString method because in react-leaflet you unfortunately cant render JSX in a popup directly. This solution was suggested here.
I try to extract the input fields contents with plain Javascript, using getElementByID but the innerHTML property returns empty. When i console.log the HTML element itself i get
<input id="popupFormName" name="name" placeholder="name..." class="popupForm_inputField__iQuhs">
which i think is the initial state of the element, the state that is in when the renderToString method executes. So it seems that after renderToString executes, the browser is somehow not sensitive anymore to changes that happen to these html elements, even though it renders them correctly.
I have tried to work with Reacts`s useRef hook, in two ways: 1) creating a ref on the level of the map component, handing it down to the PopupForm component via props and assigning it there to the HTML input element and 2) by using the ForwardRef component. In both cases i was able to console.log the actual HTML input element that had the ref assigned but its value property were also empty.
I have considered the ReactDOM.findDOMNode method but it is legacy and the documentation states it doesnt work with functional components.
I am looking for A) a way to extract the content of the HTML input elements within the popup, sticking to my approach with the renderToString method or B) an alternative way to bring HTML Code or ideally JSX code into a popup that is known to work with my usecase
help is much appreciated!
okay so i got it to work by changing the way the popup content is constructed. Instead of using the ReactDOM.renderToString method i now use the ReactDOM.render method.
This is the whole component now
import 'leaflet/dist/leaflet.css';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css';
import 'leaflet-defaulticon-compatibility';
import 'leaflet-draw/dist/leaflet.draw.css'
import { FeatureGroup, MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import { locationsType } from '../../pages/api/starterSet'
import { EditControl } from 'react-leaflet-draw'
import styles from '../../styles/components/popupForm.module.css'
import L from 'leaflet';
import { useDispatch } from 'react-redux';
import { testing } from '../../reduxState/reduxState'
import * as ReactDOM from 'react-dom/client';
interface leafletMapProps {
locations: locationsType
drawnLayersRef: any
}
const LeafletMap = ({ locations, drawnLayersRef }:leafletMapProps) => {
const dispatch = useDispatch()
const createPopupContent = (geoJsonString: string) => {
return <form
className={styles.form}
onSubmit={(event: React.FormEvent<HTMLFormElement> & { target: HTMLFormElement }) => {
console.log("FORMSUBMIT FUNC TRIGGERD")
event.preventDefault()
const formData = Object.fromEntries(new FormData(event.target));
console.log("FORMDATA: ", formData, "GEOJSON: ", geoJsonString)
dispatch(testing())
}
}
>
<input
id='popupFormName'
name='name'
placeholder='name...'
className={styles.inputField}
/>
<textarea
id='popupFormDescr'
name="description"
placeholder="description (max 300 characters)"
maxLength={300}
className={styles.inputTextarea}
/>
<input
id='submitBtn'
type='submit'
name='Submit!'
/>
</form>
}
const renderPopupForm = (geoJsonString: string) => {
const popup = L.popup();
const container = L.DomUtil.create('div');
popup.setContent(container);
const root = ReactDOM.createRoot(container);
root.render(createPopupContent(geoJsonString));
return popup;
}
return (
<>
<MapContainer center={[52.5200, 13.4050]} zoom={13} scrollWheelZoom={true} style={{height: 400, width: "100%"}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<FeatureGroup ref={drawnLayersRef}>
<EditControl
position="topright"
draw={{
rectangle: false,
circle: true,
circlemarker: false,
polyline: {
showLength: true,
metric: true },
polygon: {
allowIntersection: false,
drawError: {
color: 'red',
message: '<strong>That is a terrible polygon!'
},
}
}}
onCreated={(e) => {
const geoJsonString = e.layer.toGeoJSON()
e.layer.bindPopup(renderPopupForm(geoJsonString), {
closeButton: false
}).openPopup();
}}
/>
</FeatureGroup>
</MapContainer>
</>
)
}
export default LeafletMap
I am handing down the GeoJSON string along the functions, which is a bit clunky and i extract the input values via "new FormData" inside the formsubmit eventhandler, which is not the convention.
I have tried to rewrite those two using usestate hooks but then calling upon those states inside the formsubmit eventhandler would return empty values, no idea why, probably has to do with the async nature of usestate.
I have also tried to replace createPopupContent() with the import of a functional component but that throws an error.
So far the thing works as i want it too but if anyone has suggestions for improvements they are very appreciated
Related
I'm trying to use eventHandlers with react-leaflet in a next.js application, but the events are never being fired.
Here is an example of the code I'm using in my Map component:
import { MapContainer, Rectangle } from "react-leaflet";
const Map = () => {
return (
<MapContainer center={[25, 25]} zoom={1} style={{ height: 600, width: 600 }}>
<Rectangle
bounds={[
[0, 0],
[50, 50],
]}
eventHandlers={{
click: () => console.log("clicked.."),
}}
/>
</MapContainer>
);
};
export default Map;
I've tried using this code outside of a next.js application (plane react app) and it worked as expected, but when I use it in a next.js application, the event is never fired.
Here is an example of how I'm rendering the Map component in my index.js file:
import dynamic from "next/dynamic";
const Map = dynamic(() => import("../components/Map"), {
ssr: false,
});
export default function Home() {
return (
<>
<Map />
</>
);
}
I also tried using this code in a codesandbox, but I couldn't get the sandbox to work (it crashed). Also tested it on the live editor on https://react-leaflet.js.org/docs/example-popup-marker/ and replacing the marker with the rectangle component, the event is triggered on click.
Has anyone else experienced this issue and found a solution?
Thanks in advance for any help!
Trying to learn React and following this tutorial:https://youtu.be/GDa8kZLNhJ4?t=3547
There you have a App.js component that makes Travel Advisor API call that populates the data object:
import React, { useState, useEffect } from "react";
import { CssBaseline, Grid } from "#material-ui/core";
import { getPlacesData } from "./api";
import Header from "./components/Header/Header";
import List from "./components/List/List";
import Map from "./components/Map/Map";
import { PlaceSharp } from "#material-ui/icons";
const App = () => {
const [places, setPlaces] = useState([]);
const [coordinates, setCoordinates] = useState({});
const [bounds, setBounds] = useState(null);
useEffect(() => {
getPlacesData().then((data) => {
console.log(data) // data is there
setPlaces(data);
});
}, []);
return (
<>
<CssBaseline />
<Header />
<Grid container spacing={3} style={{ width: "100%" }}>
<Grid item xs={12} md={4}>
<List />
</Grid>
<Grid item xs={12} md={8}>
<Map
setCoordinates={setCoordinates}
setBounds={setBounds}
coordinates={coordinates}
/>
</Grid>
</Grid>
</>
);
};
export default App;
The following props are passed to Map component:
<Map
setCoordinates={setCoordinates}
setBounds={setBounds}
coordinates={coordinates}
/>
In Map component it gets passed to GoogleMapReact component:
import React from 'react'
import GoogleMapReact from 'google-map-react'
import {Paper, Typography, useMediaQuery} from '#material-ui/core'
import LocationOnOutlinedIcon from '#material-ui/icons/LocationOnOutlined'
import Rating from "#material-ui/lab"
import useStyles from './styles'
const Map = ({setCoordinates, setBounds, coordinates}) => {
const classes = useStyles()
const isMobile = useMediaQuery('(min-width: 600px)')
//console.log(coordinates)
//const coordinates= {lat: 0, lng: 0}
return (
<div className={classes.mapContainer}>
<GoogleMapReact
bootstrapURLKeys={{ key: 'xxxxxxxxxxxxxxxxxxxx'}}
defaultCenter ={coordinates}
center = {coordinates}
defaultZoom = {14}
margin = {[50, 50, 50, 50]}
options = {''}
onChange = {(e) => {
console.log(e) // this is empty but it should have data
setCoordinates({lat: e.center.lat, lng: e.center.lng});
}}
onChildClick = {''}
>
</GoogleMapReact>
</div>
)
}
export default Map
For some reason coordinates prop is not populated in onChange as seen in the video.
I double check the code and cannot find what is stopping it from getting the data.
The API call returns a bunch of restaurants like this:
So it is fetching the data. Only props {coordinates} not getting filled.
Can you see where can be the issue?
There are two pieces of state that handle some state. Those are places and coordinates. Once the App component is loaded, it tries to fetch places and update its state, triggering a re rendering. So far, so good.
The Map Component receives as prop the value of coordinates. coordinates never changes in the flow of the snippet that you posted. Maybe you want to fetch some coordinates from another endpoint? Or maybe from the places data, map through it and set a new state?. Same applies for bounds.
What it looks like it is missing is a call to setCoordinates and setBounds with the new values.
For my project, I'm using MUI's Select Component with the LanguageIcon as the IconComponent.
What I'm trying to do is turn this icon white (it's black per default), but I can't get it to work.
I tried to follow this solution, but it won't work for me.
import { makeStyles } from '#mui/styles'; throws "Module not found: Can't resolve '#mui/styles'" and on their website it says #mui/styles is deprecated.
This is what I currently have:
import * as React from 'react';
import { FunctionComponent } from 'react';
import MenuItem from "#mui/material/MenuItem";
import FormControl from "#mui/material/FormControl";
import Select, {SelectChangeEvent} from "#mui/material/Select";
import { useRouter } from 'next/dist/client/router';
import LanguageIcon from '#mui/icons-material/Language';
const LocaleSelect: FunctionComponent = () => {
const router = useRouter()
const {locale, locales, pathname, asPath, query} = router;
const handleLocaleChange = (event: SelectChangeEvent<string>) => {
router.push({ pathname, query }, asPath, { locale: event.target.value})
}
return(
<FormControl
variant='standard'
sx={{ m: 1, maxWidth: 32 }}
color="primary" >
<Select
disableUnderline
labelId="demo-simple-select-autowidth-label"
id="demo-simple-select-autowidth"
value={locale?.toLocaleUpperCase()}
onChange={handleLocaleChange}
autoWidth
IconComponent={LanguageIcon} >
{locales?.map((l) => {
return <MenuItem key={l} value={l}>
{l.toLocaleUpperCase()}</MenuItem>;
})}
</Select>
</FormControl>
)
}
export default LocaleSelect
This makes it look like this.
I managed to make the globe white by using
IconComponent={() => <LanguageIcon htmlColor='white'/>}
but that moves the globe to the right.
Any help would be fantastic; either by making the globe white or by moving it to the left.
You can do something like this.
<LanguageIcon
htmlColor="white"
sx={{ position: "absolute", left: ".5rem",cursor:'pointer',zIndex:-1 }}
/>
Position absolute makes it easy to align the icon where you want but the icon becomes un-clickable. To solve this I added cursor:pointer which partially solved the issue but wasn't able to click yet. Hence I reduced the z-index.
Reducing the z-index works because, clicking on icon actually clicks the parent and then displays the options.
I'm having a hard time trying to make ra-tree-ui-materialui work following this doc :
https://github.com/marmelab/react-admin/blob/master/packages/ra-tree-ui-materialui/README.md
I already have a react-admin backoffice with some fixture data, and I decided, after editing my App.js like explained in link above, to put my tree logic inside SegmentationList.js, which manages display of all Segmentations coming from an APIPlatform backend.
I commented datagrid logic already present in file, to test the tree alone. I kept all imports active and added those needed for the tree like in docs. With this code I manage to have the tree displayed, but with no hierarchy view at all, even when I set a row as child of another by editing, or when I do it via drag and drop.
When I try the latter, I actually view the child row nested inside its parent with collapse button for a few seconds only. Then it comes back to a flat tree.
I have tried to disable JSS in the file to see if style was guilty. I checked if react-dnd was installed and it is. I checked that parent field had value expected in child row by coming back to its edit page. Finally I went to backend side to check if parent field values in DB were consistent with what I saw in frontend, it was. Finally I had a look on this topic : https://github.com/marmelab/react-admin/issues/2980 since I also have the warning "Missing translation for key: "ra.tree.root_target"".
Thanks for your help.
First, here is my App.js file :
import React, {Component} from 'react';
import { Admin, Resource, mergeTranslations } from 'react-admin';
import { reducer as tree } from 'ra-tree-ui-materialui';
import englishMessage from 'ra-language-english';
import treeEnglishMessages from 'ra-tree-language-english';
import parseHydraDocumentation from '#api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';
import { hydraClient } from '#api-platform/admin';
import Locale from "./Resources/Locale/"
import Segmentation from "./Resources/Segmentation/"
import SegmentationTranslation from "./Resources/SegmentationTranslation/"
import Product from "./Resources/Product/"
import Attribute from "./Resources/Attribute/"
import ProductTranslation from "./Resources/ProductTranslation/"
import translations from './i18n';
import CustomRoute from './routes';
import themeReducer from './themeReducer';
import { Layout } from './layout';
import LocaleRetrieve from './Utils/LocaleRetrieve';
const messages = {
'en': mergeTranslations(englishMessage, treeEnglishMessages),
};
const dataProvider = api => hydraClient(api);
const httpEndpoint = process.env.REACT_APP_XXX_HTTP;
const apiDocumentationParser = httpEndpoint => parseHydraDocumentation(httpEndpoint)
.then(
({ api }) => ({api})
);
const i18nProvider = locale => {
// change of locale after initial call returns a promise
return translations[locale];
}
export default class extends Component {
state = { api: null };
componentDidMount() {
apiDocumentationParser(httpEndpoint).then(({ api }) => {
this.setState({ api });
}).catch((e) => {
console.log(e);
});
console.log(LocaleRetrieve());
};
render() {
if (null === this.state.api) return <div>Loading...</div>;
return (
<Admin api={ this.state.api }
apiDocumentationParser={ apiDocumentationParser }
dataProvider= { dataProvider(this.state.api) }
customReducers={{theme: themeReducer, tree}}
messages={translations}
locale="en"
i18nProvider={i18nProvider}
title="XXX"
customRoutes={CustomRoute}
appLayout={Layout}
>
<Resource name="locales" {...Locale} />
<Resource name="segmentations" {...Segmentation} />
<Resource name="segmentation_translations" {...SegmentationTranslation}/>
<Resource name="products" {...Product}/>
<Resource name="product_translations" {...ProductTranslation}/>
<Resource name="attributes" {...Attribute}/>
</Admin>
)
}
}
and here is my component with tree logic, located in src/Resources/Segmentation/SegmentationList.js :
import React, {Fragment} from 'react';
import {List, Datagrid, TextField, ChipField, BooleanField, ReferenceField, EditButton, ReferenceInput, AutocompleteInput, ShowButton, Filter, TextInput, Labeled, CardActions, ExportButton, RefreshButton, DeleteButton, SaveButton } from 'react-admin';
import LinkToTranslatations from './LinkToTranslations';
import AddChildButton from "./AddChildButton";
import ListChildButton from "./ListChildButton";
import PublishedButtons from './PublishedButtons';
import CreateSegmentationButtons from './CreateSegmentationButtons';
// for tree
import { withStyles } from '#material-ui/core/styles';
import { IgnoreFormProps, Tree, NodeForm, NodeActions, NodeView } from 'ra-tree-ui-materialui';
const styles = {
hash: {
marginLeft: '15px',
fontWeight: 'bold',
marginRight: '15px'
},
type: {
marginLeft: '15px',
marginRight: '15px',
textTransform: 'uppercase'
},
title: {
marginLeft: '15px',
marginRight: '15px'
}
};
// for tree
const SegmentationTreeActions = props => (
<NodeActions {...props}>
<LinkToTranslatations />
<ShowButton />
<IgnoreFormProps>
<EditButton />
<DeleteButton />
</IgnoreFormProps>
</NodeActions>
);
// tree
export const SegmentationList = withStyles(styles)(({ classes, ...props}) => (
<List {...props} perPage={10000}>
<Tree allowDropOnRoot enableDragAndDrop>
<NodeView actions={<SegmentationTreeActions />}>
<TextField source="hash" className={classes.hash} />
<TextField source="type" className={classes.type} />
<TextField source="title" className={classes.title} />
</NodeView>
</Tree>
</List>
));
export default SegmentationList;
I need a resource with all its configuration, but I don't want it to be showed in sidebar
You can omit the list prop for a Resource if you want to hide it in the sidebar menu.
<Resource name="posts" />
I found a different "hacky" way
You can add in your css the following to hide from the menu the resource
.MuiDrawer-root a[href^='#/resource-to-exclude'] {
display: none;
}
As explained in the documentation, you can provide your Menu component to the Admin component using it's menu prop. See
https://marmelab.com/react-admin/Admin.html#menu
Please note that this prop will be deprecated soon in favor of appLayout but you'll still use this custom menu in your custom layout anyway.
// in src/Menu.js
import React from 'react';
import { connect } from 'react-redux';
import { MenuItemLink, getResources } from 'react-admin';
import { withRouter } from 'react-router-dom';
import Responsive from '../layout/Responsive';
const Menu = ({ resources, onMenuClick, logout }) => (
<div>
{resources
.filter(resource => resource.name !== 'excluded-resource')
.map(resource => (
<MenuItemLink to={`/${resource.name}`} primaryText={resource.name} onClick={onMenuClick} />
))
}
<Responsive
small={logout}
medium={null} // Pass null to render nothing on larger devices
/>
</div>
);
const mapStateToProps = state => ({
// Rerieve all known resources
resources: getResources(state),
});
export default withRouter(connect(mapStateToProps)(Menu));
If your goal is to hide the entire sidebar, and make it not visible to the user, in your theme.js
try add the following code:
RaSidebar: {
drawerPaper: {
display: 'none',
},
},
eg.
const baseTheme = createTheme({
overrides: {
...<components you want override etc>...,
// React-admin
RaSidebar: {
drawerPaper: {
display: 'none',
},
},
},
});