I'm looking to change the way I manage state in my app.
Currently, I am using a mapped component which when selected, will set the index of the card and then use this index to colour the component background blue.
This is great! And it works, however to change from e.g card 1 to 2, I need to tap on card 1 again to set index to 0, then select card 2. I do not know how to change the function so if selected outside the container, set index =0, then set index=1, per a conventional app.
I am managing as such:
const [isSelected, setIsSelected] = useState("");
function handleParamChange(e) {
e.preventDefault();
const param = e.target.name //name may be desc
const value = e.target.value
setParams(prevParams => {
return { ...prevParams, [param]: value}
})
}
With a mapped component of:
{
jobs.length > 0 &&
jobs.map(
(job, index) =>
<JobCard
key={job.id}
job={job}
index={index + 1}
isSelected={isSelected}
setIsSelected={setIsSelected}
/>)
}
const JobCard = ({ setIsSelected, isSelected, index, job }) => {
const [open, setOpen] = useState(false)
const [isActive, setIsActive] = useState(false);
console.log(isSelected)
return (
<CardContainer>
{/* BELOW WORKING */}
{/* <CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined}> */}
<CardPrimary
onClick={() => {
if (!isSelected) {
setIsSelected(index);
setIsActive(true);
} else if (isSelected === index) {
setIsSelected("");
setIsActive(false);
}
}}
style={{
backgroundColor: isActive ? "#0062ff" : "inherit",
display: "flex",
height: "90%",
width: "95%",
borderRadius: "10px",
justifyContent: "center",
flexDirection: "column",
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)"
}}
>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1" color={isActive ? "white" : "inherit"}>
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small"
onClick={()=> setOpen(prevOpen => !prevOpen)}>
<FavoriteIcon fontSize="inherit"
/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color={isActive ? "#d6d6d6" : "text.secondary"}>
{job.company.display_name}
</Typography>
</CardHeader>
</CardContainer>
)
}
export default JobCard
This all works.
And I have no issues, I just want to improve it. So How would you implement react hooks and a ref to automatically assign the state and change the isSelected index based on clicks?
If i understand correct, you only want one card to be selected and also the isActive inside the JobCard should be in sync with the isSelected you pass.
If so, you only need one state variable and to be safe, it should really be the job.id and not the index.
So, instead of const [isSelected, setIsSelected] = useState(""); you should rename it to something like const [selectedJob, setSelectedJob] = useState();
and also handle the toggle click here
const toggleJob = useCallback((jobId)=>{
setSelectedJob( (currentJobId) => currentJobId === jobId ? null : jobId );
},[]);
Then
{
jobs.length > 0 &&
jobs.map(
(job, index) =>
<JobCard
key={job.id}
job={job}
isSelected={job.id === selectedJob}
toggleJob={toggleJob}
/>)
}
and finally
const JobCard = ({ toggleJob, isSelected, job }) => {
const [open, setOpen] = useState(false)
const handleCardClick = useCallback(()=>{
toggleJob(job.id);
},[toggleJob, job])
return (
<CardContainer>
<CardPrimary
onClick={handleCardClick}
style={{
backgroundColor: isSelected ? "#0062ff" : "inherit",
display: "flex",
height: "90%",
width: "95%",
borderRadius: "10px",
justifyContent: "center",
flexDirection: "column",
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)"
}}
>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1" color={isSelected ? "white" : "inherit"}>
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small"
onClick={()=> setOpen(prevOpen => !prevOpen)}>
<FavoriteIcon fontSize="inherit"
/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color={isSelected ? "#d6d6d6" : "text.secondary"}>
{job.company.display_name}
</Typography>
</CardHeader>
</CardContainer>
)
}
export default JobCard
Related
for MUI learning purposes I'm creating a simple CRUD app with a modal. That modal contains a simple form with a few TextField and one Select components. THe issue is, that when clicking on the Select component, the modal closes.
Modal:
<ClickAwayListener
onClickAway={handleClickAway}
>
<Box sx={{ marginTop: '80px' }}>
<Button
sx={{
borderRadius: '8px',
backgroundColor: '#fff',
color: '#091fbb',
border: '1px solid #091fbb'
}}
onClick={handleOpen}
>
Add new
</Button>
<Modal
hideBackdrop
open={open}
onClose={handleClose}
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
border: '1px solid #b9c2ff',
borderRadius: '8px',
height: 'fit-content',
width: 400,
boxShadow: 2,
}}
>
<form
onSubmit={handleSubmit}
style={{
display: 'flex',
flexDirection: 'column',
paddingTop: '12px',
paddingLeft: '18px',
paddingRight: '18px',
paddingBottom: '30px',
}}
>
<Typography variant='h6' sx={{ my: 2, textAlign: 'center' }}>ADD NEW PARTICIPANT</Typography>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Fullname</Typography>
<TextField
variant='standard'
value={fullname}
onChange={(e) => setFullname(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Gender</Typography>
<Select
variant='standard'
value={gender}
MenuProps={{
onClick: e => {
e.preventDefault();
}
}}
onChange={(e) => setGender(e.target.value)}
>
<MenuItem value="None"><em>None</em></MenuItem>
<MenuItem value='Male'>Male</MenuItem>
<MenuItem value='Female'>Female</MenuItem>
<MenuItem value='Other'>Other</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Email</Typography>
<TextField
variant='standard'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Phone nr</Typography>
<TextField
variant='standard'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Description</Typography>
<TextField
variant='standard'
value={description}
onChange={(e) => setDescription(e.target.value)}
multiline
rows={3}
/>
</FormControl>
{ !isLoading && <Button
variant='contained'
type='submit'
sx={{
backgroundColor: '#091fbb'
}}>
Add participant
</Button>}
{ isLoading && <Button
variant='contained'
type='submit'
disabled
sx={{
backgroundColor: '#091fbb'
}}>
Adding participant...
</Button>}
</form>
</Modal>
</Box>
</ClickAwayListener>
Handler functions and states for Modal:
const [open, setOpen] = useState(false);
const [fullname, setFullname] = useState('');
const [gender, setGender] = useState('None');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [description, setDescription] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleOpen = () => {
setOpen(!open);
};
const handleClose = () => {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setOpen(false);
};
const handleClickAway = (e) => {
if (!e.target.classList.contains('MuiMenuItem-root')) {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setOpen(false);
}
};
const handleSubmit = (e) => {
e.preventDefault();
const newParticipant = { fullname, gender, email, phone, description };
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newParticipant)
};
setIsLoading(true);
fetch('http://localhost:8000/participants', requestOptions)
.then(() => {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setIsLoading(false);
setOpen(!open);
})
};
Could anyone advise on how to solve this? Adding MenuProps to prevent default behavior on the Select component and the if statement in handleClickAway function didnt help in my case, even though that helped other who were facing the same issue.
Assuming that the goal is to have Select work in Modal without closing it, perhaps the default behavior of Modal could be enough and use of ClickAwayListener may be not be necessary.
Instead of styling Modal directly with the sx prop, try wrap the modal content in a Box and style this container. This preserves the default behavior of Modal, so that clicking on Select would not trigger the closing of it.
Since Modal internally detect click on the backdrop to close itself, consider to style the backdrop with a transparent background instead of disabling it, so that the use of ClickAwayListener could also be omitted.
Demo of simplified example on: stackblitz (excluded all data handling)
<Modal
open={open}
onClose={handleClose}
// 👇 Style the backdrop to be transparent
slotProps={{ backdrop: { sx: { background: "transparent" } } }}
>
<Box
// 👇 Style the container Box for modal content
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
backgroundColor: "#fff",
border: "1px solid #b9c2ff",
borderRadius: "8px",
height: "fit-content",
width: 400,
boxShadow: 2,
}}
>
{/* Modal content here */}
</Box>
</Modal>
This happens because the menu is by default mounted in the DOM outside of the modal HTML hierarchy; the Select component uses a Menu component which in turn uses a Popper component. Looking at the API documentation for Popper:
The children will be under the DOM hierarchy of the parent component.
disablePortal:bool = false
A simple solution is to override this default in MenuProps, which will cause the component to be rendered as a child to the Modal and will no longer trigger the ClickAwayListener callback. I'm not aware of any downsides to this approach.
<Select
variant='standard'
value={gender}
MenuProps={{
disablePortal: true, // <--- HERE
onClick: e => {
e.preventDefault();
}
}}
onChange={(e) => setGender(e.target.value)}
> . . . </Select>
I need to change a Mui Stepper ( which the code works perfetly )
but what I need is a bit different ,
Instead of having this :
I want to get the text under the icon and instead of having a line between tow steps I prefer to have a '<'
Here is the code :
import Box from '#mui/material/Box';
import Stepper from '#mui/material/Stepper';
import Step from '#mui/material/Step';
import StepButton from '#mui/material/StepButton';
import Button from '#mui/material/Button';
import Typography from '#mui/material/Typography';
const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];
export default function HorizontalNonLinearStepper() {
const [activeStep, setActiveStep] = React.useState(0);
const [completed, setCompleted] = React.useState<{
[k: number]: boolean;
}>({});
const totalSteps = () => {
return steps.length;
};
const completedSteps = () => {
return Object.keys(completed).length;
};
const isLastStep = () => {
return activeStep === totalSteps() - 1;
};
const allStepsCompleted = () => {
return completedSteps() === totalSteps();
};
const handleNext = () => {
const newActiveStep =
isLastStep() && !allStepsCompleted()
? // It's the last step, but not all steps have been completed,
// find the first step that has been completed
steps.findIndex((step, i) => !(i in completed))
: activeStep + 1;
setActiveStep(newActiveStep);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleStep = (step: number) => () => {
setActiveStep(step);
};
const handleComplete = () => {
const newCompleted = completed;
newCompleted[activeStep] = true;
setCompleted(newCompleted);
handleNext();
};
const handleReset = () => {
setActiveStep(0);
setCompleted({});
};
return (
<Box sx={{ width: '100%' }}>
<Stepper nonLinear activeStep={activeStep}>
{steps.map((label, index) => (
<Step key={label} completed={completed[index]}>
<StepButton color="inherit" onClick={handleStep(index)}>
{label}
</StepButton>
</Step>
))}
</Stepper>
<div>
{allStepsCompleted() ? (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Box sx={{ flex: '1 1 auto' }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</React.Fragment>
) : (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: '1 1 auto' }} />
<Button onClick={handleNext} sx={{ mr: 1 }}>
Next
</Button>
{activeStep !== steps.length &&
(completed[activeStep] ? (
<Typography variant="caption" sx={{ display: 'inline-block' }}>
Step {activeStep + 1} already completed
</Typography>
) : (
<Button onClick={handleComplete}>
{completedSteps() === totalSteps() - 1
? 'Finish'
: 'Complete Step'}
</Button>
))}
</Box>
</React.Fragment>
)}
</div>
</Box>
);
}
Is there a way to override the MUI Stepper styles ?
Thank you in advance
Okay so basically it was 2 steps. The first one was to make the labels appear below the icons which was relatively easy.
I had to add alternativeLabel as a prop to the <Stepper />.
The next step was to remove the lines and replace them with < which wasn't straightforward. I did that by styling the .MuiStepConnector class, replacing its content and removing the border.
<Stepper
nonLinear
alternativeLabel
activeStep={activeStep}
sx={{
".MuiStepConnector-root": {
top: 0
},
".MuiStepConnector-root span": {
borderColor: "transparent"
},
".MuiStepConnector-root span::before": {
display: "flex",
justifyContent: "center",
content: '"<"'
}
}}
>
This is the result:
I'm learning Material UI. I am struggling with one problem.
I am using Stepper component and for mobile devices I want add props alternativeLabel which will make the labels will be under the circle.
I got lost. Could someone show me how to achieve it ?
Thank you for any help ! ❤️
export default function ProgressBarStepper({ step }) {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(step);
const [skipped, setSkipped] = React.useState(new Set());
const isStepSkipped = (step) => {
return skipped.has(step);
};
// how to have alternativeLabel only for mobile ??
//connector={false}
return (
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep} connector={<QontoConnector />} >
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step
sx={{
"& .MuiStepLabel-root .Mui-completed": {
fontWeight: "700",
},
}}
key={label}
{...stepProps}
>
<StepLabel
className={classes.boolspac}
sx={{ fontWeight: "700", padding: "0" }}
{...labelProps}
>
{label}
</StepLabel>
</Step>
);
})}
</Stepper>
</Box>
);
}
Objective is to have an array with captured pokemons if user clicks on the input and an array of not-captured pokemons if user un-clicks the input. I've managed to filter out the pokemon when it's no longer captured and have it in the not-captured array but I can't seem to delete that pokemon from the old captured array.
Eg. If I click on "bulbasaur", "charmender", "squirtle", I get them all in the captured array. If I then remove one of them, I correctly get the removed one in the not-captured array but I can't seem to delete it from the previous captured array.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import PokemonIcon from "./PokemonIcon";
const PokemonCard = ({ pokemon, capturedPkm, setCapturedPkm, notCapturedPkm, setNotCapturedPkm }) => {
const [label, setLabel] = useState('Not captured')
const toggleCaptured = (checked, id) => {
const pokemonName = { id: pokemon.id, name: pokemon.name }
if (checked && id === pokemon.id) {
setCapturedPkm([...capturedPkm, pokemonName])
setLabel('Captured!')
} else {
setLabel('Not captured!')
setNotCapturedPkm([...notCapturedPkm, pokemonName])
if (checked === false) {
let newArr = [...capturedPkm]
let pkmRemoved = newArr.filter((el, i) => el.id === id)
let newArrPkm = newArr.splice(pkmRemoved, 1)
console.log('newArr',newArrPkm, 'pkmRemoved', pkmRemoved)
setCapturedPkm(newArrPkm)
}
}
}
useEffect(() => {
console.log('captured', capturedPkm, 'not captured', notCapturedPkm)
}, [capturedPkm, notCapturedPkm])
return (
<>
<div
className="pokemon-card"
style={{
height: "250px",
maxWidth: "250px",
margin: "1rem",
boxShadow: "5px 5px 5px 4px rgba(0, 0, 0, 0.3)",
cursor: "pointer",
}}
>
<Link
to={{ pathname: `/pokemon/${pokemon.id}` }}
style={{ textDecoration: "none", color: "#000000" }}
state={{ pokemon: pokemon, capturedPkm }}
>
<div
style={{
padding: "20px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<PokemonIcon img={pokemon.sprites?.['front_default']} />
</div>
</Link>
<div style={{ textAlign: "center" }}>
<h1>{pokemon.name}</h1>
<label>
<input type="checkbox"
defaultChecked={false}
value={pokemon.name}
onChange={(e) => toggleCaptured(e.target.checked, pokemon.id)} />
<span style={{ marginLeft: 8, cursor: 'pointer' }}>
{label}
</span>
</label>
</div>
</div>
<div></div>
</>
);
};
export default PokemonCard;
I guess you forgot to update the notCapturedPkm array. You could try something like this :
if (checked && id === pokemon.id) {
setCapturedPkm([...capturedPkm, pokemonName])
// Update this array, by removing the selected pokemon
setNotCapturedPkm([...notCapturedPkm.filter(pkm => pkm.id !== pokemon.id)])
setLabel('Captured!')
}
I am trying to change the color only of a button, which is clicked. And by default the first button to be active. The problem that I have is that in Material UI when I use Button there is a span so I cannot use e.target.name ... because there is no name in the span. This span is created when I type some text between the button tags => Some title. As well I intend to have some other actions when the button is clicked, except that it should change its color to show, which one is active.
If there is some way around I will appreciate it. Down below is some code, that I tried, but I do not know what to do in clickedButtonHandler and if it's possible to pass any argument on it when the button is clicked... for example the name.
import React, { useState } from "react";
import { Container, Box, Button } from "#material-ui/core";
import { makeStyles, withStyles } from "#material-ui/styles";
const StyledButton = withStyles(() => ({
root: {
marginRight: "1rem",
width: "25%",
padding: "1rem",
fontSize: "1.2rem",
borderRadius: "1rem",
color: "#000",
fontWeight: "400",
textTransform: "capitalize"
}
}))(Button);
const useStyles = makeStyles(() => ({
buttonContainerWrapper: {
display: "flex",
justifyContent: "center"
},
buttonContainer: {
backgroundColor: "#ccc",
border: "1px solid #000",
padding: "1rem",
display: "flex",
justifyContent: "space-between"
},
lastButtonFilter: {
marginRight: "0rem"
},
activeButton: {
background: "#fc7303",
color: "#fff"
}
}));
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState({
first: true,
second: false,
third: false,
fourth: false
});
const clickedButtonHandler = (e) => {
console.log(e.target);
const { name } = e.target;
setActiveButton(name);
console.log(activeButton);
};
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
<StyledButton
name="button-one"
className={activeButton?.first ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button One
</StyledButton>
<StyledButton
name="button-two"
className={
activeButton?.second ? `${classes.activeButton}` : ""
}
onClick={clickedButtonHandler}
>
Button Two
</StyledButton>
<StyledButton
name="button-three"
className={activeButton?.third ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Three
</StyledButton>
<StyledButton
name="button-four"
className={
activeButton?.fourth ? `${classes.activeButton}` : ""
}
onClick={clickedButtonHandler}
>
Button Four
</StyledButton>
</Box>
</Container>
);
}
here is the link to codepan: https://codesandbox.io/s/awesome-sinoussi-u3o3s
It looks like you can also loop through an array for the buttons
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState("button-one");
const clickedButtonHandler = (name) => {
setActiveButton(name);
};
const buttons = ["button-one", "button-two", "button-three", "button-four"];
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
{buttons.map((name) => (
<StyledButton
name={name}
className={activeButton === name ? `${classes.activeButton}` : ""}
onClick={() => clickedButtonHandler(name)}
>
{name}
</StyledButton>
))}
</Box>
</Container>
);
}
for targetting the root element use the ButtonBase component.
And also to keep track of active buttons in useState, spread the old state first and then update the new value of the new variable (might differ in different use case or requirement). I've updated that issue.
export default function Filter() {
const classes = useStyles();
const [activeButton, setActiveButton] = useState('first');
const clickedButtonHandler = (e) => {
console.log(e.target);
const { name } = e.target;
setActiveButton(name);
console.log(activeButton);
};
return (
<Container className={classes.buttonContainerWrapper}>
<Box className={classes.buttonContainer}>
<StyledButton
name="first"
className={activeButton === "first" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button One
</StyledButton>
<StyledButton
name="second"
className={activeButton === "second" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Two
</StyledButton>
<StyledButton
name="third"
className={activeButton === "third" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Three
</StyledButton>
<StyledButton
name="fourth"
className={activeButton === "fourth" ? `${classes.activeButton}` : ""}
onClick={clickedButtonHandler}
>
Button Four
</StyledButton>
</Box>
</Container>
);
}
Workign demo:-