ReactJS - How can I make this option conditional? - reactjs

I am working with the FlatPickr Date/Time module trying to implement the monthSelect plugin on only those fields that need it. How can I make the plugin option "monthSelectPlugin" line of code conditional based on a prop that I pass in?
(see comments in Flatpickrdatetime.jsx)
I get the following error with the code below. Also, I'm sure there is a better way to make one line of code conditional, and I am here for it, just not sure how.
Error:
Error: Flatpickrdatetime(...): Nothing was returned from render.
This usually means a return statement is missing. Or, to render nothing, return null.
Arrival / Departure page:
import React from 'react';
import { Grid, Typography, FormControl} from '#material-ui/core';
import Flatpickrdatetime from './FormFields/Flatpickrdatetime';
<Grid container item spacing={3}>
<Grid item xs={12} md={6}>
<InputLabel shrink htmlFor="bootstrap-input">Arrival Date</InputLabel>
<Flatpickrdatetime type="monthSelect" />
</Grid>
<Grid item xs={12} md={6}>
<InputLabel shrink htmlFor="bootstrap-input">Depart Date</InputLabel>
<Flatpickrdatetime />
</Grid>
</Grid>
Flatpickrdatetime.jsx
/* https://flatpickr.js.org/examples/ */
import React from 'react';
import { fade, makeStyles } from '#material-ui/core/styles';
import "flatpickr/dist/themes/light.css";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index.js";
import "flatpickr/dist/plugins/monthSelect/style.css";
import "flatpickr/dist/themes/light.css";
import Flatpickr from "react-flatpickr";
export default function Flatpickrdatetime(props) {
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexWrap: 'wrap',
},
margin: {
marginTop: theme.spacing(3),
width:'100%'
},
flatpickr:{
marginBottom:'0.25em',
borderRadius: 4,
}
}));
const classes = useStyles();
{(() => {
if (props.type === 'monthSelect'){
return (
<Flatpickr
options={{
format: "m/d/Y",
altFormat: "m/d/Y",
altInput: true,
allowInput: true,
// if props.type equals monthSelect
plugins: [new monthSelectPlugin({shorthand: false, dateFormat: "Y-m-d", altFormat: "M Y"})]
}}
className={classes.flatpickr} />
);
}else if(props.type != 'monthSelect'){
return (
<Flatpickr
options={{
format: "m/d/Y",
altFormat: "m/d/Y",
altInput: true,
allowInput: true,
// no plugin if props.type does not equal monthSelect
}}
className={classes.flatpickr} />
);
}else{
return null;
}
})()}
}

Your IFFE here {(() => { seems to be the problem. Why do you use it? Just remove it and leave the conditional statement and you should be fine.

your IFFE does return a value, but it is wrapped in a block statement and you miss your return Component. if you had it like below would work, but it's not neat:
return (() => {return something })()
First, I would recommend to define your styles outside of the component. Otherwise each rerender makeStyles is called unnecessary.
Second, you could abstract your options object build logic outside the render part, and avoid all that code duplication. You could useMemo to memorize its value. It will keep the same options unless type changes (you set as dependency).
one more thing, based on your logic null is never hit. It's a not reachable point, it will always render the first or second condition since if one is true the other is false.
after this you could have your component as:
import React, { useMemo } from 'react';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexWrap: 'wrap',
},
margin: {
marginTop: theme.spacing(3),
width:'100%'
},
flatpickr:{
marginBottom:'0.25em',
borderRadius: 4,
}
}));
export default function Flatpickrdatetime({ type }) {
const classes = useStyles();
const options = useMemo(() => {
const flatpickOps = {
format: "m/d/Y",
altFormat: "m/d/Y",
altInput: true,
allowInput: true,
}
if (type === 'monthSelect') {
flatpickOps.plugins = [new monthSelectPlugin({shorthand: false, dateFormat: "Y-m-d", altFormat: "M Y"})]
}
return flatpickOps
}, [type])
return <Flatpickr options={options} className={classes.flatpickr} />
}

Related

Mui Datagrid not populating, data is there

