Custom button on the leaflet map with React-leaflet version3 - reactjs

I'm a new leaflet learner with React typescript. Want to create a custom button on the map. On clicking the button a popup will appear. I saw many example but they are all based on older version and I also tried to create my own but no luck. The documentation also not providing much help. Even a functional custom control component is also very effective for my app. Any help on this will be much appreciated. Here is my code,
Custom button
import React, { Component } from "react";
import { useMap } from "react-leaflet";
import L, { LeafletMouseEvent, Map } from "leaflet";
class Description extends React.Component<{props: any}> {
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map : Map) => {
const helpDiv = L.DomUtil.create("button", ""); //how to pass here the button name and
//other property ?
//a bit clueless how to add a click event listener to this button and then
// open a popup div on the map
}
});
return new MapHelp({ position: "bottomright" });
}
componentDidMount() {
const { map } = this.props as any;
const control = this.createButtonControl();
control.addTo(map);
}
render() {
return null;
}
}
function withMap(Component : any) {
return function WrappedComponent(props : any) {
const map = useMap();
return <Component {...props} map={map} />;
};
}
export default withMap(Description);
The way I want to call it
<MapContainer
center={defaultPosition}
zoom={6}
zoomControl={false}
>
<Description />
<TileLayer
attribution="Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
/>
<ZoomControl position={'topright'}/>
</MapContainer>

You're close. Sticking with the class component, you just need to continue creating your buttons instance. You can use a prop on Description to determine what your button will say and do:
<Description
title={"My Button Title"}
markerPosition={[20.27, -157]}
description="This is a custom description!"
/>
In your decsription's createButtonControl, you're almost there. You just need to fill it out a bit:
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map) => {
const helpDiv = L.DomUtil.create("button", "");
this.helpDiv = helpDiv;
// set the inner content from the props
helpDiv.innerHTML = this.props.title;
// add the event listener that will create a marker on the map
helpDiv.addEventListener("click", () => {
console.log(map.getCenter());
const marker = L.marker()
.setLatLng(this.props.markerPosition)
.bindPopup(this.props.description)
.addTo(map);
marker.openPopup();
});
// return the button div
return helpDiv;
}
});
return new MapHelp({ position: "bottomright" });
}
Working codesandbox
There's a million ways to vary this, but hopefully that will get you going.

Related

React createProtal called outsite a JSX component not updating the DOM

I am trying to render a dynamically generated react component in a react app using createProtal.
When I call createProtal from a class the component is not rendered.
Handler.ts the class the contains the business logic
export class Handler {
private element: HTMLElement | null;
constructor(selector: string) {
this.element = document.getElementById(selector);
}
attachedEvent() {
this.element?.addEventListener("mouseenter", () => {
let cancel = setTimeout(() => {
if (this.element != null)
this.attachUi(this.element)
}, 1000)
this.element?.addEventListener('mouseleave', () => {
clearTimeout(cancel)
})
})
}
attachUi(domNode: HTMLElement) {
createPortal(createElement(
'h1',
{className: 'greeting'},
'Hello'
), domNode);
}
}
Main.tsx the react component that uses Handler.ts
const handler = new Handler("test_comp");
export default function Main() {
useEffect(() => {
// #ts-ignore
handler.useAddEventListeners();
});
return (
<>
<div id="test_comp">
<p>Detect Mouse</p>
</div>
</>
)
}
However when I repleace attachUi function with the function below it works
attachUi(domNode: HTMLElement) {
const root = createRoot(domNode);
root.render(createElement(
'h1',
{className: 'greeting'},
'Hello'
));
}
What am I missing?
React uses something called Virtual DOM. Only components that are included in that VDOM are displayed to the screen. A component returns something that React understands and includes to the VDOM.
createPortal(...) returns exactly the same as <SomeComponent ... />
So if you just do: const something = <SomeComponent /> and you don't use that variable anywhere, you can not display it. The same is with createPortal. const something = createPortal(...). Just use that variable somewhere if you want to display it. Add it to VDOM, let some of your components return it.
Your structure is
App
-children
-grand children
-children2
And your portal is somewhere else, that is not attached to that VDOM. You have to include it there, if you want to be displayed.
In your next example using root.render you create new VDOM. It is separated from your main one. This is why it is displayed

Inject Props to React Component

