How to use react-leaflet v3 to create class components? - react-leaflet

React-leaflet package allows to use leaflet and react using functional components, at least all the examples at https://react-leaflet.js.org/ are using functional components. Is possible to use class components with version 3 of react-leaflet? If yes, there are examples?

It is supported to create a class components in react-leaflet v3 although not the same way as it was officially supported in previous versions.
In version v1 and v2 you were suppose to implement a custom component by:
a) extending one of the abstract classes provided by react-leaflet, for example:
class MapInfo extends MapControl {
//...
}
b) and implementing createLeafletElement (props: Object): Object method
Refer this answer for a more details since official documentation for implementing custom components for v1 and v2 versions is no longer available.
The following example demonstrates how to convert custom component into version 3.
Here is an a custom component compatible with v1/v2 version:
import React, { Component } from "react";
import { withLeaflet, MapControl } from "react-leaflet";
import L from "leaflet";
class MapInfo extends MapControl {
constructor(props, context) {
super(props);
props.leaflet.map.addEventListener("mousemove", ev => {
this.panelDiv.innerHTML = `<h2><span>Lat: ${ev.latlng.lat.toFixed(
4
)}</span> <span>Lng: ${ev.latlng.lng.toFixed(4)}</span></h2>`;
console.log(this.panelDiv.innerHTML);
});
}
createLeafletElement(opts) {
const MapInfo = L.Control.extend({
onAdd: map => {
this.panelDiv = L.DomUtil.create("div", "info");
return this.panelDiv;
}
});
return new MapInfo({ position: "bottomleft" });
}
componentDidMount() {
const { map } = this.props.leaflet;
this.leafletElement.addTo(map);
}
}
export default withLeaflet(MapInfo);
which could be converted into the following class component compatible with version 3:
class MapInfo extends React.Component {
createControl() {
const MapInfo = L.Control.extend({
onAdd: (map) => {
const panelDiv = L.DomUtil.create("div", "info");
map.addEventListener("mousemove", (ev) => {
panelDiv.innerHTML = `<h2><span>Lat: ${ev.latlng.lat.toFixed(4)}</span> <span>Lng: ${ev.latlng.lng.toFixed(4)}</span></h2>`;
console.log(panelDiv.innerHTML);
});
return panelDiv;
},
});
return new MapInfo({ position: "bottomleft" });
}
componentDidMount() {
const {map} = this.props;
const control = this.createControl();
control.addTo(map);
}
render() {
return null;
}
}
function withMap(Component) {
return function WrappedComponent(props) {
const map = useMap();
return <Component {...props} map={map} />;
};
}
export default withMap(MapInfo);
Note: withMap is a high order component which is intended to pass map instance into class component
Here is a demo

Related

reference error when I try to get a value from other slice redux toolkit?

