I'm trying to display a remove icon on a grid display that the users mouse is hovering over.
this.state = {
action: [],
}
<div>
{this.state.action.map((value, index) => {
return (
<div key={index} onMouseEnter={this.removeElementIcon} onMouseLeave={this.hideRemoveElementIcon} className={classes.gridClass}>
<Grid className={classes.marginGrid}>
<Paper className={classes.paddingPaper}>
<Typography variant={"h5"}>{value}</Typography>
<Typography component={"p"}>{value}</Typography>
</Paper>
</Grid>
{this.state.removeElementIcon ?
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={()=> this.removeElement(value)}>
<ClearIcon color={"error"} />
</IconButton>
: null}
</div>
I've tried just returning some jsx from the method itself.
removeElementIcon = () => {
return ({
this.state.removeElementIcon ?
<IconButton className={classes.removeElement} color={"secondary"}
arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton> :
null
});
Instead of:
removeElementIcon = () => {
this.setState({removeElementIcon: true});
};
hideRemoveElementIcon = () => {
this.setState({removeElementIcon: false});
};
Instead of just displaying the clear icon on one element it displays on all elements.
You need to maintain item index in state,
this.state = {
action: [],
hoverIndex: '',
}
Pass the index to your removeElementIcon function,
<div
key={index}
onMouseEnter={() => this.removeElementIcon(index)}
onMouseLeave={hideRemoveElementIcon}
className={classes.gridClass}
>
...
</div>
In your function's set the hoverIndex,
removeElementIcon = (index) => {
this.setState({removeElementIcon: true, hoverIndex: index});
};
hideRemoveElementIcon = () => {
this.setState({removeElementIcon: false, hoverIndex:''});
};
And finally apply the condition,
{this.state.removeElementIcon && this.state.hoverIndex === index ?
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton>
: null
}
or even short way
{this.state.removeElementIcon && this.state.hoverIndex === index &&
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton>
}
Demo with simple button.
Related
Im having troubles to expand and contract a Collapse Component from MaterialUI since Im mapping and array and iterating the same component, when i press the collapse button, all components expands/contracts at the same time ( I suppose that Im not providing an identifier to point where the collapse function should be used),Im currently Using an State to control the collapse action:
const [expanded, setExpanded] = useState(false);
This is the return where I iterate the component using map on RecetasAll object,
return (
<React.Fragment key={RecetasAll.id}>
<Card className="searchItem" sx={{ maxWidth: 345 }}>
<CardHeader
action={<IconButton aria-label="settings"></IconButton>}
title={RecetasAll.titulo}
/>
<h4
className="Dieta"
style={{
backgroundColor: color(RecetasAll.Tiporeceta.tipoReceta),
}}
>
{RecetasAll.Tiporeceta.tipoReceta}
</h4>
<span className="Calorias">{RecetasAll.informacionNutricional}</span>
<CardMedia
component="img"
height="194"
image={RecetasAll.imagen}
alt="Paella dish"
/>
<CardContent>
{RecetasAll.Productos.map((Productos) => {
return (
<React.Fragment key={Productos.id}>
<Typography variant="body2" color="text.secondary">
{Productos.producto}
</Typography>
</React.Fragment>
);
})}
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent id={RecetasAll.id}>
<Typography paragraph>Preparacion:</Typography>
<Typography paragraph>{RecetasAll.pasos}</Typography>
<Button
href="#contained-buttons"
variant="contained"
onClick={handleSearch}
>
Ver mas
</Button>
</CardContent>
</Collapse>
</Card>
</React.Fragment>
);
});
return <>{itemRecetas}</>;
}
Im triying to set an id property to the CardContent since its the child of the Collapse component
id={RecetasAll.id}
this is the function Im using to expand or collapse but I dont know how to get the id properly to compare its value with expanded state:
const handleExpandClick = (e) => {
let clickedItemId = e.currentTarget.id;
if (expanded === clickedItemId) {
setExpanded(!expanded);
} else {
setExpanded(clickedItemId);
}
};
You could refactor every card into a new component and that way you can have a state to open/close the individual card. When iterating you can pass in the RecetasAll.
const MyCard = ({ RecetasAll }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => {
setIsExpanded(prevIsExpanded => !prevIsExpanded);
};
return (
...
<ExpandMore
expand={isExpanded}
onClick={toggleExpanded}
aria-expanded={isExpanded}
>
...
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
...
);
};
If you don't want to use a new component you could store all the ids of the expanded cards in a state. Based on if the id is in the array the card will be expanded or collapsed.
const [expandedIds, setExpandedIds] = useState([]);
const toggleExpanded = (id) => {
setExpandedIds((prevExpandedIds) => {
// if id is already in array remove
if (prevExpandedIds.includes(id))
return prevExpandedIds.filter((i) => i !== id);
// else add to array
return [...prevExpandedIds, id];
});
};
return (
...
<ExpandMore
expand={expandedIds.includes(RecetasAll.id)}
onClick={() => toggleExpanded(RecetasAll.id)}
aria-expanded={expandedIds.includes(RecetasAll.id)}
>
...
<Collapse in={expandedIds.includes(RecetasAll.id)} timeout="auto" unmountOnExit>
...
)
I have the component below where I'm trying the build functionality to allow a user to update opening times of a store.
I'm passing the original opening times as a prop and creating some state using the props opening times for initial state. I want to use the new state to submit changes but if the user selects cancel the UI updates to reflect the original times.
I have most of the functionality working but for some reason my handler to update the state with the input change also seems to update the props value so it won't go back to the original value.
How can I stop the props updating and ensure only the allOpeningHours state is changed?
VIDEO: https://www.veed.io/view/c40bf9e8-7502-408a-ba6d-fd306dbf4b6f?sharingWidget=true
const EditStudioHours: FC<{ studio: Studio }> = ({ studio }) => {
const { value: edit, toggle: toggleEdit } = useBoolean(false)
const { value: submitting, toggle: toggleSubmitting } = useBoolean(false)
const [allOpeningHours, setAllOpeningHours] = useState([
...studio.openingHours.regularDays,
])
return (
<Box>
<Typography variant='h6' mt={2} gutterBottom>
Set standard hours
</Typography>
<Typography fontWeight='light' fontSize={14}>
Configure the standard operating hours of this studio
</Typography>
<Stack mt={3} spacing={2}>
{studio.openingHours.regularDays.map((hours, i) => (
<DayOfWeek
dow={daysOfWeek[i]}
openingHours={hours}
edit={edit}
i={i}
setAllOpeningHours={setAllOpeningHours}
allOpeningHours={allOpeningHours}
/>
))}
</Stack>
<Button
variant={edit ? 'contained' : 'outlined'}
onClick={() => {
toggleEdit()
}}
fullWidth
sx={{ mt: 2 }}
disabled={!edit ? false : submitting}
>
{submitting ? (
<CircularProgress size={22} />
) : edit ? (
'Submit changes'
) : (
'Edit'
)}
</Button>
{edit && (
<Button
onClick={toggleEdit}
variant={'outlined'}
sx={{ mt: 1 }}
fullWidth
>
Cancel
</Button>
)}
</Box>
)
}
export default EditStudioHours
const DayOfWeek: FC<{
openingHours: { start: number; end: number }
dow: string
edit: boolean
i: number
setAllOpeningHours: any
allOpeningHours: any
}> = ({ openingHours, dow, edit, i, setAllOpeningHours, allOpeningHours }) => {
const [open, setOpen] = useState(openingHours.end !== openingHours.start)
const handleOpenClose = () => {
open &&
setAllOpeningHours((ps: any) => {
const newHours = [...ps]
newHours[i].start = 0
newHours[i].end = 0
return newHours
})
setOpen((ps) => !ps)
}
const handleStart = (e: any) => {
setAllOpeningHours((prevState: any) => {
const newHours = [...prevState]
newHours[i].start = e.target.value
return newHours
})
}
const handleEnd = (e: any) => {
setAllOpeningHours((ps: any) => {
const newHours = [...ps]
newHours[i].end = e.target.value
return newHours
})
}
return (
<Box display='flex' alignItems='center' justifyContent={'space-between'}>
<Box display={'flex'} alignItems='center'>
<Typography width={150}>{dow}</Typography>
<FormGroup>
<FormControlLabel
control={
<Switch
disabled={!edit}
checked={open}
onChange={handleOpenClose}
/>
}
label='Open'
/>
</FormGroup>
</Box>
{open && (
<Box display={'flex'} alignItems='center'>
<TextField
disabled={!edit}
id={`${i}open`}
select
label='Open'
value={edit ? allOpeningHours[i].start : openingHours.start}
type='number'
sx={{ minWidth: 120 }}
size='small'
onChange={handleStart}
>
{openingOptions.map((option: { value: number; label: string }) => (
<MenuItem dense key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<Typography mx={2}>TO</Typography>
<TextField
disabled={!edit}
id={`${i}close`}
select
label='Close'
value={edit ? allOpeningHours[i].end : openingHours.end}
type='number'
sx={{ minWidth: 120 }}
size='small'
onChange={handleEnd}
>
{openingOptions.map((option: { value: number; label: string }) => (
<MenuItem dense key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
</Box>
)}
</Box>
)
}
The problem is that objects within the studio.openingHours.regularDays array still share the same reference, even though you copied the array itself.
When you use something like
newHours[i].start = e.target.value
You're still updating the original objects from props.
You can use Array.prototype.splice() to remove the object at index i and replace it with a new one
const day = newHours[i];
newHours.splice(i, 1, {
...day,
start: e.target.value,
});
Do this in each of your 3 handle functions.
Alternately, break all references when creating local state from props
const [allOpeningHours, setAllOpeningHours] = useState(
studio.openingHours.regularDays.map((day) => ({ ...day }))
);
I have an array of items that i want to show with a map function, and every item is shown as a card.
I'm trying to show two kinds of cards with a different content, one if "isHover" is false, and the other if it true using onMouseEnter/onMouseOver.
I made "isHover" as an array in order to know which item to show/hide.
(The "isHover" array has the same length that the items' array has).
The problem is that when I hover one card it dissappears and nothing is shown in place of it. :(
The code:
function TeachersShow(props) {
const [isHover, setIsHover] = useState(null);
const updateIsHover = (index, isHover1) => {
let newArray = isHover;
newArray[index] = isHover1;
setIsHover([...newArray]);
console.log(isHover[index]);
};
return (
<div>
{isHover[index] === false && (<Card className="teacher-card"
onMouseEnter={() => { updateIsHover(index, true) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Img className="teachersImg" src={item.photoURL}>
</Card.Img>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
</Card>)}
{isHover[index] === true && (
<Card className="card-hover"
onMouseleave={() => { updateIsHover(index, false) }}
key={index}
item={item}
onClick={() => navigateToTeacher(item)}
>
<Card.Title className=" teachersName">
{item.username}
</Card.Title>
<Card.Subtitle className="proTeacher">
{`${item.profession} teacher`}
</Card.Subtitle>
<Card.Text className="teacherDesc">
{item.teacher_description}
</Card.Text>
</Card>)}
</Col>
))}
<Col></Col>
</Row>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(TeachersShow);
It's little hard to tell what's wrong, since a lot of information is missing from the code. But try to create a component Let's say <Teacher />, and let it be responsible for hovering action. Try this:
function Teacher(item) {
const [hover, setHover] = useState(false);
const renderCardData = () => {
if (!hover) {
return (
<Card.Img className="teachersImg" src={item.photoURL} />
<Card.Title className="teachersName">
{item.username}
</Card.Title>
);
}
return (
<Card.Title className=" teachersName">
{ item.username }
</Card.Title>
<Card.Subtitle className="proTeacher">
{ `${item.profession} teacher` }
</Card.Subtitle>
<Card.Text className="teacherDesc">
{ item.teacher_description }
</Card.Text>
);
};
return (
<Card
className={ hover
? 'card-hover'
: 'teacher-card' }
onMouseEnter={ () => setHover(true) }
onMouseLeave={ () => setHover(false) }
>
{ renderCardData() }
</Card>
);
}
export default Teacher;
And you render it like that:
function TeachersList(teachers) {
return teachers.map(Teacher);
};
I wish to add spinner animation after clicking on button, when get response, spinner is supposed to disappear. So far works fine but the problem is that I render list with many elements and every element has own delete button, while clicking on one, animation is added to all elements of the list. I wish it to appear only once, next to this particular clicked element of the list.
const displayCertificateList = (
classes,
mainStatus,
handleDeleteSingleCertificate,
animateDelete
) => {
return mainStatus.map((el, i) => {
return (
<div className={classes.certificatesListContainer} style={{border:'none'}}>
<List key={i} style={{padding: '10px'}}>
<ListItem style={{ padding: "0 0 0 20px" }}>
<ListItemText
className={classes.certificatesList}
primary={
<Typography type="body2" style={{ fontWeight: "bold" }} className={classes.certificatesListFont}>
Valid until:
</Typography>
}
secondary={
<Typography
type="body2"
className={classNames(
classes.certificatesListSecondArgument,
classes.certificatesListFont,
el.expiresIn > 90 ? classes.green : classes.red
)}
>
{el.validUntil.slice(0,9)} ({el.expiresIn} days)
</Typography>
}
/>
</ListItem>
</List>
<div className={classes.certificatesBtn}>
<Button
variant="contained"
size="small"
color="secondary"
className={classes.button}
onClick={() => {
if (
window.confirm(
`Are you really sure?
)
)
handleDeleteSingleCertificate(el, i);
}}
>
<DeleteIcon className={classes.leftIcon} />
Delete
</Button>
<div style={{left: '-50%',top: '30%'}} className={classNames(animateDelete ? classes.spinner : null)}></div>
</div>
</div>
);
});
} else {
return (
<div>
<Typography component="h1" variant="h6">
The applet is not innitialized, please initialize it first
</Typography>
</div>
);
};
And in parent component:
handleDeleteSingleCertificate = (el, i) => {
this.setState({animatingDelete: true})
this.make_call(
this.state.selected,
(res) => {
console.log(res)
this.setState({animatingDelete: false})
}
)
}
And pass it like this:
{this.state.view === 'certificates' && this.state.certificates && displayCertificates(classes, fakeData, this.handleDeleteSingleCertificate, this.state.animatingDelete)}
I suggest to make displayCertificateList function component to stateful component and store the animatingDelete in it - `cause it is the state of that particular item in deed.
class ListItem extends React.Component {
state = {
isDeleting: false
}
handleDelete = () => {
const { onDelete, id } = this.props;
onDelete(id);
this.setState({
isDeleting: true
})
}
render(){
const { isDeleting } = this.state;
return (
<li>
<button onClick={this.handleDelete}>Delete {isDeleting && '(spinner)'}</button>
</li>
)
}
}
class List extends React.Component {
state = {
listItems: [
{id: 1},
{id: 2}
]
}
handleDelete = id => {
console.log('delete ' + id);
// do the async operation here and remove the item from state
}
render(){
const { listItems } = this.state;
return (
<ul>
{listItems.map(({id}) => (
<ListItem id={id} key={id} onDelete={this.handleDelete} />
))}
</ul>
)
}
}
ReactDOM.render(<List />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
In my opinion, it's better to use count instead of animatingDelete to mark. You can plus 1 when click on the delete button and then when it's done minus 1. when count equals to 0, hide spining otherwise show it.
I am getting data from the backend using postman. But when i am using frontend for the same, it is not working. I am getting errors like
1.TypeError: Cannot read property 'map' of null
2.Unhandled Rejection (TypeError): Cannot read property 'map' of null.
I think, I am getting this error because cards are not able to render when I am searching.The backend data is coming as a array.
const styles = theme => ({
appBar: {
position: 'relative',
},
icon: {
marginRight: theme.spacing.unit * 2,
},
layout: {
width: 'auto',
marginLeft: theme.spacing.unit * 3,
marginRight: theme.spacing.unit * 3,
[theme.breakpoints.up(1100 + theme.spacing.unit * 3 * 2)]: {
width: 1100,
marginLeft: 'auto',
marginRight: 'auto',
},
},
cardGrid: {
padding: `${theme.spacing.unit * 8}px 0`,
},
card: {
height: '100%',
display: 'flex',
flexDirection: 'column',
},
cardContent: {
flexGrow: 1,
},
});
class Products extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
searchString: ''
};
this.onSearchInputChange = this.onSearchInputChange.bind(this);
this.getProducts = this.getProducts.bind(this);
}
componentDidMount() {
this.getProducts();
}
// delete = id => {
// axios.post('http://localhost:9022/products/delete/' + id)
// .then(res => {
// let updatedProducts = [...this.state.products].filter(i => i.id !== id);
// this.setState({ products: updatedProducts });
// });
// }
delete = id => {
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
this.setState((prevState, prevProps) => {
let updatedProducts = [...prevState.products].filter(i => i.id !== id);
return ({
products: updatedProducts
});
});
});
}
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data }, () => {
console.log(this.state.products);
});
});
}
onSearchInputChange = (event) => {
let newSearchString = '';
if (event.target.value) {
newSearchString = event.target.value;
}
axios.get('http://localhost:9022/products/getproducts' + newSearchString)
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
this.getProducts();
}
// onSearchInputChange(event) {
// let newSearchString = '';
// if (event.target.value) {
// newSearchString = event.target.value;
// }
// // call getProducts once React has finished updating the state using the callback (second argument)
// this.setState({ searchString: newSearchString }, () => {
// this.getProducts();
// });
// }
render() {
const { classes } = this.props;
return (
<React.Fragment>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for products"
margin="normal"
onChange={this.onSearchInputChange} />
<CssBaseline />
<main>
<div className={classNames(classes.layout, classes.cardGrid)}>
<Grid container spacing={40}>
{this.state.products.map(currentProduct => (
<Grid item key={currentProduct.id} sm={6} md={4} lg={3}>
<Card className={classes.card}>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant="h5" component="h2">
{currentProduct.title}
</Typography>
<Typography>
{currentProduct.price}
</Typography>
</CardContent>
<CardActions>
<Button size="small" color="primary" component={Link} to={"/products/" + currentProduct.id}>
Edit
</Button>
<Button size="small" color="primary" onClick={() => this.delete(currentProduct.id)}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</div>
</main>
</React.Fragment>
)
}
}
Products.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Products);
It has been observed that, you have added wrong URL of getproducts, slash is missing in the URL. Please find details below:
If search-string is r, then you are using this URL of getproducts: http://localhost:9022/products/getproductsr
which is wrong and it should be http://localhost:9022/products/getproducts/r
Hence you have to change your code of retrieving products as follows:
axios.get('http://localhost:9022/products/getproducts/' + newSearchString)
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
Also it will be good to provide a check for undefined/null for this.state.products and then render the components because it is possible that products might be null if one provide wrong URL and undefined as axios request is async. Hence by adding 'this.state.products && ' in existing render code will be good to avoid such issues. I have updated your render function, please find it below:
render() {
const { classes } = this.props;
return (
<React.Fragment>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for products"
margin="normal"
onChange={this.onSearchInputChange} />
<CssBaseline />
<main>
<div className={classNames(classes.layout, classes.cardGrid)}>
<Grid container spacing={40}>
{this.state.products && this.state.products.map(currentProduct => (
<Grid item key={currentProduct.id} sm={6} md={4} lg={3}>
<Card className={classes.card}>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant="h5" component="h2">
{currentProduct.title}
</Typography>
<Typography>
{currentProduct.price}
</Typography>
</CardContent>
<CardActions>
<Button size="small" color="primary" component={Link} to={"/products/" + currentProduct.id}>
Edit
</Button>
<Button size="small" color="primary" onClick={() => this.delete(currentProduct.id)}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</div>
</main>
</React.Fragment>
)
}
Hope it will help..
You may try this one:
You just missed the slash in the api end Point as below : Use this
axios.get('http://localhost:9022/products/getproducts/' + newSearchString)
instead of :
axios.get('http://localhost:9022/products/getproducts' + newSearchString)