For security reasons, I have to update ant design in my codebase from version 3 to 4.
Previously, this is how I use the icon:
import { Icon } from 'antd';
const Demo = () => (
<div>
<Icon type="smile" />
</div>
);
Since my codebase is relatively big and every single page use Icon, I made a global function getIcon(type) that returns <Icon type={type}>, and I just have to call it whenever I need an Icon.
But starting from antd 4, we have to import Icon we want to use like this:
import { SmileOutlined } from '#ant-design/icons';
const Demo = () => (
<div>
<SmileOutlined />
</div>
);
And yes! Now my getIcon() is not working, I can't pass the type parameter directly.
I tried to import every icon I need and put them inside an object, and call them when I need them. Here's the code:
import {
QuestionCircleTwoTone,
DeleteOutlined,
EditTwoTone
} from '#ant-design/icons';
let icons = {
'notFound': <QuestionCircleTwoTone/>,
'edit': <EditTwoTone/>,
'delete': <DeleteOutlined/>,
}
export const getIcon = (
someParam: any
) => {
let icon = icons[type] !== undefined ? icons[type] : icons['notFound'];
return (
icon
);
};
My problem is: I want to put someParam to the Icon Component, how can I do that?
Or, is there any proper solution to solve my problem?
Thanks~
You can pass props as follows in the icons Object:
let icons = {
'notFound':(props:any)=> <QuestionCircleTwoTone {...props}/>,
'edit': (props:any)=><EditTwoTone {...props}/>,
'delete':(props:any)=> <DeleteOutlined {...props}/>,
}
And then if you will pass any prop to the Icon component then it will pass the prop to the specific icon component
let Icon = icons[type] !== undefined ? icons[type] : icons['notFound'];
return (<Icon someParam={'c'}/>)

react functional component with ag grid cannot call parent function via context

I am using ag-grid-react and ag-grid-community version 22.1.1. My app is written using functional components and hooks. I have a cellrenderer component that is attempting to call a handler within the parent component using the example found here. Is this a bug in ag-grid? I have been working on this application for over a year as I learn React, and this is my last major blocker so any help or a place to go to get that help would be greatly appreciated.
Cell Renderer Component
import React from 'react';
import Button from '../../button/button';
const RowButtonRenderer = props => {
const editClickHandler = (props) => {
let d = props.data;
console.log(d);
props.context.foo({d});
//props.editClicked(props);
}
const deleteClickHandler = (props) => {
props.deleteClicked(props);
}
return (<span>
<Button classname={'rowbuttons'} onClick={() => { editClickHandler(props) }} caption={'Edit'} />
<Button classname={'rowbuttons'} onClick={() => { deleteClickHandler(props) }} caption={'Delete'} />
</span>);
};
export default RowButtonRenderer;
Parent Component
function Checking() {
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: (props) => foo(props)};
const gridOptions = (params) => {
if (params.node.rowIndex % 2 === 0) {
return { background: "#ACC0C6" };
}
};
const frameworkComponents = {
rowButtonRenderer: RowButtonRenderer,
};
.
.
.
return (
<>
<AgGridReact
getRowStyle={gridOptions}
frameworkComponents={frameworkComponents}
context = {context}
columnDefs={columnDefinitions}
rowData={rowData}
headerHeight="50"
rowClass="gridFont"
></AgGridReact>
</>
);
}
When clicking the edit button on a row, the debugger says that there is a function.
This error is received though:
You are passing the context object in this code section:
const context = {componentParent: (props) => foo(props)};
...
<AgGridReact
context={context}
{...}
></AgGridReact>
And in your cell renderer you call this
props.context.foo({d});
While it should be this
props.context.componentParent({d});
Also you can assign your callback directly since it receives the same parameter and returns the same result (if any)
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: foo};
You can also use this shorthand syntax from ES6 when assigning object property
function componentParent(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent};
Live Demo

How can I reset a dragged component to its original position with react-draggable?