I have a few Mui datagrids through my app. I recently switched over to using RTK Query for the api and was able to get all the grids functioning and displaying fine. I honestly do not know what changed (since it took place after I was done mucking with the grid components and was adjusting the auth/login to RTK) but 3 of the 4 are displaying an empty grid with no errors. I can console the data, and even the corner of the grids shows the total number of entries, which reflects the changes if I add an item to the array that should be displayed by the grid. I already had the container components set to display: flex and set autoHeight and autoWidth on the grids. I've tried adjusting the containers, even putting height to 5000, with no change. As I mentioned, I have 1 that still displays correctly, and even copying the display props for that grid to the others had no effect.
import React from 'react';
import PropTypes from 'prop-types';
import { DataGrid, GridActionsCellItem } from '#mui/x-data-grid';
import { Link } from 'react-router-dom';
import { useGetVisitsByUserIdQuery } from '../../redux/apiSlice';
import { CircularProgress, Box } from '#mui/material';
const UserVisits = ({ user }) => {
const userId = user._id
const {
data,
isLoading,
isSuccess
} = useGetVisitsByUserIdQuery(userId);
console.log(data)
const columns =
[
{
field: 'client',
headerName: 'Client',
width: 150,
renderCell: (params) => {
return (
<Link to={`/ClientChart/${params?.value?._id}`}>{params?.value?.fullName}</Link>
)}
},
{
field: 'visitStart',
headerName: 'Start Time',
type: 'date',
width: 200,
valueFormatter: (params) => {
const date = new Date(params.value);
let options = {
year: "numeric",
month: "numeric",
day: "numeric"
};
return date.toLocaleTimeString("en-US", options);
}
},
{
field: 'visitEnd',
headerName: 'End Time',
type: 'date',
width: 200,
valueFormatter: (params) => {
const date = new Date(params.value);
let options = {
year: "numeric",
month: "numeric",
day: "numeric"
};
return date.toLocaleTimeString("en-US", options);
}
},
{field: 'location', headerName: 'Location', width: 150},
];
let content
if (isLoading) {
content = <CircularProgress />
} else if (isSuccess) {
content =
<div style={{ display: 'flex', height: '100%'}}>
<Box sx={{ flexGrow: 1 }}>
{ data &&
<DataGrid
autoHeight
autoWidth
getRowId={(row) => row.id}
rows={data}
columns={columns}
rowsPerPageOptions={[20, 50, 100]}
autoPageSize
//sx={{ height: '100%', width: '100%' }}
/>}
</Box>
</div>
}
return (
<div>
<h1>Visits</h1>
<div>
{content}
</div>
</div>
)
}
If I push new data to the grid, the number of entries in the bottom right corner adjusts, and the console shows the correct array of data, as well as Redux DevTools showing the data in state. Even manually adjusting the height of the div/Box containing the grid, the grid itself never changes in height.
Again they had been working perfectly, and I'm at a total loss as to what would have affected them. Any help is greatly appreciated.
Thanks
So the issue was resolved by removing 'autoPageSize' from the Datagrid props. Playing with it, it seems that autoHeight and autoPageSize cause the grid to break when in place together.
Could not find any specific references online to this being an issue, but I was able to recreate the issue on this codesandbox I found: https://codesandbox.io/s/datagrid-v5-0-1-autoheight-autopagesize-9dy64?file=/src/App.tsx
If you add in autoPageSize to the datagrid props, the grid goes empty.

Testing component with lodash.debounce delay failing