I am trying to import a value form other slice that has some user information, any idea why I am getting this nasty error ? I read it is normal to request data from other slices, the error seem to be like the slice cannot find the store... below is my code structure, my store is at the top of my app, does this getState function works in a component only and not in slice to other slice .
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import App from './App';
import './index.css';
// Redux Tool Kit
import { store } from './app/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
import {
RootState,
store
} from './store';
import {
createSlice,
PayloadAction
} from '#reduxjs/toolkit';
export interface miscState {
dayNumber: true,
dayOfWeek: false,
};
export const miscSlice = createSlice({
name: 'misc',
initialState,
reducers: {
setDisplayDay: (state, action: PayloadAction < {
bool: boolean;type: string
} > ) => {
const {
user,
uid
} = store.getState().global.currentUser;
const setDisplay = async() => {
const docRef = doc(db, colDynamic(user)[0], uid);
await updateDoc(docRef, {
[action.payload.type]: action.payload.bool,
});
};
},
},
});
// Values
export const MiscCurrentState = (state: RootState) => state.misc;
// Action creators are generated for each case reducer function
export const {
setDisplayDay
} = miscSlice.actions;
export default miscSlice.reducer;
import { configureStore } from '#reduxjs/toolkit';
// Global
import globalReducer from './globalSlice';
// Misc
import miscReducer from './miscSlice';
export const store = configureStore({
reducer: {
global: globalReducer,
misc: miscReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
In Redux, you are not allowed to access the store from within a reducer. A reducer has to be a pure function that only reads the variables passed into it - so all information you have is the slice's own state and the action being dispatched. You are not allowed to read a global variable, have any kind of side effect or read from the global Redux store to get the data of another slice.

How to remove the surround IIFE from #babel/template?

I'm trying to convert some Ember codes to React. Here is what I want to transform.
From
export default Ember.Component.extend(({
didInsertElement() { }
});
To
export default class MyComponent extends React.Component {
componentDidMount() { }
}
I write a babel plugin, and try to replace the Ember.Component.extend call with an AST node which is produced by template method. Here is the code snippet.
babel-plugin
const { default: template } = require("#babel/template");
const code = `class TestComponent extends React.Component {
componentDidMount() { }
}`;
let classDeclarationNode = template.ast(code);
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression(path) {
if (!path.getSource().startsWith("Ember.Component.extend")) {
return;
}
path.replaceWith(classDeclarationNode);
}
}
};
};
Output
export default (function () {
class TestComponent extends React.Component {
componentDidMount() {}
}
})();
Instead of the expected code I wrote above, I get the ClassDeclaration statement surrounded with IIFE. Is there any way to remove the IIFE?
I have struggled with the problem one whole day, but have no way to resolve it.
BTW, I also tried parseExpression method, but still can't get what exactly I want.
babel-plugin
const { parseExpression } = require('#babel/parser');
const code = `class TestComponent extends React.Component {
componentDidMount() { }
}`;
let expression = parseExpression(code);
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression(path) {
if (!path.getSource().startsWith("Ember.Component.extend")) {
return;
}
path.replaceWith(expression);
}
}
};
};
Output
export default (class TestComponent extends React.Component {
componentDidMount() {}
});
It's quite close to the correct code, except an extra pair of (). Is there any way to generate the pure class declaration?
Thanks for the help of loganfsmyth on Slack, the problem was finally resolved. I should replace the node of whole export default but not only the CallExpression. Here is the code.
babel-plugin
const { default: template } = require("#babel/template");
const code = `export default class TestComponent extends React.Component {
componentDidMount() { }
}`;
let rootNode = template.ast(code);
module.exports = function ({ types: t }) {
return {
visitor: {
ExportDefaultDeclaration(path) {
let isMatchedNode = (path.node.declaration &&
t.matchesPattern(path.node.declaration.callee, "Ember.Component.extend"));
if (!isMatchedNode) {
return;
}
path.replaceWith(rootNode);
}
}
};
};
Output
export default class TestComponent extends React.Component {
componentDidMount() {}
}

How to provide plugin for all forms in redux-form?

Ok, so I wanted to deal with whitespaces at the beginning of the input in my register form and I have achieved this by providing plugin for redux-form reducer:
export default function(cookies, server) {
return combineReducers({
auth: auth(cookies, server),
reduxAsyncConnect,
alert,
programs,
exercises,
routing: routerReducer,
form: form.plugin(formPlugin),
profile,
spinner,
companies
});
}
The plugin is:
import {ltrim} from './ltrim';
const formPlugin = {
registerForm: (state, action) => {
switch(action.type) {
case '##redux-form/CHANGE':
if (action.meta.form === 'registerForm') {
return {
...state,
values: {
...state.values,
[action.meta.field]: ltrim(action.payload)
}
}
} else {
return state;
}
default:
return state;
}
}
}
export default formPlugin;
How can I get the same effect on all forms withouth hardcoding? Maybe I have to somehow edit redux-form CHANGE action to achieve this?
You could make a custom Field component that does this for you.
import { Field } from 'redux-form';
import {ltrim} from './ltrim';
const normalize = value => ltrim(value);
const TrimmedField = props => <Field {...props} normalize={normalize} />;
You could also give your custom normalize a callback argument if you need to do other normalization in some cases.
http://redux-form.com/6.6.3/docs/api/Field.md/#-normalize-value-previousvalue-allvalues-previousallvalues-nextvalue-optional-

React DnD - "Cannot have two HTML5 backends at the same time."