I try to implement a function in my app that allows the user to reset all the components that he dragged around to be reset to their original position.
I assume that this functionality exists in react-draggable because of this closed and released issue: "Allow reset of dragging position" (https://github.com/idanen/react-draggable/issues/7). However I did not find any hint in the documentation (https://www.npmjs.com/package/react-draggable).
There was one question with the same content in stackoverflow, but it has been removed (https://stackoverflow.com/questions/61593112/how-to-reset-to-default-position-react-draggable).
Thanks for your help :-)
The referenced issue on the GitHub references a commit. After taking a look at the changes made in this commit, I found a resetState callback added to the useDraggable hook. In another place in the commit, I found a change to the test file which shows usage of the hook.
function Consumer(props) {
const {
targetRef,
handleRef,
getTargetProps,
resetState,
delta,
dragging
} = useDraggable(props);
const { style = defaultStyle } = props;
return (
<main
className='container'
ref={targetRef}
data-testid='main'
style={style}
{...getTargetProps()}
>
{dragging && <span>Dragging to:</span>}
<output>
{delta.x}, {delta.y}
</output>
<button className='handle' ref={handleRef}>
handle
</button>
<button onClick={resetState}>reset</button>
</main>
);
}
The hook returns a set of callbacks, including this callback, which can be used to reset the state of the draggable.
I wanted the component to reset back to its original position when the component was dropped.
Using hooks I monitored if the component was being dragged and when it was false reset the position otherwise it would be undefined.
export default function DraggableComponent(props: any) {
const {label} = props
const [isDragging, setIsDragging] = useState<boolean>(false)
const handleStart = (event: any, info: DraggableData) => {
setIsDragging(true)
}
const handleStop = (event: any, info: DraggableData) => {
setIsDragging(false)
}
return (
<Draggable
onStart={handleStart}
onStop={handleStop}
position={!isDragging? { x: 0, y: 0 } : undefined}
>
<Item>
{label}
</Item>
</Draggable>
)
}
Simple approach would be:
creating a new component to wrap our functionality around the Draggable callbacks
reset position when onStop callback is triggered
Example:
import { useState } from 'react';
import Draggable, { DraggableData, DraggableEvent, DraggableProps } from 'react-draggable';
export function Drag({ children, onStop, ...rest }: Partial<DraggableProps>) {
const initial = { x: 0, y: 0 }
const [pos, setPos] = useState(initial)
function _onStop(e: DraggableEvent, data: DraggableData){
setPos(initial)
onStop?.(e, data)
}
return (
<Draggable position={pos} onStop={_onStop} {...rest}>
{children}
</Draggable>
)
}
Usage:
export function App() {
return (
<Drag> Drag me </Drag>
)
}
Note that this answer does not work.
None of these approaches worked for me, but tobi2424's post on issue 214 of the Draggable repo did. Here's a minimal proof-of-concept:
import React from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
// Fires when the user stops dragging the element
const choiceHandler = () => {
setDragPosition({x: 0, y: 0});
};
return (
<Draggable
onStop={choiceHandler}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;
Edit
The code above works intermittently but not particularly well. As far as I can work out, react-draggable stores data about the position of the dragged element somewhere outside of React, in order to preserve the position of the element between component refreshes. I was unable to determine how to reset the position of the element on command and none of the other example code solves the problem for me.
You can do this in a very haphazard manner. There may be another way to set state more safely on this but I didn't look too deeply into it.
import React from 'react';
export default class 😊 extends Component {
constructor(props) {
super(props);
this.draggableEntity = React.createRef();
}
resetDraggable() {
try {
this.draggableEntity.current.state.x = 0;
this.draggableEntity.current.state.y = 0;
} catch (err) {
// Fail silently
}
}
render() {
return (
<Draggable
ref={this.draggableEntity}
>
<img onClick={(e) => {this.resetDraggable()}}></img>
</Draggable>
)
}
}
There happens to be another way! You can use it's exposed ref element to reset its offset. This can be achieved like so:
import React, {useRef, useCallback} from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
const draggerRef = useRef(null);
// Fires when the user stops dragging the element
const resetDrag = useCallback(() => {
setDragPosition({x: 0, y: 0});
draggerRef.current?.setState({ x: 0, y: 0 }); // This is what resets it!
}, [setDragPosition, draggerRef]);
return (
<Draggable
ref={draggerRef}
onStop={resetDrag}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;

close popup react-leaflet after user click on button in popup

So basically want to make custom close for react-leaflet Popup component, seams that is not a big problem to do with native API leaflet but with react component from react-leaflet I can't find the solution.
at the moment, the only way I found to close the popup is the following:
constructor(props){
super(props);
this.popup = React.createRef();
}
// the magic
closePopusOnClick(){
this.popup.current.leafletElement.options.leaflet.map.closePopup();
}
render(){
return <Marker position={[this.props.lat, this.props.lng]}>
<Popup ref={this.popup}>
<Button onClick={this.closePopusOnClick}>Close popup</Button>
</Popup>
</Marker>;
}
Hope it helps!
In "react-leaflet": "^3.0.2" I managed to close the popup with:
popupRef.current._closeButton.click()
Not very nice comparing to a future Popup.close() method which MUST work out-of-box, but gets the job done...
I ended up with a similar solution to Luca's Answer, so I thought I'd add it as an answer too. I needed to close all popups when moving or zooming the map and ended up with the following:
import React, { useRef } from "react";
import { Map } from "react-leaflet"
export default () => {
const mapRef = useRef(null);
const closePopups = () => {
mapRef.current.leafletElement.closePopup();
};
const handleOnDragend = e => {
closePopups();
};
const handleOnZoomend = e => {
closePopups();
};
if (typeof window === 'undefined') {
return null;
}
return (
<Map
ref={mapRef}
onDragend={handleOnDragend}
onZoomend={handleOnZoomend}
>
</Map>
)
}
This can, however, be extended so that anything can call the closePopups method.
I found the working solution for react-leaflet v3 by modifying these two links codesandbox https://codesandbox.io/s/4ws0i and https://stackoverflow.com/a/67750291/8339172
here is the function to hide the Popup component
const hideElement = () => {
if (!popupElRef.current || !map) return;
map.closePopup();
};
here is the Popup component
<Popup ref={popupElRef} closeButton={false}>
<button onClick={hideElement}>Close popup</button>
</Popup>

Resources