I have a rich text editor input field that I wanted to wrap with a debounced component. Debounced input component looks like this:
import { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
function DebouncedInput(props) {
const [value, setValue] = useState(props.value);
const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue), props.delay);
const handleChange = (nextValue) => {
setValue(nextValue);
debouncedSave(nextValue);
};
return props.renderProps({ onChange: handleChange, value });
}
export default DebouncedInput;
I am using DebouncedInput as a wrapper component for MediumEditor:
<DebouncedInput
value={task.text}
onChange={(text) => onTextChange(text)}
delay={500}
renderProps={(props) => (
<MediumEditor
{...props}
id="task"
style={{ height: '100%' }}
placeholder="Task text…"
disabled={readOnly}
key={task.id}
/>
)}
/>;
MediumEditor component does some sanitation work that I would like to test, for example stripping html tags:
class MediumEditor extends React.Component {
static props = {
id: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
disabled: PropTypes.bool,
uniqueID: PropTypes.any,
placeholder: PropTypes.string,
style: PropTypes.object,
};
onChange(text) {
this.props.onChange(stripHtml(text) === '' ? '' : fixExcelPaste(text));
}
render() {
const {
id,
value,
onChange,
disabled,
placeholder,
style,
uniqueID,
...restProps
} = this.props;
return (
<div style={{ position: 'relative', height: '100%' }} {...restProps}>
{disabled && (
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
cursor: 'not-allowed',
zIndex: 1,
}}
/>
)}
<Editor
id={id}
data-testid="medium-editor"
options={{
toolbar: {
buttons: ['bold', 'italic', 'underline', 'subscript', 'superscript'],
},
spellcheck: false,
disableEditing: disabled,
placeholder: { text: placeholder || 'Skriv inn tekst...' },
}}
onChange={(text) => this.onChange(text)}
text={value}
style={{
...style,
background: disabled ? 'transparent' : 'white',
borderColor: disabled ? 'grey' : '#FF9600',
overflowY: 'auto',
color: '#444F55',
}}
/>
</div>
);
}
}
export default MediumEditor;
And this is how I am testing this:
it('not stripping html tags if there is text', async () => {
expect(editor.instance.state.text).toEqual('Lorem ipsum ...?');
const mediumEditor = editor.findByProps({ 'data-testid': 'medium-editor' });
const newText = '<p><b>New text, Flesk</b></p>';
mediumEditor.props.onChange(newText);
// jest.runAllTimers();
expect(editor.instance.state.text).toEqual(newText);
});
When I run this test I get:
Error: expect(received).toEqual(expected) // deep equality
Expected: "<p><b>New text, Flesk</b></p>"
Received: "Lorem ipsum ...?"
I have also tried running the test with jest.runAllTimers(); before checking the result, but then I get:
Error: Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
I have also tried with:
jest.advanceTimersByTime(500);
But the test keeps failing, I get the old state of the text.
It seems like the state just doesn't change for some reason, which is weird since the component used to work and the test were green before I had them wrapped with DebounceInput component.
The parent component where I have MediumEditor has a method onTextChange that should be called from the DebounceInput component since that is the function that is being passed as the onChange prop to the DebounceInput, but in the test, I can see this method is never reached. In the browser, everything works fine, so I don't know why it is not working in the test?
onTextChange(text) {
console.log('text', text);
this.setState((state) => {
return {
task: { ...state.task, text },
isDirty: true,
};
});
}
On inspecting further I could see that the correct value is being passed in the test all the way to handleChange in DebouncedInput. So, I suspect, there are some problems with lodash.debounce in this test. I am not sure if I should mock this function or does mock come with jest?
const handleChange = (nextValue) => {
console.log(nextValue);
setValue(nextValue);
debouncedSave(nextValue);
};
This is where I suspect the problem is in the test:
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
I have tried with mocking debounce like this:
import debounce from 'lodash.debounce'
jest.mock('lodash.debounce');
debounce.mockImplementation(() => jest.fn(fn => fn));
That gave me error:
TypeError: _lodash.default.mockImplementation is not a function
How should I fix this?
I'm guessing that you are using enzyme (from the props access).
In order to test some code that depends on timers in jest:
mark to jest to use fake timers with call to jest.useFakeTimers()
render your component
make your change (which will start the timers, in your case is the state change), pay attention that when you change the state from enzyme, you need to call componentWrapper.update()
advance the timers using jest.runOnlyPendingTimers()
This should work.
Few side notes regarding testing react components:
If you want to test the function of onChange, test the immediate component (in your case MediumEditor), there is no point of testing the entire wrapped component for testing the onChange functionality
Don't update the state from tests, it makes your tests highly couple to specific implementation, prove, rename the state variable name, the functionality of your component won't change, but your tests will fail, since they will try to update a state of none existing state variable.
Don't call onChange props (or any other props) from test. It makes your tests more implementation aware (=high couple with component implementation), and actually they doesn't check that your component works properly, think for example that for some reason you didn't pass the onChange prop to the input, your tests will pass (since your test is calling the onChange prop), but in real it won't work.
The best approach of component testing is to simulate actions on the component like your user will do, for example, in input component, simulate a change / input event on the component (this is what your user does in real app when he types).

Remove (or at least hide) card on react-admin List

