mui-Autocomplete dropdown goes up when the state changed - reactjs

I have a component which written with mui Autocomplete component. I have a expand-collapse ability in the component and ı keep the expanded collapsed values in one state.
When ı clicked to something that will change this state, my autocomplete re-renders as ı expect but dropdown list goes up. This is my main problem.
How can ı avoid this? Is there any prop or something to solve that ? I tried to manage with sholudComponent update but in that case, state changes but autocomplete not re-renders so it calses another problem.
Example gif belong here
I hold the expanded portfolios in expandedPortfolios state and when ı clicked arrow ıcon handle with handleExpand function and change the state so ı can manage which group will expanded or collapsed.
handleExpand = (portfolioName) => {
var portfolio = (this.state.portfoliosAsGroup || []).find(p => p.name == portfolioName);
if (portfolio) {
let portfolioId = portfolio.id;
let current = this.state.expandedPortfolios;
console.log(current);
let Ids = [];
if (current.includes(portfolioId)) {
console.log("filter");
Ids = current.filter((i) => i !== portfolioId);
} else {
console.log("push");
Ids.push(...current, portfolioId);
}
console.log(Ids);
this.setState({ expandedPortfolios: Ids, expand: !this.state.expand });
}
}
renderGroup={(params) => (
<>
{console.log(params)}
<div style={{ display: "flex", alignItems: "center", height: "20px", marginTop: '15px' }}>
{params.group != dropdownGroupTags.all && <IconButton size="small" onClick={() => this.handleExpand(params.group)}>
{(!(this.state.expandedPortfolios || []).includes(this.getPortfolioIdByName(params.group)) ?
<ExpandLessIcon /> : <ExpandMoreIcon />)}
</IconButton>}
{params.group != dropdownGroupTags.all ? <WarehouseIcon /> : <AccountBalanceIcon />}
<Button onClick={() => params.group != "Unportfolio" && this.handleCompanyChange(this.createCompanyGroupChangeText(params.group), params.group)}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
{params.group}
</Typography>
</Button>
<div style={{
marginLeft: 'auto',
marginRight: '30px'
}}>
<Typography align="right" variant="subtitle1" sx={{ fontWeight: 'bold' }}>
{params.group != dropdownGroupTags.all && `${(params.children || []).length} Unit`}
</Typography>
</div>
</div>
{/* <Typography variant="subtitle1" sx={{fontWeight: 'bold'}}>{(params.children || []).length} Unit</Typography> */}
<br />
{(this.state.expandedPortfolios || []).includes(this.getPortfolioIdByName(params.group)) ? params.children : []}
</>
)}

Related

react-table : How can I keep visible globalFilteredRows after editing my data?

I'm using v7 of react-table.
My issue is I have can set global filter but when i update my data it shows all my data and don't keep my filtered data.
How can I keep my filtered data visible when i want to update my react-table data?
const TableSearchFilter = ({ preGlobalFilteredRows, globalFilter, setGlobalFilter }) => {
const count = preGlobalFilteredRows.length
return (
<div style={{ textAlign: "right", marginBottom: '10px' }}>
<MaterialUI.FormControl sx={{ m: 1, width: '250px' }} variant="outlined">
<MaterialUI.Input
sx={{ fontSize :'12px' }}
value={globalFilter || ""}
onKeyDown={(e) => {
if (e.keyCode ==13) {
e.preventDefault()
}
}}
onChange={(e) => {
setGlobalFilter(e.target.value)
}}
startAdornment={<i className="fas fa-search" style={{ marginRight:'10px' }}></i>}
placeholder={`${count} référence${count > 0 ? 's' : ''}`}
/>
</MaterialUI.FormControl>
</div>
)
}
in my app.js
<Table columns={columns} data={data} />

Get All TextField values from loop in Next.js when I press Submit button

