Related
I was following the documentation to implement google map on demand rides and deliveries solution (ODRD) here.
And my Map component in React:
const MapComponent = ({ styles }) => {
const ref = useRef(null);
const tripId = useRef<string>('');
const locationProvider =
useRef<google.maps.journeySharing.FleetEngineTripLocationProvider>();
const [error, setError] = useState<string | undefined>();
const mapOptions = useRef<MapOptionsModel>({
showAnticipatedRoutePolyline: true,
showTakenRoutePolyline: true,
destinationMarker: ICON_OPTIONS.USE_DEFAULT,
vehicleMarker: ICON_OPTIONS.USE_DEFAULT,
});
const [trip, setTrip] = useState<TripModel>({
status: null,
dropOff: null,
waypoints: null,
});
const setTripId = (newTripId: string) => {
tripId.current = newTripId;
if (locationProvider.current) locationProvider.current.tripId = newTripId;
};
const setMapOptions = (newMapOptions: MapOptionsModel) => {
mapOptions.current.showAnticipatedRoutePolyline =
newMapOptions.showAnticipatedRoutePolyline;
mapOptions.current.showTakenRoutePolyline =
newMapOptions.showTakenRoutePolyline;
mapOptions.current.destinationMarker = newMapOptions.destinationMarker;
mapOptions.current.vehicleMarker = newMapOptions.vehicleMarker;
setTripId(tripId.current);
};
const authTokenFetcher = async () => {
const response = await fetch(
`${PROVIDER_URL}/token/consumer/${tripId.current}`
);
const responseJson = await response.json();
return {
token: responseJson.jwt,
expiresInSeconds: 3300,
};
};
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
}, []);
return (
<div style={styles.map} ref={ref} />
);
};
And my App component like this:
import React from 'react';
import { Wrapper, Status } from '#googlemaps/react-wrapper';
import MapComponent from './src/components/MapComponent';
import { API_KEY } from './src/utils/consts';
const render = (status: Status) => <Text>{status}</Text>;
const App = () => {
return (
<Wrapper
apiKey={API_KEY}
render={render}
version={'beta'}
// #ts-ignore
libraries={['journeySharing']}
>
<MapComponent />
</Wrapper>
);
};
Everything will works fine but I do not know how to destroy the map when component unmount in React. That's why my App always call API update the trip info.
I was tried to use clean up function in useEffect:
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
const updateEvent = locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
return () => {
mapView.map = null // or mapView.map.setmap(null);
google.maps.event.removeListener(updateEvent);
};
}, []);
But it was not working. Hope anyone can help me find out this. Thanks
I want to test my component Actions when I pass actions to children. In a nutshell, every source like twitter or facebook has its own set of actions. I'd like to check that it is called or not using spy.
This is my Actions component
const targetMetric = 'account dropdown'
const availableActions = {
addQuery: {
facebook: '^facebook://page/',
twitter: true
},
exclude: {
blog: [
'^blog://user/eventregistry/',
'^eventregistry://user/'
],
news: [
'^news://user/eventregistry/',
'^eventregistry://user/'
],
twitter: true,
youtube: true
},
reportAsNews: {
youtube: true,
mastodon: true,
twitter: true
}
}
const requiredHandlers = {
exclude: [
'onExcludeProfile'
],
reportAsNews: [
'onReportAsNews'
]
}
class Actions extends React.PureComponent {
get actions() {
const { account, handlers } = this.props
const actions = {};
Object.keys(availableActions).forEach(key =>
actions[ key ] = false
)
Object.keys(actions).forEach(key => {
const value = (
!!account.uri
&&
availableActions[ key ][ account.type ]
)
if (!value) {
return
}
if (typeof value === 'boolean') {
actions[ key ] = value
return
}
if (typeof value === 'string') {
const re = new RegExp(value, 'i')
actions[ key ] = re.test(account.uri)
return
}
if (
typeof value === 'object'
&&
Array.isArray(value)
) {
actions[ key ] = value.some(v => {
const re = new RegExp(v, 'i')
return re.test(account.uri)
})
}
})
Object.keys(actions).forEach(key => {
if (!actions[ key ] || !requiredHandlers[ key ]) {
return
}
actions[ key ] = requiredHandlers[ key ].some(i => handlers[ i ])
})
if (actions.addQuery) {
actions.addQuery = Object
.keys(this.addQueryActions)
.some(key => this.addQueryActions[ key ])
}
return actions
}
get addQueryActions() {
const { availableLanguages = [], userIsAdmin } = this.context
const { caseItem, handlers } = this.props
const actions = {
addQueryToFilter: !caseItem.isPaused && !!handlers.onAddQuery,
addQueryToAccountList: userIsAdmin && !!handlers.onAddToAccountList
}
actions.addQueryToSearch = actions.addQueryToFilter && !!availableLanguages.length
return actions
}
get actor() {
return pick(
this.props.account,
[ 'uri', 'name', 'link' ]
)
}
onExclude = () => {
const { account, handlers, isCaseLocked } = this.props
if (isCaseLocked) {
return
}
handlers.onExcludeProfile(account)
}
onReportAsNews = () => this.props.handlers.onReportAsNews(this.actor)
onAddToAccountList = () => {
const { account, from, handlers } = this.props
handlers.onAddToAccountList(account, from)
}
onAddToQuery = type => ({ language } = {}) => {
const { account, caseItem, handlers } = this.props
const { id } = caseItem
const metrics = {
index: getId(),
language,
type
}
handlers.onAddQuery({
...metrics,
expression: this.expressionFromAccount(account),
hideSearch: true,
id,
})
return metrics
}
expressionFromAccount = account => ({
and: [
{ account }
]
})
trackExcludeEvent = () => {
const { account } = this.props
this.trackEvent(
events.excludeAccounts,
{
accountsAdded: 1,
source: account.type
}
)
}
trackCreateNewQueryEvent = ({ index, language, type }) => {
const eventNameMap = {
filters: events.createNewFilter,
queries: events.createNewSearch,
}
const metrics = {
queryId: index,
target: targetMetric
}
if (type === 'queries') {
metrics[ 'language' ] = language.toLowerCase()
}
this.trackEvent(
eventNameMap[ type ],
metrics
)
}
trackReportAsNewsEvent = () => (
this.trackEvent(
events.reportAsNews,
{ source: this.props.account.type }
)
)
trackEvent = (eventName, props = {}) => {
const { from, message } = this.props
eventTracker.track(
eventName,
{
...props,
from,
messageUri: message.uri
}
)
}
getLangMenuActions = ({ handlers, isCaseLocked }, { availableLanguages }) => {
if (
isCaseLocked
||
!availableLanguages
||
!handlers.onAddQuery
) {
return []
}
const onClick = compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('queries')
)
return availableLanguages.map(({ label, value: language }) => ({
handler: onClick.bind(this, { language }),
id: `add-account-to-search-lang-${language}`,
label
}))
}
getActions = () => {
const { isCaseLocked } = this.props
const { userIsAdmin } = this.context
const actions = []
if (this.actions.addQuery) {
const addQueryAction = {
id: 'add-account-to',
isInactive: isCaseLocked && !userIsAdmin,
label: i18n.t('SOURCES.DROPDOWN_ADD_TO'),
children: []
}
if (this.addQueryActions.addQueryToSearch) {
addQueryAction.children.push({
id: 'add-account-to-search',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_SEARCH'),
children: this.getLangMenuActions(this.props, this.context)
})
}
if (this.addQueryActions.addQueryToFilter) {
addQueryAction.children.push({
handler: compose(
this.trackCreateNewQueryEvent,
this.onAddToQuery('filters')
),
id: 'add-account-to-filter',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_NEW_FILTER')
})
}
if (this.addQueryActions.addQueryToAccountList) {
addQueryAction.children.push({
handler: this.onAddToAccountList,
id: 'add-account-to-account-list',
label: i18n.t('SOURCES.DROPDOWN_ACCOUNT_LIST')
})
}
actions.push(addQueryAction)
}
if (this.actions.reportAsNews) {
actions.push({
handler: compose(
this.onReportAsNews,
this.trackReportAsNewsEvent
),
id: 'report-as-news',
label: i18n.t('SOURCES.DROPDOWN_REPORT_AS_NEWS')
})
}
if (this.actions.exclude) {
actions.push({
handler: compose(
this.onExclude,
this.trackExcludeEvent
),
id: 'exclude-account',
isInactive: isCaseLocked,
label: i18n.t('SOURCES.DROPDOWN_EXCLUDE')
})
}
console.log(actions)
return actions
}
render() {
return this.props.children({
actions: this.getActions()
})
}
}
This is my test file
import expect from 'expect'
const injectActions = require('inject-loader!./actions')
const Actions = injectActions({
'cm/common/event-tracker': {
eventTracker: {
track: () => {},
clear: () => {}
},
events: {
createNewFilter: '...',
createNewSearch: '...',
excludeAccounts: '...',
reportAsNews: '...',
}
},
}).default
const handlers = {
onAddQuery: () => { },
onAddToAccountList: () => { },
onExcludeProfile: () => { },
onReportAsNews: () => { }
}
const testProps = {
twitter: {
account: {
name: 'Twitter account',
uri: 'twitter://status/12345',
type: 'twitter'
},
handlers,
},
facebookPage: {
account: {
name: 'Facebook page account',
uri: 'facebook://page/12345',
type: 'facebook'
},
handlers
}
}
describe('Actions component', () => {
let node
beforeEach(() => {
node = document.createElement('div')
})
afterEach(() => {
ReactDOM.unmountComponentAtNode(node)
})
it('returns empty actions array by default', () => {
const spyFn = expect.createSpy().andReturn(null)
ReactDOM.render(
<Actions>{spyFn}</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [] })
})
describe('Twitter', () => {
it('returns "Exclude" action', () => {
const { account, handlers } = testProps.twitter
const spyFn = expect.createSpy()
ReactDOM.render(
<Actions
account={account}
handlers={{
onExcludeProfile: handlers.onExcludeProfile
}}
isCaseLocked={false}
>
{spyFn}
</Actions>,
node
)
expect(spyFn).toHaveBeenCalledWith({ actions: [
{
handler: () => {},
id: 'exclude-account',
isInactive: false,
label: 'Exclude',
}
] })
})
})
First unit case works fine, but the second is wrong. Actually I don't need all object there too. I'd like to be only sure that it contains id: 'exclude-account' there.
Please guys about any help.
You can use expect.objectContaining(object)
matches any received object that recursively matches the expected properties
E.g.
describe('68770432', () => {
test('should pass', () => {
const spyFn = jest.fn();
spyFn({
actions: [{ handler: () => {}, id: 'exclude-account', isInactive: false, label: 'Exclude' }],
});
expect(spyFn).toBeCalledWith({
actions: [
expect.objectContaining({
id: 'exclude-account',
}),
],
});
});
});
I followed this recommendation from the Leaflet Routing Machine regarding interactions i.e. onClicks.
With my implementation, I'm saving the waypoints in local-storage—saving the latitude and longitude obj I get from the map click, to an array called markers
The event handler has a conditional which separates the click into two outcomes—an adding (to the markers array) or updating it.
Like I said in the title, initial interaction is fine, it's just when I remove any marker and try to add it again is the problem. Also I noticed the markers array is completely empty, and next event fired is an update when clearly it should be an addition:
Here is the relevant code in the Routing Machine:
class Routing extends MapLayer {
static contextType = UserContextDispatch;
constructor(props) {
super(props);
this.state = {
showSpinner: false,
localDispatch: null,
};
this.handleLoader = this.handleLoader.bind(this);
this.handleRemoveWayPoint = this.handleRemoveWayPoint.bind(this);
this.handleSetMarker = this.handleSetMarker.bind(this);
}
handleRemoveWayPoint() {
var waypoints = this.control.getWaypoints();
for (let i = waypoints.length - 1; i >= 0; i--) {
console.log('waypoints[i].latLng !== null ', waypoints[i].latLng !== null);
if (waypoints[i].latLng !== null) {
waypoints[i].latLng = null;
break;
}
}
this.control.setWaypoints(waypoints);
}
createLeafletElement(props) {
const { map } = this.props.leaflet;
if (map && !this.control) {
this.control = L.Routing.control({
collapsible: true,
show: false,
position: 'bottomleft',
lineOptions: {
styles: [{ color: 'chartreuse', opacity: 1, weight: 5 }]
},
waypoints: [null],
createMarker: function(i, wp, nWps) {
if (i === 0) {
return L.marker(wp.latLng, {
icon: startIcon,
draggable: true,
keyboard: true,
alt: 'current location'
}).on('drag', function(e) {
e.latlng.alt = 'current location';
console.log('there be dragons start!!', e);
RoutingMachineRef.handleSetMarker({
...e.oldLatLng,
...e.latlng
});
});
}
if (i === nWps - 1) {
return L.marker(wp.latLng, {
icon: endIcon,
draggable: true,
alt: 'current destination'
}).on('drag', function(e) {
e.latlng.alt = 'current destination';
console.log('there be dragons dest!!', e);
RoutingMachineRef.handleSetMarker({
...e.oldLatLng,
...e.latlng
});
});
}
}
});
L.Routing.errorControl(this.control).addTo(map);
}
return this.control.getPlan();
}
componentDidMount() {
const { map } = this.props.leaflet;
console.log('markers ', markers);
this.setState(prevState => {
localDispatch: prevState.localDispatch = this.context.dispatch;
});
map.addControl(this.control);
}
updateLeafletElement(fromProps, toProps) {
const { map } = this.props.leaflet;
var self = this;
self;
var { markers } = this.props;
function createButton(label, container) {
var btn = L.DomUtil.create('button', '', container);
btn.setAttribute('type', 'button');
btn.innerHTML = label;
return btn;
}
var { localDispatch } = this.state;
var container = L.DomUtil.create('div'),
startBtn = createButton('Start from this location', container),
destBtn = createButton('Go to this location', container);
map.on(
'click',
function(e) {
L.popup()
.setContent(container)
.setLatLng(e.latlng)
.openOn(map);
L.DomEvent.on(startBtn, 'click', function() {
if (e.latlng) {
e.latlng.alt = 'current location';
console.log('adding);
localDispatch({
type: 'addMarker',
payload: {
marker: e.latlng
}
});
}
if (markers.length === 0) {
console.log('updating ');
e.latlng.alt = 'current location';
localDispatch({
type: 'updateMarkers',
payload: {
marker: e.latlng
}
});
}
self.control.spliceWaypoints(0, 1, e.latlng);
map.closePopup();
});
L.DomEvent.on(
destBtn,
'click',
function() {
console.log('e', e);
if (markers[1] === undefined) {
e.latlng.alt = 'current destination';
console.log('e.latlng ', e.latlng);
localDispatch({
type: 'addMarker',
payload: {
marker: e.latlng
}
});
}
if (toProps.markers[1] !== undefined) {
console.log('updating ');
e.latlng.alt = 'current destination';
localDispatch({
type: 'updateMarkers',
payload: {
marker: e.latlng
}
});
}
this.control.spliceWaypoints(1, 1, e.latlng);
map.closePopup();
}.bind(this)
);
}.bind(this)
);
if (toProps.removeRoutingMachine !== false) {
this.control.setWaypoints([]);
}
}
componentWillUnmount() {
this.destroyRouting();
}
destroyRouting() {
const { map } = this.props.leaflet;
if (map) {
map.removeControl(this.control);
}
}
}
export default withLeaflet(Routing);
Thanks in advance!
As you can see I have some code related to the Map (the onClick for the waypoints) in the RoutingMachine itself; after thinking about it I moved it to the Map component as a handlerFunction. And now it works!
import React, { useState, useEffect, useRef } from 'react';
import { Button, Dimmer, Loader } from 'semantic-ui-react';
import L from 'leaflet';
import * as ELG from 'esri-leaflet-geocoder';
import Control from 'react-leaflet-control';
// import MapboxLayer from '../MapboxLayer/MapboxLayer.jsx';
import Routing from '../RoutingMachine/RoutingMachine.jsx';
import { parse, stringify } from 'flatted';
import { userState, userDispatch } from '../Context/UserContext.jsx';
import UIContext from '../Context/UIContext.jsx';
function currentMapViewPropsAreEqual(prevProps, nextProps) {
console.log('prevProps, nextProps ', prevProps, nextProps);
console.log(
'prevProps.currentMapView === nextProps.currentMapView && prevProps.Map === nextProps.Map && prevProps.TileLayer === nextProps.TileLayer ',
prevProps.currentMapView === nextProps.currentMapView &&
prevProps.Map === nextProps.Map &&
prevProps.TileLayer === nextProps.TileLayer
);
return (
prevProps.currentMapView === nextProps.currentMapView &&
prevProps.Map === nextProps.Map &&
prevProps.TileLayer === nextProps.TileLayer
);
}
function MyMap({ currentMapView, Map, TileLayer }) {
console.log('currentMapView; ', currentMapView);
var [animate, setAnimate] = useState(false);
var [userLocation, setUserLocation] = useState(null);
const [myState, setMyState] = useState(null);
var handleWaypointsOnMapRef = useRef(handleWaypointsOnMap);
var mapRef = useRef();
var mapRefForRoutingMachine = useRef();
var { state } = userState();
var { dispatch } = userDispatch();
var {
currentMap,
isRoutingVisible,
removeRoutingMachine,
isLengthOfMarkersLessThanTwo,
markers
} = state;
useEffect(() => {
handleWaypointsOnMapRef.current = handleWaypointsOnMap;
}); // update after each render
useEffect(() => {
var { current = {} } = mapRef;
var { leafletElement: map } = current;
console.log('foo');
console.log('currentMap ', currentMapView);
map.locate({ setView: true });
map.on('locationfound', handleOnLocationFound);
}, []);
useEffect(() => {
var searchControl = new ELG.Geosearch({
useMapBounds: false
});
var { current = {} } = mapRef;
var { leafletElement: map } = current;
console.log('mapRef ', mapRef);
searchControl.addTo(map);
var cb = e => handleWaypointsOnMapRef.current(e); // then use most recent cb value
searchControl.on('results', cb);
if (Object.keys(currentMap).length === 0) {
dispatch({
type: 'setMap',
payload: {
currentMap: stringify(map)
}
});
}
return () => {
searchControl.off('results', cb);
};
}, []);
function handleOnClickClearOneMarkerAtTime() {
dispatch({
type: 'setIsRoutingVisible',
payload: {
isRoutingVisible: false
}
});
mapRefForRoutingMachine.current.handleRemoveWayPoint();
dispatch({
type: 'deleteUserMarkers'
});
}
function handleOnClickClearAllMarkers() {
mapRefForRoutingMachine.current.handleClearWayPoints();
dispatch({
type: 'resetUserMarkers'
});
}
function handleOnClickMarkerClick(e) {
e.originalEvent.view.L.DomEvent.stopPropagation(e);
}
function handleWaypointsOnMap(e) {
var { current = {} } = mapRef;
var { leafletElement: map } = current;
dispatch({
type: 'setIsRoutingVisible',
payload: {
isRoutingVisible: true
}
});
dispatch({
type: 'setRemoveRoutingMachine',
payload: {
removeRoutingMachine: false
}
});
function createButton(label, container) {
var btn = L.DomUtil.create('button', '', container);
btn.setAttribute('type', 'button');
btn.innerHTML = label;
return btn;
}
var container = L.DomUtil.create('div'),
startBtn = createButton('Start from this location', container),
destBtn = createButton('Go to this location', container);
L.popup()
.setContent(container)
.setLatLng(e.latlng)
.openOn(map);
L.DomEvent.on(startBtn, 'click', function() {
if (markers.length === 0) {
e.latlng.alt = 'current location';
console.log('adding current location', e.latlng);
dispatch({
type: 'addMarker',
payload: {
marker: e.latlng
}
});
}
if (markers[0] != undefined) {
e.latlng.alt = 'current location';
console.log('updating current location', e.latlng);
dispatch({
type: 'updateMarkers',
payload: {
marker: e.latlng
}
});
}
mapRefForRoutingMachine.current.handleSpliceWaypoints(0, 1, e.latlng);
map.closePopup();
});
L.DomEvent.on(
destBtn,
'click',
function() {
console.log('e', e);
if (markers.length === 1) {
e.latlng.alt = 'current destination';
console.log('adding destination ', e.latlng);
dispatch({
type: 'addMarker',
payload: {
marker: e.latlng
}
});
}
if (markers.length === 2 && markers[1] !== undefined) {
e.latlng.alt = 'current destination';
console.log('updating destination', e.latlng);
dispatch({
type: 'updateMarkers',
payload: {
marker: e.latlng
}
});
}
mapRefForRoutingMachine.current.handleSpliceWaypoints(1, 1, e.latlng);
map.closePopup();
}.bind(this)
);
}
function handleOnViewportChanged(e) {
console.log('viewport change', e);
console.log('currentMapView ', currentMapView);
var { current = {} } = mapRef;
var { leafletElement: map } = current;
map.on('zoomend', function() {
var zoom = map.getZoom();
console.log('zoom ', zoom);
console.log("'dispatch setMapZoom'; ");
dispatch({
type: 'setMapZoom',
payload: {
currentMapView: zoom
}
});
});
}
function handleOnLocationFound(e) {
console.log('e ', e);
var { current = {} } = mapRef;
var { leafletElement: map } = current;
map.setZoom(currentMapView);
var latlng = e.latlng;
var radius = e.accuracy;
var circle = L.circle(latlng, radius);
circle.addTo(map);
}
return (
<Map
preferCanvas={true}
id="myMap"
animate={animate}
zoom={currentMapView}
ref={mapRef}
onViewportChanged={handleOnViewportChanged}
onClick={e => handleWaypointsOnMap(e)}
>
<TileLayer
url={`https://api.mapbox.com/styles/v1/${process.env.MAPBOX_USERNAME}/${
process.env.MAPBOX_STYLE_ID
}/tiles/256/{z}/{x}/{y}#2x?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`}
attribution='Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox'
/>
<Control position="bottomleft">
<Button onClick={handleOnClickClearOneMarkerAtTime} color="orange" size="small">
delete one marker!
</Button>
</Control>
<Control position="bottomright">
<Button onClick={handleOnClickClearAllMarkers} color="red" size="small">
clear all!
</Button>
</Control>
{mapRef && (
<Routing
isRoutingVisible={isRoutingVisible}
ref={mapRefForRoutingMachine}
markers={markers}
stringify={stringify}
isLengthOfMarkersLessThanTwo={isLengthOfMarkersLessThanTwo}
removeRoutingMachine={removeRoutingMachine}
userLocation={userLocation}
/>
)}
</Map>
);
}
var MemoizedMyMap = React.memo(MyMap, currentMapViewPropsAreEqual);
export default MemoizedMyMap;
And this is the Routing Machine:
import { MapLayer } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet-routing-machine';
import { withLeaflet } from 'react-leaflet';
import UserContextDispatch from '../Context/UserContext.jsx';
import { Dimmer, Loader } from 'semantic-ui-react';
import { isEqual } from 'lodash';
import AwesomeDebouncePromise from 'awesome-debounce-promise';
class Routing extends MapLayer {
static contextType = UserContextDispatch;
constructor(props) {
super(props);
this.state = {
showSpinner: false,
localDispatch: null,
markerIsBeingDragged: false,
currentMarker: {}
};
this.handleLoader = this.handleLoader.bind(this);
this.handleRemoveWayPoint = this.handleRemoveWayPoint.bind(this);
this.handleClearWayPoints = this.handleClearWayPoints.bind(this);
this.handleSpliceWaypoints = this.handleSpliceWaypoints.bind(this);
this.handleSetMarker = this.handleSetMarker.bind(this);
}
handleSetMarker(marker) {
var { markers } = this.props;
if (markers[0] !== undefined && markers[0].alt === 'current location') {
this.setState(prevState => ({
currentMarker: { ...prevState.currentMarker, ...marker }
}));
}
if (markers[1] !== undefined && markers[1].alt === 'current destination') {
this.setState(prevState => ({
currentMarker: { ...prevState.currentMarker, ...marker }
}));
}
console.log('this.state ', this.state);
}
handleSpliceWaypoints(start, end, obj) {
this.control.spliceWaypoints(start, end, obj);
}
handleLoader() {
var { showSpinner } = this.state;
if (this.state.showSpinner === false) {
this.setState(function(prevState) {
return { showSpinner: !prevState.showSpinner };
});
return (
<Dimmer active inverted>
<Loader />
</Dimmer>
);
}
this.setState(function(prevState) {
return { showSpinner: (prevState.showSpinner = true) };
});
}
handleRemoveWayPoint() {
var waypoints = this.control.getWaypoints();
for (let i = waypoints.length - 1; i >= 0; i--) {
console.log('waypoints[i].latLng !== null ', waypoints[i].latLng !== null);
if (waypoints[i].latLng !== null) {
waypoints[i].latLng = null;
break;
}
}
console.log('waypoints ', waypoints);
this.control.setWaypoints(waypoints);
}
handleClearWayPoints() {
this.control.setWaypoints([L.latLng(null, null), L.latLng(null, null)]);
}
componentDidMount() {
const { map } = this.props.leaflet;
var { markers } = this.props;
this.control.setWaypoints([L.latLng(markers[0]), L.latLng(markers[1])]);
this.setState(prevState => {
localDispatch: prevState.localDispatch = this.context.dispatch;
});
map.addControl(this.control);
}
createLeafletElement(props) {
const { map } = this.props.leaflet;
var startIcon = new L.Icon({
iconUrl:
'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
var endIcon = new L.Icon({
iconUrl:
'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
if (map && !this.control) {
var RoutingMachineRef = this;
this.control = L.Routing.control({
collapsible: true,
show: false,
position: 'bottomleft',
lineOptions: {
styles: [{ color: 'chartreuse', opacity: 1, weight: 5 }]
},
waypoints: [null],
createMarker: function(i, wp, nWps) {
if (i === 0) {
return L.marker(wp.latLng, {
icon: startIcon,
draggable: true,
keyboard: true,
alt: 'current location'
}).on('move', function(e) {
e.target._latlng.alt = 'current location';
console.log('e.target._latlng', e.target._latlng);
console.log('there be dragons start!!', e);
RoutingMachineRef.setState(prevState => ({
markerIsBeingDragged: !prevState.markerIsBeingDragged
}));
RoutingMachineRef.handleSetMarker(e.target._latlng);
});
}
if (i === nWps - 1) {
return L.marker(wp.latLng, {
icon: endIcon,
draggable: true,
alt: 'current destination'
}).on('move', function(e) {
e.target._latlng.alt = 'current destination';
console.log(' e.target._latlng', e.target._latlng);
RoutingMachineRef.setState(prevState => ({
markerIsBeingDragged: !prevState.markerIsBeingDragged
}));
console.log('there be dragons dest!!', e);
RoutingMachineRef.handleSetMarker(e.target._latlng);
});
}
}
});
L.Routing.errorControl(this.control).addTo(map);
}
return this.control.getPlan();
}
updateLeafletElement(fromProps, toProps) {
var { currentMarker, localDispatch, markerIsBeingDragged } = this.state;
// console.log('fromProps, toProps ', fromProps, toProps);
if (markerIsBeingDragged && currentMarker.hasOwnProperty('alt')) {
if (isEqual(toProps.markers[0], currentMarker) === false) {
localDispatch({
type: 'updateMarkers',
payload: {
marker: currentMarker
}
});
}
if (isEqual(toProps.markers[1], currentMarker) === false) {
localDispatch({
type: 'updateMarkers',
payload: {
marker: currentMarker
}
});
}
this.setState(prevState => ({
markerIsBeingDragged: !prevState.markerIsBeingDragged
}));
}
if (toProps.removeRoutingMachine !== false) {
this.control.setWaypoints([]);
}
}
componentWillUnmount() {
console.log("'unmount' ", 'unmount');
this.destroyRouting();
}
destroyRouting() {
const { map } = this.props.leaflet;
if (map) {
map.removeControl(this.control);
}
}
}
export default withLeaflet(Routing);
In the Material Table below taken from the example, how would I do to make the 'ahiddenfield' appear when adding a new row? I'm not really sure how to access the state when the new row is about to be added (Before onRowAdd). Is it possible without too much headache?
constructor(props) {
super(props);
this.state = {
columns: [
{ title: 'Name', field: 'name' },
{ title: 'Hidden Field', field: 'hiddenfield', hidden: true }
],
data: [
{ name: 'Mehmet' },
{ name: 'Zerya Betül'},
]
}
}
render() {
return (
<MaterialTable
title="Editable Preview"
columns={this.state.columns}
data={this.state.data}
editable={{
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = this.state.data;
data.push(newData);
this.setState({ data }, () => resolve());
}
resolve()
}, 1000)
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = this.state.data;
const index = data.indexOf(oldData);
data[index] = newData;
this.setState({ data }, () => resolve());
}
resolve()
}, 1000)
}),
onRowDelete: oldData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
let data = this.state.data;
const index = data.indexOf(oldData);
data.splice(index, 1);
this.setState({ data }, () => resolve());
}
resolve()
}, 1000)
}),
}}
/>
)
}
} ```
In your <MaterialTable /> component, you should replace the data prop like this:
data={Array.from(this.state.data)}.
This is not documented in the docs. More info here: https://github.com/mbrn/material-table/issues/1900
My code:
componentWillReceiveProps(nextProps) {
if (get(nextProps.signInSliderDetails, 'showWelcomePage')) {
this.timer = setTimeout(() => {
this.onPrimaryCloseButtonClick();
}, 3000);
}
}
onClickOfCreateAccountButton() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
el.classList.add('SignInSlider-animate-show');
el.classList.remove('SignInSlider-animate-hide');
setTimeout(() => {
this.props.signInSliderActions.openCreateAccountPage();
el1.classList.add('SignInSlider-animate-show');
}, 5);
}
onClickPasswordReset() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el.classList.add('SignInSlider-animate-show');
el.classList.remove('SignInSlider-animate-hide');
setTimeout(() => {
this.props.signInSliderActions.openForgotPasswordResetPage();
el1.classList.add('SignInSlider-animate-show');
}, 5);
}
onBackButtonClick() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
const el2 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el1.classList.remove('SignInSlider-animate-show');
el2.classList.remove('SignInSlider-animate-show');
setTimeout(() => {
this.props.signInSliderActions.resetSignInSlider();
el.classList.add('SignInSlider-animate-hide');
}, 5);
}
onPrimaryCloseButtonClick() {
this.removeSliderClasses();
this.timer && clearTimeout(this.timer);
this.props.signInSliderActions.resetSignInSlider();
this.props.onPrimaryCloseButtonClick(this.props.signInSliderDetails.showWelcomePage);
}
removeSliderClasses() {
const el = document.getElementsByClassName('SignInSlider-loginSlider')[0];
const el1 = document.getElementsByClassName('SignInSlider-createAccountSlider')[0];
const el2 = document.getElementsByClassName('SignInSlider-passwordSlider')[0];
el.classList.remove('SignInSlider-animate-show');
el1.classList.remove('SignInSlider-animate-show');
el2.classList.remove('SignInSlider-animate-show');
}
render() {
const { deviceType, preferences, messagesTexts, signInSliderDetails } = this.props;
const { showCreateAccountPage, showWelcomePage, showForgotPasswordPage, createAccount,
signInDetails, passwordResetResponse } = signInSliderDetails;
const passwordResetDetails = { passwordResetResponse };
const hideBackArrowCloseButton = !(showCreateAccountPage || showForgotPasswordPage);
// Need to have a new logic. Not Token Provider here
const firstName = TokenProvider.get('DP_USER_NAME');
return (
<SlidePanel
isOpenPanel={this.props.isOpenPanel}
onClosePanel={!hideBackArrowCloseButton && this.onBackButtonClick}
onPrimaryCloseButtonClick={this.onPrimaryCloseButtonClick}
panelTitle={!hideBackArrowCloseButton && 'Back to Sign-In'}
hideBackArrowCloseButton={hideBackArrowCloseButton}
isPrimaryCloseButtonRequired>
<div className={cx('signInSliderPanel')}>
<div className={cx('loginSlider')}>
{ !showCreateAccountPage && !showWelcomePage && !showForgotPasswordPage &&
<LoginWrapper
signInDetails={signInDetails}
deviceType={deviceType}
preferences={preferences}
messagesTexts={messagesTexts}
source="account"
actions={this.props.signInActions}
onClickOfCreateAccountButton={this.onClickOfCreateAccountButton}
onClickPasswordReset={this.onClickPasswordReset}
isSignInSliderReq
/> }
</div>
<div className={cx('createAccountSlider')}>
{showCreateAccountPage &&
<CreateAccountWrapper
createAccount={createAccount}
isSignInSliderReq
deviceType={deviceType}
messagesTexts={this.props.messagesTexts}
preferences={this.props.preferences}
actions={this.props.createAccountActions}/> } </div>
<div className={cx('passwordSlider')}>
{showForgotPasswordPage &&
<PasswordResetWrapper
isSignInSliderReq
messagesTexts={messagesTexts.passwordReset}
preferences={preferences}
createAccountActions={this.props.createAccountActions}
actions={this.props.passwordResetActions}
passwordResetDetails={passwordResetDetails}
signInSliderActions={this.props.signInSliderActions}
onPrimaryCloseButtonClick={this.onPrimaryCloseButtonClick}
deviceType
/>} </div>
<div className={cx('welcomeSlider')}>
{ showWelcomePage &&
<Welcome
messagesTexts={messagesTexts.signInSlider}
firstName={firstName} />} </div>
</div>
</SlidePanel>
);
}
}
My Test file:
const actions = {
signInActions: sinon.spy(),
createAccountActions: sinon.spy(),
passwordResetActions: sinon.spy(),
signInSliderActions: {
resetSignInSlider: sinon.spy(),
openForgotPasswordResetPage: sinon.spy(),
openCreateAccountPage: sinon.spy(),
},
};
const props = {
actions,
isOpenPanel: false,
onPrimaryCloseButtonClick: sinon.spy(),
preferences: preferences.common,
messagesTexts,
deviceType,
signInSliderDetails,
isSignInSliderReq: true,
};
describe('Shallow Render', () => {
let wrapper;
beforeEach(() => {
const gl = global.window.document.getElementsByClassName.returns({ className: '' });
gl.returns([elStub]);
wrapper = shallow(
<SignInSlider
{...props}
/>,
);
});
afterEach(() => {
global.window.document.getElementsByClassName.reset();
elStub.classList.add.reset();
elStub.classList.remove.reset();
wrapper.unmount();
});
it('it rendered successfully', () => {
expect(wrapper.find('SlidePanel')).to.exists;
expect(wrapper.find('LoginWrapper')).to.exists;
wrapper.find('LoginWrapper').prop('onClickOfCreateAccountButton')();
wrapper.find('LoginWrapper').prop('onClickPasswordReset')();
});
it('it shows createAccountPage SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: true,
showWelcomePage: false,
showForgotPasswordPage: false,
},
});
wrapper.update();
expect(wrapper.find('CreateAccountWrapper')).to.exists;
});
it('it shows Forgot password page SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: false,
showWelcomePage: false,
showForgotPasswordPage: true,
},
});
wrapper.update();
expect(wrapper.find('PasswordResetWrapper')).to.exists;
wrapper.find('PasswordResetWrapper').prop('onPrimaryCloseButtonClick')();
wrapper.find('SlidePanel').prop('onBackButtonClick')();
});
it('it shows Welcome page SignInSlider', () => {
wrapper.setProps({
signInSliderDetails: {
createAccount: {},
signInDetails: {},
passwordResetDetails: {
passwordResetResponse: {},
},
showCreateAccountPage: false,
showWelcomePage: true,
showForgotPasswordPage: false,
},
firstName: 'Vini',
});
wrapper.update();
setTimeout(() => {
expect(props.onPrimaryCloseButtonClick).to.have.been.called;
}, 3000);
expect(wrapper.find('Welcome')).to.exists;
});
});
Please help.