I want to get rid of the Card on the background react-admin's List (v3.4.2). I get the desired effect if I define a string on the component property:
<List component={"some string"}/>
But this spams the console with an error:
And I don't want to have that error. On top of that, I think I shouldn't be changing the component property (I can't find it on the official docs).
The code should be the following: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/list/List.js
How should I do this? Is it possible to pass a style? Is there any component that works out of the box? Or should I just go custom?
You can hide the background using styling:
import { makeStyles } from '#material-ui/core/styles'
const useListStyles = makeStyles(theme => ({
content: {
boxShadow: 'none',
backgroundColor: 'inherit',
},
main: {
// backgroundColor: 'red',
},
root: {
// backgroundColor: 'red',
},
}))
const MyList = (props) => {
const classes = useListStyles()
return (
<List classes={classes} {...props} >
...
</List>
)
}

Clearing map layers with react-leaflet and hooks

I'm building a custom plugin for react-leaflet to locate the user using leaflet's locate method.
It works basically, but with the one problem of the layer not clearing between turning location off and back on again. Each time the locate button is toggled, locate should start fresh.
Here is a codesandbox of the problem. As you toggle the button, the circle becomes darker as the layers are stacked on top of each other.
Here is the component:
import React, { useEffect, useState, useRef } from 'react'
import L from 'leaflet'
import { useLeaflet } from 'react-leaflet'
import LocationSearchingIcon from '#material-ui/icons/LocationSearching';
import MapButton from './MapButton'
function Locate() {
const { map } = useLeaflet();
const [locate, setLocate] = useState(false);
function toggleLocate() {
setLocate(!locate)
}
console.log(locate)
const layerRef = useRef(L.layerGroup());
useEffect(() => {
if (locate) {
map.removeLayer(layerRef.current)
map.locate({ setView: false, watch: true, enableHighAccuracy: true }) /* This will return map so you can do chaining */
.on('locationfound', function (e) {
L.circleMarker([e.latitude, e.longitude], {
radius: 10,
weight: 3,
color: 'white',
fillColor: 'blue',
fillOpacity: 1
}).addTo(layerRef.current);
L.circle([e.latitude, e.longitude], e.accuracy / 2, {
weight: 1,
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.2
}).addTo(layerRef.current);
window.localStorage.setItem('accuracy', e.accuracy)
map.setView([e.latitude, e.longitude], 16)
layerRef.current.addTo(map)
})
.on('locationerror', function (e) {
alert("Location error or access denied.");
})
} if (!locate) {
map.stopLocate();
map.removeLayer(layerRef.current);
}
}, [locate, map]);
return (
<MapButton
title={locate ? 'Click to disable location' : 'Click to locate'}
onClick={toggleLocate}
left
top={102}
>
<LocationSearchingIcon fontSize="small" style={{ color: locate ? 'orange' : '' }} />
</MapButton>
)
}
export default Locate
I would appreciate any solution or tips to stop the layers stacking, and clear properly went the button is toggled. Thanks
As Falke Design's mentions in a comment above:
Add the circle to a featureGroup and then every time locationfound is fired, you call featuregroup.clearLayers() and then add the circle new to the featureGroup
This solved the problem.
The working codesandbox is here

React component won't take onChange event

Obligatory "new to react" paragraph here. I have this rating component I got from material-ui and i'm trying to send the value to a database.
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Rating from '#material-ui/lab/Rating';
import Box from '#material-ui/core/Box';
const labels = {
0.5: 'Worst of the Worst',
1: 'Bad',
1.5: 'Poor',
2: 'Passable',
2.5: 'Ok',
3: 'Good',
3.5: 'Damn Good',
4: 'Great',
4.5: 'Love',
5: 'Perfection',
};
const useStyles = makeStyles({
root: {
width: 200,
display: 'flex',
alignItems: 'center',
},
});
export default function HoverRating(props) {
const [value, setValue] = React.useState(2);
const [hover, setHover] = React.useState(-1);
const classes = useStyles();
const onRatingChange = (event) => {
console.log(event.target.value)
props.reduxDispatch ({ type: "RATING_CHANGE", value: event.target.value
})
}
return (
<div className={classes.root}>
<Rating
name="hover-feedback"
value={value}
defaultValue={0}
precision={0.5}
size="large"
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
onChangeActive={(event, newHover) => {
setHover(newHover);
}}
{ onRatingChange }
/>
<br/>
{value !== null && <Box ml={2}>{labels[hover !== -1 ? hover : value]}</Box>}
</div>
);
}
It doesn't like something about my onRatingChange function. I've moved it all over the place and it's still throwing errors. I just really don't understand the issue. I'm mostly getting-
"./src/components/Rating.js
Line 54:11: Parsing error: Unexpected token, expected "..."
I've been at this for hours and I salvation.
Change your code from:
{ onRatingChange }
to:
onRatingChange={onRatingChange}
and change your file extension from .js to .jsx because you are using the JSX syntax
First, you appear the be storing the rating in two places: in your local state (with React.useState), and from the looks of your onRatingChange function, in a Redux store somewhere. It would be a good idea to pick one, and use that.
As for the direct answer to your question, your syntax is wrong. You're writing your Rating component in the following way:
<Rating
// ...
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
// ...
{ onRatingChange }
/>
The Rating component expects an onChange prop. I assume you want your onRatingChange function to be called when the rating changes. As such, you'd write:
<Rating
// ...
onChange={onRatingChange}
/>
The complication here though is that you're trying to register two different handlers for the rating change event. The bottom line is, decide on one, and then pass that as a callback function to the onChange prop.

Resources