Issue using eventHandlers with react-leaflet and next.js - reactjs

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!

Related

extracting values from html forms rendered in react-leaflet popup

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

OBJ Model not loading in react three fiber

I'm trying to add an OBJ Model (armchair.obj) but it's not being loaded. I'm using React three fiber library.
Here's my codesandbox: CodeSandbox
There's no problem with the model because I tried to load it using some other website and it is being loaded.
Anyway, I tried uploading another model (spongebob.obj) but it's not being really visible
However, in the other website, it's visible:
So, here's my codesandbox link
But, if you prefer the code here:
My App.js component:
import React, { Suspense } from "react";
import { Canvas } from "#react-three/fiber";
import { OrbitControls } from "#react-three/drei";
import LoadModel from "./components/LoadModel";
import Loader from "./components/Loader";
const App = () => {
return (
<main className="main-area">
<div id="canvas-container">
<Canvas pixelratio={[1, 2]} camera={{ position: [15, 15, 15], fov: 50, scale: [2,2,2] }}>
<ambientLight intensity={1} />
<Suspense fallback={<Loader />}>
<LoadModel url="./spongebob.obj" />
</Suspense>
<OrbitControls />
</Canvas>
</div>
</main>
);
};
export default App;
My LoadModel.js component:
import React, { useMemo, useState } from "react";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
const LoadModel = ({ url }) => {
const [obj, set] = useState();
// useMemo(() => new OBJLoader().load(url, set), [url])
useMemo(() => new OBJLoader().load(url, set), [url]);
//useMemo(() => new GLTFLoader().load(url, set), [url])
return obj ? <primitive object={obj} dispose={null} /> : null;
};
export default LoadModel;
You can improve the rendering of the SpongeBob model by adding a directional light to your scene. A single ambient light is not sufficient for proper illumination. Try adding the following line to your codesandbox:
<directionalLight />
The chair model has unfortunately some design issues. It has an extreme scale and is highly translated. I suggest you scale the model down and then center it after the loading process. However, it would be better to fix the model in a DCC tool like Blender and model the chair according to real world units.

Correct use of ReactToPrint?

The problem is that the button that is supposed to give the option to print is not working anymore.
the error in the console says:
To print a functional component ensure it is wrapped with `React.forwardRef`, and ensure the forwarded ref is used. See the README for an example: https://github.com/gregnb/react-to-print#examples
I Have already seen some solutions specifically talking about the same problem but I have not been able to make it work.
any suggestion?
this is the library i'm using: ReactToPrint npm
React To print
import { useRef } from "react";
import { useReactToPrint } from "react-to-print";
import Resume from "./Pdf/Pdf";
const Example = () => {
const componentRef = useRef();
const handlePrint = useReactToPrint({
content: () => componentRef.current
});
return (
<div >
<button onClick={handlePrint}> ------> NOT WORKING!
Descargar Pdf
</button>
<Resume ref={componentRef} /> ------> COMPONENT TO PRINT
</div>
);
};
export default Example;
Component to be printed
import React from "react";
import styled from 'styled-components';
import PdfSection from './PdfSection';
import AlienLevel from './AlienLevel';
import {connect } from 'react-redux';
class Resume extends React.Component {
renderList = () => {
return this.props.posts.diagnose.map((post) => {
return (
<PdfSection
key={post.id}
id={post.id}
divider={"/images/pdf/divider.png"}
img={"/images/alienRandom.png"}
title={post.title}
// data={post.data}
text={post.text0}
subtext={post.subtext0}
/>
);
});
};
render(){
return (
<div>
<Container>
<Page>
<Portada>
<img id="portada" src="/images/pdf/PortadaPdf.png" />
</Portada>
</Page>
<Page>
<AlienLevel
result= "{props.diagn}{"
character={"/images/pdf/alienMedio.png"}
fileName={"responseBody[4].data"}
level={"/images/pdf/level6.png"}
correct={"/images/pdf/correct.png"}
medium={"/images/pdf/medium.png"}
incorrect={"/images/pdf/incorrect.png"}
text='"Necesitas mejorar tus prácticas intergalácticas de CV, pero ya eres nivel medio!"'
/>
<div>{this.renderList()}</div>
</Page>
</Container>
</div>
);
};
}
const mapStateToProps = (state) => {
return { posts: state.posts };
};
export default connect(mapStateToProps)( Resume);
thanks in advance!
The problem is with connect() function of react-redux.
You wrapped your component in connect and connect by default does not forward ref. Which means, the ref you are passing here <Resume ref={componentRef} /> does not reach to your component.
You need to pass options { forwardRef: true } in fourth parameter of connect function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?).
Just change this code export default connect(mapStateToProps)(Resume); in Resume component to this
export default connect(mapStateToProps, null, null, { forwardRef: true })(Resume);
For anyone that is struggling with the same error, it seems that they found the proper way to resolve this, I actually resolved it by following the Codesandbox I found in the Github issues here si the link. hope is useful! -->
LINK TO GITHUB SPECIFIC ISSUE (SOLVED!!)
I had the same issue and I am happy to share my findings as soon as now.
The component has to be rendered somewhere using ref.
I added it to my page as hidden using React Material UI's Backdrop. Or u can hide it using hooks like examples below.
Using backdrop and only calling it when I need to preview the print. 👇👇
<Backdrop sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={openBD}>
<ComponentToPrint ref={componentRef} />
</Backdrop>
Using Hooks plus display styling to only display it when needed. 👇👇
const [isReady, setIsReady] = useState("none");
<Paper style={{ display: isReady }} >
<ComponentToPrint ref={componentRef} />
</Paper>
<Button
variant="contained"
endIcon={<BackupTableRoundedIcon />}
onClick={() => setIsReady("")}
>
Start Printing
</Button>
Note: I used MUI components, if u decide to copy paste, then change Button to html <button and paper to <div. Hope this helps.