I am trying to make a POC with Rails5, action Cable, React and Rails and React DnD.
The purpose is to make an app like trello but for an recruitment process.
My front is in ReactJS.
I have 3 components, first, the container call "Candidates", this component call 2 "CardBoard" components that call "Card" component.
I user react DnD library for draggable card and droppable CardBoard. when i drop card on cardboard, i use a post call and a websocket(action cable from rails5) for update my state. I don't understand why i have this message after the post call :
Uncaught Error: Cannot have two HTML5 backends at the same time.
at HTML5Backend.setup (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4175), <anonymous>:87:15)
at DragDropManager.handleRefCountChange (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3566), <anonymous>:52:22)
at Object.dispatch (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4931), <anonymous>:186:19)
at HandlerRegistry.addSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:3594), <anonymous>:104:18)
at registerSource (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4294), <anonymous>:9:27)
at DragDropContainer.receiveType (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:146:32)
at DragDropContainer.receiveProps (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:135:14)
at new DragDropContainer (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:1793), <anonymous>:102:13)
at eval (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:295:18)
at measureLifeCyclePerf (eval at <anonymous> (webpack-bundle.self-7b1a342….js?body=1:4399), <anonymous>:75:12)
Candidate.jsx =
import React, { PropTypes } from 'react';
import { DragDropContextProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import CardBoard from './CardBoard.jsx';
export default class Candidates extends React.Component {
constructor(props, _railsContext) {
super(props);
this.state = {
candidates: this.props.candidates
}
this.filterByStatus = this.filterByStatus.bind(this)
}
componentDidMount() {
this.setupSubscription();
}
setupSubscription() {
App.candidate = App.cable.subscriptions.create("CandidateChannel", {
connected: () => {
console.log("User connected !")
},
received: (data) => {
this.setState({ candidates: data.candidates })
},
});
}
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
<div className="recruitment">
{
["À Rencontrer", "Entretien"].map((status, index) => {
return (
<CardBoard candidates={(this.filterByStatus({status}))} status={status} key={index} />
);
})
}
</div>
</DragDropContextProvider>
);
}
}
CardBoard.jsx =
import React, { PropTypes } from 'react';
import Card from './Card.jsx';
import { DropTarget } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardTarget = {
drop(props: Props) {
var status = ''
if(props.status == "À Rencontrer") {
status = 'to_book'
} else {
status = 'interview'
}
return { status: status };
},
};
#DropTarget(ItemTypes.CARD, cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}))
export default class CardBoard extends React.Component<Props> {
constructor(props, _railsContext) {
super(props);
}
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
return connectDropTarget(
<div className={`${this.props.status} ui cards`}>
<h2>{`${this.props.status} (${this.props.candidates.length})`}</h2>
{
(this.props.candidates).map((candidate, index) => {
return <Card candidate={candidate} key={index} />
})
}
{ isActive?
'Release to drop' : 'drag a card here'
}
</div>
);
}
}
Card.jsx=
import React, { PropTypes } from 'react';
import { DragSource } from 'react-dnd';
import ItemTypes from './ItemTypes';
const cardSource = {
beginDrag(props) {
return {
candidate_id: props.candidate.id,
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if (dropResult) {
console.log(`You dropped ${item.candidate_id} vers ${dropResult.status} !`);
$.post(`/update_status/${item.candidate_id}/${dropResult.status}`);
}
},
};
#DragSource(ItemTypes.CARD, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Card extends React.Component {
constructor(props, _railsContext) {
super(props);
}
render() {
const { isDragging, connectDragSource } = this.props;
const { name } = this.props;
const opacity = isDragging ? 0 : 1;
var candidate = this.props.candidate;
return (
connectDragSource(
<div className="card" key={candidate.id} style={{opacity}}>
<div className="content">
<div className="header">{`${candidate.first_name} ${candidate.last_name}`}</div>
<div className="description">
{candidate.job_title}
</div>
<span className="right floated">
<i className="heart outline like icon"></i>
{candidate.average_rate}
</span>
</div>
</div>
)
);
}
}
for better comprehension, here a gif of my feature and his bug :
feature with bug
here my github repo with all code
Just try to make the context of HTML5Backend a singleton. You can check the codes at:
https://github.com/react-dnd/react-dnd/issues/186
https://github.com/react-dnd/react-dnd/issues/708
I faced with similar issue and found workaround by moving
<DragDropContextProvider></DragDropContextProvider>
to the topmost react component.
Structure before:
App
Component1
Component2
...
ComponentX with render(<DragDropContextProvider>...<DragDropContextProvider>)
Structure after:
App with render(<DragDropContextProvider>...<DragDropContextProvider>)
Component1
Component2
...
ComponentX
Supposing App is loaded only once and HTML5 backend is not replicated.
You can either create a new file and import DragDropContext where you need it:
import HTML5 from 'react-dnd-html5-backend';
import {DragDropContext} from 'react-dnd';
export default DragDropContext(HTML5);
Source: https://github.com/react-dnd/react-dnd/issues/708#issuecomment-309259695
Or using Hooks:
import { DndProvider, createDndContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import React, { useRef } from "react";
const RNDContext = createDndContext(HTML5Backend);
function useDNDProviderElement(props) {
const manager = useRef(RNDContext);
if (!props.children) return null;
return <DndProvider manager={manager.current.dragDropManager}>{props.children}</DndProvider>;
}
export default function DragAndDrop(props) {
const DNDElement = useDNDProviderElement(props);
return <React.Fragment>{DNDElement}</React.Fragment>;
}
Use:
import DragAndDrop from "../some/path/DragAndDrop";
export default function MyComp(props){
return <DragAndDrop>....<DragAndDrop/>
}
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-561631584
If you use BrowserRouter then move the DragDropContextProvider out of BrowserRouter:
Source: https://github.com/react-dnd/react-dnd/issues/186#issuecomment-453990887
DndProvider has a options prop in where you can set rootElement which bounds DnD to that specified context, and unfortunately it isn't documented well. This approach solved all my issues, as I had other component which was using DnD and they were out of my boundary and I wasn't able to make HTML5Backend Singleton.
I tried this approach with "react-dnd": "^14.0.2"
const myFirstId = 'first-DnD-Containier';
const mySecondId = 'second-DnD-Containier';
export const DndWrapper = React.memo((props) => {
const [context, setContext] = useState(null);
useEffect(() => {
setContext(document.getElementById(props.id))
},[props.id])
return context ? (
<DndProvider backend={HTML5Backend} options={{ rootElement: context}}>
{props.children}
</DndProvider>
) : null;
});
export default function App() {
return (
<div>
<div id={myFirstId}>
<DndWrapper id={myFirstId}>
<MyOtherComponents />
</DndWrapper>
</div>
<div id={mySecondId}>
<DndWrapper id={mySecondId}>
<MyOtherComponents />
</DndWrapper>
</div>
</div>
);
}
P.S. Make sure by the time you call document.getElementById the DOM exist with ids.
Move the DragDropContextProvider out of BrowserRouter.
import { BrowserRouter as Router } from 'react-router-dom';
import { DndProvider } from 'react-dnd';
<DndProvider>
<Router>
<App />
</Router>
</DndProvider>

How to use node-simple-schema reactively?

Given that there is not much examples about this, I am following the docs as best as I can, but the validation is not reactive.
I declare a schema :
import { Tracker } from 'meteor/tracker';
import SimpleSchema from 'simpl-schema';
export const modelSchema = new SimpleSchema({
foo: {
type: String,
custom() {
setTimeout(() => {
this.addValidationErrors([{ name: 'foo', type: 'notUnique' }]);
}, 100); // simulate async
return false;
}
}
}, {
tracker: Tracker
});
then I use this schema in my component :
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.newContext();
this.state = {
isValid: this.validation.isValid()
};
}
...
render() {
...
const errors = this.validation._validationErrors;
return (
...
)
}
}
So, whenever I try to validate foo, the asynchronous' custom function is called, and the proper addValidationErrors function is called, but the component is never re-rendered when this.validation.isValid() is supposed to be false.
What am I missing?
There are actually two errors in your code. Firstly this.addValidationErrors cannot be used asynchronously inside custom validation, as it does not refer to the correct validation context. Secondly, TrackerReact only registers reactive data sources (such as .isValid) inside the render function, so it's not sufficient to only access _validationErrors in it. Thus to get it working you need to use a named validation context, and call isValid in the render function (or some other function called by it) like this:
in the validation
custom() {
setTimeout(() => {
modelSchema.namedContext().addValidationErrors([
{ name: 'foo', type: 'notUnique' }
]);
}, 100);
}
the component
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.namedContext();
}
render() {
let errors = [];
if (!this.validation.isValid()) {
errors = this.validation._validationErrors;
}
return (
...
)
}
}
See more about asynchronous validation here.