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='© <a href="http://osm.org/copyright">OpenStreetMap</a> 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='© <a href="http://osm.org/copyright">OpenStreetMap</a> 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