How to move the camera using react-three-fiber and #react-three/drei?

I'm drawing a 3d model with the following configuration.
react-three-fiber
#react-three/drei
The following is a CodeSandBox with a model drawn.
CodeSandBox-A
The drawn model is close to the top of the canvas, and I want to center it.
Could you please tell me how to center the model on the canvas?
This is the first time I've touched Three.js, so I'm sure I may have made some elementary mistakes.
The code is as follows.
I referred to the following document for the props of the Canvas component.
React Three Fiber Documentation
import { useMemo, Suspense } from "react";
import "./style.css";
import { Canvas } from "#react-three/fiber";
const { useGLTF, OrbitControls, Stage } = require("#react-three/drei");
const CanvasContainer = () => {
const { scene } = useGLTF("/scene.glb");
return (
<Canvas camera={{ fov: 70, position: [0, 1000, 1000] }}>
// No matter what I put in this "position" value, I could not see any change in the canvas.
<OrbitControls enablePan={true} enableZoom={true} enableRotate={true} />
<Stage>
<group dispose={null}>
<primitive scale={[1, 1, 1]} object={scene} />
</group>
</Stage>
</Canvas>
);
};
const App = () => {
const isWindow = typeof window === "undefined";
const memoCanvas = useMemo(() => <CanvasContainer />, []);
return (
<>
<h1>Canvas</h1>
<div className="canvas_container">
{!isWindow && <Suspense fallback={<div />}>{memoCanvas}</Suspense>}
</div>
</>
);
};
export default App;
setDefaultCamera removed in v6.x. · Issue #1147 · pmndrs/react-three-fiber
I also tried the code to use useThree as described in the above Issue.
The following is its CodeSandBox.
CodeSandBox-B
As you can see from the CodeSandBox, I was unable to move the camera.
give orbitcontrols the "makeDefault" prop, then it should in the center. otherwise check out dreis Bounds component. see: https://twitter.com/0xca0a/status/1453807293074661385
the memocanvas thing btw makes no sense, don't put jsx into usememo.

Swiper React Mouse-wheel Scrolling and Keyboard Control Not Working

Using the mouse-wheel or keyboard to control the sliding in SwiperJs for React doesn't work. I can't seem to find anything on it and following Swiper API docs doesn't help either.
Using
react: 17.0.1
swiper: 6.4.11
There's a sandbox setup of it here
const App = () => {
const slides = [1, 2, 3, 4];
return (
<Swiper
slidesPerView={2}
keyboard={{ enabled: true }}
direction="vertical"
mousewheel
>
{slides.map((slide) => (
<SwiperSlide key={slide}>
<h1>Slide</h1>
</SwiperSlide>
))}
</Swiper>
);
};
Please get SwiperCore module to do below:
import SwiperCore, { Keyboard } from 'swiper';
and add following line of code:
SwiperCore.use([Keyboard]);
This should have the keyboard working.
This works for me perfectly.
import SwiperCore, { Keyboard, Mousewheel } from "swiper/core";
SwiperCore.use([Keyboard, Mousewheel]);
const App = () => {
const slides = [1, 2, 3, 4];
return (
<Swiper
slidesPerView={2}
keyboard={true}
direction="vertical"
mousewheel={true}
>
{slides.map((slide) => (
<SwiperSlide key={slide}>
<h1>Slide</h1>
</SwiperSlide>
))}
</Swiper>
);
};
Not a proper solution but for anyone interested I have come up with a work around.
I was unable to get the mouse-wheel or keyboard controls to work for Swiper using React components or with pure javascript. Official Swiper examples didn't work when replicated so I tried some previous versions. Went all the way back to 5.4.5 to get it work.
Unfortunately 5.4.5 doesn't support React but Swiper can still be incorporated into a React setup.
// import Swiper JS
import Swiper from "swiper/js/swiper";
// import Swiper styles
import "swiper/css/swiper.css";
const SimpleSwiper = () => {
useEffect(() => {
const swiper = new Swiper(".swiper-container", {
direction: "vertical",
mousewheel: true,
keyboard: { enabled: true },
slidesPerView: 1,
});
}, []);
const slides = [1, 2, 3];
return (
<div className="swiper-container">
<div className="swiper-wrapper">
{slides.map((slide) => (
<div
className="swiper-slide"
key={slide}
>{`slide ${slide}`}</div>
))}
</div>
</div>
);
};
You can view the code sandbox here. The keyboard control doesn't seem to work here but it does work locally.

Resources