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.
I have problem tracking updated state from fullcalendar in my custom actions header.
I have ref to fullcalendar in parent component that I'm passing as a prop to this child. Then I render calendar itself. OnClicks work but calApi.getApi().getDate() doesn't update value when It changes.
const Actions: FC<ActionsProps> = ({ calApi }) => {
const { t } = useTranslation();
const onToday = () => calApi.getApi().today();
const changeView = (changedView: CalendarView) => calApi.getApi().changeView(changedView);
const onPrev = () => calApi.getApi().prev();
const onNext = () => calApi.getApi().next();
return (
<Grid container spacing={3} alignItems="center" justifyContent="space-between">
<Grid item>
<Tooltip arrow placement="top" title={t('Previous Day').toString()}>
<IconButton color="primary" onClick={onPrev}>
<ArrowBackTwoToneIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title={t('Today').toString()}>
<IconButton
color="primary"
sx={{
mx: 1
}}
onClick={onToday}
>
<TodayTwoToneIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title={t('Next Day').toString()}>
<IconButton color="primary" onClick={onNext}>
<ArrowForwardTwoToneIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid
item
sx={{
display: { xs: 'none', sm: 'inline-block' }
}}
>
<Typography variant="h3" color="text.primary">
{format(calApi.getApi().getDate(), 'MMMM yyyy')}
</Typography>
</Grid>
<Grid
item
sx={{
display: { xs: 'none', sm: 'inline-block' }
}}
>
{viewOptions.map((viewOption) => {
const Icon = viewOption.icon;
return (
<Tooltip key={viewOption.value} arrow placement="top" title={t(viewOption.label).toString()}>
<IconButton
color={
viewOption.value === calApi.getApi().getCurrentData().currentViewType
? 'primary'
: 'secondary'
}
onClick={() => changeView?.(viewOption.value)}
>
<Icon />
</IconButton>
</Tooltip>
);
})}
</Grid>
</Grid>
);
};
I'm currently using the Data Grid Toolbar (a feature of the Material-UI Data Grid component) because I want the Column Show/Hide component, but I also want to add my own menu item in the form of an IconButton with a Menu that opens when clicked. The issue is when you click said button, the Toolbar appears to re-render, which causes the Menu to lose its anchor and render in the upper left. Is there a special way to get an anchor within the Data Grid Toolbar for the Menu popper to appear in the correct location?
function CustomToolbar() {
return (
<GridToolbarContainer>
<Box
height="65px"
width="100%"
display="flex"
flexDirection="row"
justifyContent="center"
>
<Box width="300px" display="flex" justifyContent="flex-start" alignItems="center">
<GridToolbarColumnsButton sx={{ ml: 2 }} />
</Box>
<Box width="100%" alignSelf="center" textAlign="center">
<Typography sx={{ flex: "1 1 100%" }} variant="h6" component="div">
Title Goes Here
</Typography>
</Box>
<Box width="300px" display="flex" justifyContent="flex-end" alignItems="center">
<Tooltip title="Filter">
<IconButton
color="primary"
component="span"
disabled={loading}
sx={{ mr: 2 }}
onClick={handleMenuClick}
>
<FilterList />
</IconButton>
</Tooltip>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={() => handleClose(menuState, filters)}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
PaperProps={MenuProps}
>
<MenuItem /> //Clipped
<MenuItem /> //Clipped
<MenuItem /> //Clipped
</Menu>
</Box>
</Box>
</GridToolbarContainer>
);
}
You must create the Toolbar component outside of your component that declares the DataGrid, and get the properties you need through the DataGrid's componentsProps property.
GridToolbarCustom Component:
type Props = {
selectionModel: GridSelectionModel;
}
const GridToolbarCustom = ({ selectionModel }: Props) => {
const [anchorElMenu, setAnchorElMenu] = useState<null | HTMLButtonElement>(null);
const openMenu = Boolean(anchorElMenu);
return (
<GridToolbarContainer>
<Grid container item xs>
{/* default buttons */}
<GridToolbarColumnsButton />
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
<GridToolbarExport />
</Grid>
<Grid>
<Button
variant="contained"
size="small"
disabled={selectionModel.length === 0}
startIcon={<MoreVertIcon />}
onClick={(event: MouseEvent<HTMLButtonElement>) => {
setAnchorElMenu(event.currentTarget);
}}
>
Actions
</Button>
<Menu
id="menu-options"
anchorEl={anchorElMenu}
open={openMenu}
onClose={() => {
setAnchorElMenu(null);
}}
>
<MenuItem /> //Clipped
<MenuItem /> //Clipped
<MenuItem /> //Clipped
</Menu>
</Grid>
</GridToolbarContainer>
);
}
export default GridToolbarCustom;
MyComponent:
import GridToolbarCustom from './GridToolbarCustom';
const MyComponent = () => {
const [selectionModel, setSelectionModel] = useState<GridSelectionModel>([]);
return (
<DataGrid
//Clipped
components={{
Toolbar: GridToolbarCustom,
}}
componentsProps={{
toolbar: {
selectionModel,
},
}}
checkboxSelection
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
/>
);
};
I have this component that contains a card and inside this card there are elements and I want to separate them through a vertical line and the problem is that the vertical line does not work.
const useStyles = makeStyles((theme: Theme) =>
createStyles({
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
}
}),
);
const SpaceForm: FC = (props) => {
const classes = useStyles()
const workspaceData = useWorkspaceModule((state) => state.workspace)
console.log("inside component 1: ", workspaceData);
return (
<>
<Grid
container
spacing={3}
>
<Grid
item
lg={8}
md={6}
xs={12}
>
<Card>
<CardHeader title="Name your Workspace:"/>
<CardContent>
<Avatar style={{width: '3.4rem', height: '3.4rem'}} className={classes.orange}>N</Avatar>
{/*llll*/}
<Divider style={{ backgroundColor:'red'}} orientation="vertical" flexItem />
</CardContent>
</Card>
</Grid>
</Grid>
</>
);
};
export default SpaceForm;
You just wrap Avatar inside a flex Box and it will show Divider after Avatar:
<Box display="flex">
<Avatar
style={{ width: "3.4rem", height: "3.4rem" }}
className={classes.orange}
>
N
</Avatar>
{/*llll*/}
<Divider
style={{ backgroundColor: "red" }}
orientation="vertical"
flexItem
/>
</Box>
I have a menu component that pops open in a table. When I copy the material ui example into the cell in the table it works perfectly.
https://codesandbox.io/s/gitl9
The material ui example uses hooks and I want to change it to a class component and use redux.
When I made the change the pop-up menu does not align beside the the button you press anymore.
The anchorEl attribute is responsible for passing the location of the button that has been called.
I added these attributes allow me to move the menu pop but it does not align with button that you click to open the menu.
const options = ["View", "Edit", "Delete"];
const ITEM_HEIGHT = 48;
class ActionsOptionMenu extends Component {
state = { anchorEl: null };
render() {
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
>
<MoreVertIcon />
</IconButton>
<Menu
getContentAnchorEl={null}
anchorOrigin={{
height: "54px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `0 ${padding} 0 ${padding}`,
margin: "0 auto 7px auto"
}}
transformOrigin={{ vertical: "top", horizontal: "right" }}
id='long-menu'
anchorEl={anchorEl}
keepMounted
open={true}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 120
}
}}
>
{options.map(option => (
<MenuItem key={option}>{option}</MenuItem>
))}
</Menu>
</div>
);
}
}
I solved the issue doing it this way.
render() {
const { open } = this.state;
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
buttonRef={node => {
this.anchorEl = node;
}}
onClick={event => this.handleClick(event)}
>
<MoreVertIcon />
</IconButton>
<Popper
open={open}
anchorEl={this.anchorEl}
transition
disablePortal
style={{ zIndex: 100 }}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
id='menu-list-grow'
style={{
zIndex: 1001,
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={event => this.handleClose(event)}
>
<MenuList>
<MenuItem onClick={event => this.handleClose(event)}>
Profile
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
My account
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
Logout
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}