first of all look at these below screenshots:
There are two tasks which I want to achieve:
There are two questions shown on the page using the array map method, by default I'm showing only one question, and when I press the next part button the second question will appear with the same question and a TextField (multiline). Now I've implemented a word counter in TextField but when I type something in 1st question the counter works properly. But when I go to the next question the here counter shows the previous question's word counter value, I want them to separately work for both questions.
When I click on the next part and again when I click on the previous part then the values from TextField are removed automatically. I want the values there if I navigate to the previous and next part questions. Also, I want to get both TextField values for a form submission when I press the Submit Test button.
Below are my codes for this page. I'm using Next.js and MUI
import { Grid, Typography, Box, NoSsr, TextField } from '#mui/material';
import PersonIcon from '#mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '#mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '#mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '#mui/material/AppBar';
import Toolbar from '#mui/material/Toolbar';
import ArrowBackIosIcon from '#mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '#mui/material/colors';
export default function Writing({ questions }) {
const [show, setShow] = useState(false);
const [show1, setShow1] = useState(true);
const [showQuestionCounter, setShowQuestionCounter] = useState(0);
const [wordsCount, setWordsCount] = useState(0);
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="fixed" style={{ background: blueGrey[900] }}>
<Toolbar>
<Grid container spacing={2} alignItems="center">
<Grid item xs={4} display="flex" alignItems="center">
<PersonIcon
sx={{ background: '#f2f2f2', borderRadius: '50px' }}
/>
<Typography variant="h6" color="#f2f2f2" ml={1}>
xxxxx xxxxx-1234
</Typography>
</Grid>
<Grid item xs={4} container justifyContent="center">
<Timer timeValue={2400} />
</Grid>
<Grid item xs={4} container justifyContent={'right'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Settings
</Button>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0px 10px',
}}
size="small">
Hide
</Button>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Help
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
</Box>
<Box
sx={{
background: blueGrey[50],
height: '100%',
width: '100%',
position: 'absolute',
}}
pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Box
key={question.id}
px={3}
sx={{ background: '#f2f2f2', pb: 4 }}
position={{
xs: 'sticky',
sm: 'sticky',
lg: 'initial',
md: 'initial',
xl: 'initial',
}}>
<Box
style={{ background: '#f7fcff', borderRadius: '4px' }}
py={1}
px={2}>
<Box>
<Typography variant="h6" component="h6" ml={1}>
Part {question.id}
</Typography>
<Typography variant="subtitle2" component="div" ml={1} mt={1}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.questions[0].question, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Box>
</Box>
<Box
style={{
background: '#f7fcff',
borderRadius: '4px',
marginBottom: '75px',
}}
pt={1}
px={3}
mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
<Box
py={{ lg: 1, md: 1, xl: 1 }}
style={{ height: '50vh' }}>
<Scrollbars universal>
<Typography
variant="body1"
component="div"
style={{ textAlign: 'justify' }}
mr={2}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.question_text, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Scrollbars>
</Box>
</Grid>
<Grid
item
xs={12}
sm={12}
lg={6}
md={6}
xl={6}
mt={{ md: 4, lg: 4, xl: 4 }}>
<TextField
id={`${question.id}`}
label="Type your answer here"
multiline
name={`answer_${question.id}`}
rows={12}
variant="outlined"
fullWidth
helperText={`Words Count: ${wordsCount}`}
onChange={(e) => {
setWordsCount(
e.target.value.trim().split(/\s+/).length
);
}}
/>
</Grid>
</Grid>
</Box>
</Box>
) : null
)}
<Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
<Grid
container
style={{ background: blueGrey[300], display: 'flex' }}
py={2}
px={3}>
<Grid
item
xs={3}
sm={3}
lg={6}
md={6}
xl={6}
container
justifyContent={'start'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Save Draft
</Button>
</Grid>
<Grid
item
xs={9}
sm={9}
lg={6}
md={6}
xl={6}
container
justifyContent={'end'}>
<Button
variant="contained"
size="small"
style={{
background: 'white',
color: 'black',
visibility: show1 ? 'visible' : 'hidden',
}}
endIcon={<ArrowForwardIosIcon />}
onClick={() => {
setShow((prev) => !prev);
setShowQuestionCounter(showQuestionCounter + 1);
setShow1((s) => !s);
}}>
Next Part
</Button>
{show && (
<>
<Box>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0 10px',
visibility: show ? 'visible' : 'hidden',
}}
startIcon={<ArrowBackIosIcon />}
size="small"
onClick={() => {
setShow1((s) => !s);
setShowQuestionCounter(showQuestionCounter - 1);
setShow((prev) => !prev);
}}>
previous Part
</Button>
<Button variant="contained" color="success">
Submit Test
</Button>
</Box>
</>
)}
</Grid>
</Grid>
</Box>
</Box>
</>
);
}
export async function getServerSideProps(context) {
const { id } = context.query;
const token = context.req.cookies.token;
if (!token) {
context.res.writeHead(302, {
Location: '/',
});
context.res.end();
}
const res = await axios.get(`test/${id}/questions`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.data.success) {
return {
props: {
questions: res.data.data.questions,
},
};
}
}
The wordsCount state is shared between both questions, which means that when you go to the next question the state remains unchanged and shows the wordsCount from the first question. To solve it, each question needs to have it's own state which you can do by creating a Question component and mapping over it:
export default function Question({ question }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box
// ...
>
{/* ... */}
<TextField
// ...
helperText={`${wordsCount} words`}
onChange={(e) => {
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
Then map over it:
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question key={question.id} question={question} />
) : null
)}
Currently, the value of TextField gets reset you the component is unmounted (i.e. when you go to the next question). You need to make the TextField component a controlled component, meaning that you store the value of the field in useState. And if you need to submit the value of TextField later, then you probably need to store the values in the parent component:
export default function Writing({ questions }) {
// ...
const [answers, setAnswers] = useState([])
function handleChange(id, answer) {
// copy the current answers
let newAnswers = [...answers]
// find the index of the id of the answer in the current answers
const index = newAnswers.findIndex((item) => item.id === id)
// if the answer does exist, replace the previous answer with the new one, else add the new answer
if (index) {
newAnswers[index] = { id, answer }
setAnswers(newAnswers)
} else {
setAnswers([...answers, { id, answer }])
}
}
return (
<>
{/* ... */}
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question
key={question.id}
question={question}
value={answers.find((item) => item.id === question.id)?.answer || ''}
handleChange={handleChange}
/>
) : null
)}
</>
}
In the Question component, add the handler.
export default function Question({ question, value, handleInputChange }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box>
{/* ... */}
<TextField
helperText={`${wordsCount} words`}
value={value}
onChange={(e) => {
handleInputChange(question.id, e.target.value)
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
In the parent component (Writing) you should be able to use the values for form submission.

Conditionally Rendering Components From An Array of Components

I have 3 cards I want to render on the screen all with a similar layout. What is this pattern called when we have a component as a value?
const steps = [
{
label: "Order details",
component: OrderDateStep,
},
{
label: "Driver details",
component: OrderDriverStep,
},
{
label: "Acknowledgements",
component: OrderAcknowledgementStep,
},
];
Additionally I keep running into an issue when these are conditionally rendered. I want to wait until stripe has initialised before displaying the form. However, I get an error Error: Rendered more hooks than during the previous render.. I know I can just add the different components but that isn't very scalable. Is there another way I can achieve this re-usable pattern without running into this issue with the number of hooks changing? Why does using step[x].component() change the number of hooks where just using the component does not?
{stripe && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}
If you want to make sure something is filled, or rendered, before displaying other data in react, you can just do
{
loadedVariable ?
<div>
......
</div>
:null
}
If your question is not fully answered by the point i get home i'll be happy to help you further.
Add one more conditionally into your render component to make sure that steps had filled:
{stripe && steps.length && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}

Accordion - Open first tab from a mapped array - React TS

{accordion.map(accordionItem => (
<AccordionItem
key={accordionItem.title}
text={accordionItem.text}
title={accordionItem.title}
></AccordionItem>
))}
I have an Accordion component that maps over an array of data. I am trying to open just the first tab. There are properties you can add to default expand all or none, but wondering how to do this on the first tab?
Material UI also has customised Accordions but they are focused when all data is in one file and not mapped through an array.
<Accordion className={classes.accordion} defaultExpanded={false}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
style={{
borderBottom: '2px solid #EBEDF7'
}}
>
<Typography className={classes.heading}>{title}</Typography>
</AccordionSummary>
<AccordionDetails>
<Text label={text} className={classes.body} html>
{text}
</Text>
</AccordionDetails>
</Accordion>
This seem to work for me with MUI v5 to open first item (summary) of the accordion
...
summaries.map((summary, index) => (
<MuiAccordion
defaultExpanded={index === 0 ? true : false}
key={summary.id}
>
...
In my case I know what the title of the tabs will be so I was able to do a simple if statement that would open the first tab if the tabs name was as such.
const firstTabCheck =
title === 'Invest some of it' ? (
<Accordion className={classes.accordion} defaultExpanded={true}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
style={{
borderBottom: '2px solid #EBEDF7'
}}
>
<Typography className={classes.heading}>{title}</Typography>
</AccordionSummary>
<AccordionDetails>
<Text label={text} className={classes.body} html>
{text}
</Text>
</AccordionDetails>
</Accordion>
) : (
<Accordion className={classes.accordion} defaultExpanded={false}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
style={{
borderBottom: '2px solid #EBEDF7'
}}
>
<Typography className={classes.heading}>{title}</Typography>
</AccordionSummary>
<AccordionDetails>
<Text label={text} className={classes.body} html>
{text}
</Text>
</AccordionDetails>
</Accordion>
)
return firstTabCheck
I'm sure there is a cleaner way to do this if anyone wants to reply but this has fixed my issue for the time being.
{forms.map((form,index) => {
return (
<Accordion expanded={expanded === `panel_${index}`} onChange={handleChange(`panel_${index}`)}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
{form.status === "pending" && (
<Chip
style={{ margin: '0' }}
size="small"
label={form.status}
color="default"
clickable
icon={<HistoryIcon />}
/>
)}
{form.status === "approved" && (
<Chip
style={{ margin: '0' }}
size="small"
label={form.status}
color="primary"
clickable
icon={<DoneIcon />}
/>
)}
{form.status === "rejected" && (
<Chip
style={{ margin: '0' }}
size="small"
label={form.status}
color="secondary"
clickable
icon={<ClearIcon />}
/>
)}
<Typography className={classes.heading}>{form.typeOfleave}</Typography>
<Typography className={classes.secondaryHeading}>{moment(form.createdAt).format('MMMM Do YYYY, h:mm:ss a')}</Typography>
</AccordionSummary>
<AccordionDetails style={{ display: 'flex', flexDirection: 'column' }}>
<div>
<Typography gutterBottom className={classes.secondaryHeading}>
Reason : {form.reason}
</Typography>
</div>
<div style={{ display: 'flex' }}>
<Typography className={classes.secondaryHeading}>
From : {moment(form.from).subtract(10, 'days').calendar()}
</Typography>
<Typography className={classes.secondaryHeading} style={{ marginLeft: '20px' }}>`enter code here`
To : {moment(form.to).subtract(10, 'days').calendar()}
</Typography>
</div>
</AccordionDetails>
</Accordion>
)
})}
set expanded state (default state) to '0' because array index is starting from 0
const [expanded, setExpanded] = React.useState('panel_0');
const handleChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};

React Conditional Rendering to check if something is an empty tag

I have the following code:
{item && item.description && (
<Link
style={{ display: 'block', paddingBottom: '2px' }}
title="View Details"
onClick={() => setIsOpen(!isOpen)}
{...aria}
>
View Details
<Icon icon={isOpen ? 'collapse_arrow' : 'expand_arrow'}
marginLeft="5px"
verticalAlign="bottom"
/>
</Link>
)}
The problem is that item.description could be an empty tag and therefore the condition is met and a Link component is rendered when it actually shouldn't be.
How is the best way I could avoid this case?
Thanks in advance!
You could use a regex to check for empty tags.
function checkForEmptyTags(tags) {
if (!tags) return false;
const EMPTY_TAGS_REGEX = RegExp('<[^>]*>\s*<\/[^>]*>');
return EMPTY_TAGS_REGEX.test(tags);
}
{item && !checkForEmptyTags(item.description) && (
<Link
style={{ display: 'block', paddingBottom: '2px' }}
title="View Details"
onClick={() => setIsOpen(!isOpen)}
{...aria}
>
View Details
<Icon icon={isOpen ? 'collapse_arrow' : 'expand_arrow'}
marginLeft="5px"
verticalAlign="bottom"
/>
</Link>
)}

Resources