So I've looked everywhere for a solution, with no luck.
I am trying to pass a parent React component's this.state.nextID to the child component as a property. However, when I try to access that property in the child, it is null. I am using Material-UI for react and I think the problem is with the withStyles function, because when I inspect the source of the page, I see the key property on the withStyles(ServerBlock) node. But then there is a child of that node that is ServerBlock, with no key property. What am I doing wrong?
ConfigBlock.js
class ConfigBlock extends Component {
constructor () {
super()
this.state = {
children: [],
nextID: 0
}
this.handleChildUnmount = this.handleChildUnmount.bind(this);
}
handleChildUnmount = (key) => {
console.log(key)
this.state.children.splice(key, 1);
this.setState({children: this.state.children});
}
addServerBlock() {
this.state.children.push({"id": this.state.nextID, "obj": <ServerBlock unmountMe={this.handleChildUnmount} key={this.state.nextID} />})
this.setState({children: this.state.children})
this.state.nextID += 1
}
addUpstreamBlock() {
this.state.children.push({"id": this.state.nextID, "obj": <UpstreamBlock unmountMe={this.handleChildUnmount} key={this.state.nextID} />})
this.setState({children: this.state.children})
this.state.nextID += 1
}
render () {
const {classes} = this.props;
return (
<div className={classes.container}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="primary">
Config
</Typography>
<div>
{this.state.children.map((child, index) => {
return (child.obj);
})}
</div>
</CardContent>
<CardActions>
<Button variant="contained" color="primary" className={classes.button} onClick={ this.addServerBlock.bind(this) }>
Server
<AddIcon />
</Button>
<Button variant="contained" color="primary" className={classes.button} onClick={ this.addUpstreamBlock.bind(this) }>
Upstream
<AddIcon />
</Button>
</CardActions>
</Card>
</div>
);
}
}
ConfigBlock.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(ConfigBlock);
ServerBlock.js
class ServerBlock extends Component {
constructor (props) {
super(props)
this.state = {
children: []
}
}
addServerBlock() {
this.state.children.push(<NginxEntry/>)
this.setState({children: this.state.children})
}
deleteMe = () => {
this.props.unmountMe(this.props.key);
}
render () {
const {classes} = this.props;
return (
<div className={classes.container}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="primary">
Server
</Typography>
</CardContent>
<CardActions>
<Button variant="contained" color="primary" className={classes.button} onClick={() => { console.log('onClick'); }}>
Key/Value
<AddIcon />
</Button>
<Button variant="contained" color="primary" className={classes.button} onClick={() => { console.log('onClick'); }}>
Location
<AddIcon />
</Button>
<Button variant="contained" color="primary" className={classes.button} onClick={() => { console.log('onClick'); }}>
Comment
<AddIcon />
</Button>
<Button variant="contained" color="primary" className={classes.button} onClick={ this.deleteMe }>
<DeleteIcon />
</Button>
</CardActions>
</Card>
</div>
);
}
}
ServerBlock.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(ServerBlock);
key is a special React attribute, it is not a prop, i.e. the child will never have access to it's value. If the child needs to use that value, provide it via another prop (as well as via the key), e.g.
<ServerBlock
unmountMe={this.handleChildUnmount}
key={this.state.nextID}
id={this.state.nextID}
/>
...Aside from that, your code is quite unusual. You should not be directly mutating state (you should always use setState), and you wouldn't normally store whole components in your state. Just as food for thought here's an alternative (untested) implementation of your ConfigBlock component that uses setState and shifts around some of the logic a bit:
class ConfigBlock extends Component {
constructor () {
super()
this.state = {
nextID = 0,
children: [],
}
this.handleChildUnmount = this.handleChildUnmount.bind(this);
// bind this function once here rather than creating new bound functions every render
this.addBlock = this.addBlock.bind(this)
}
handleChildUnmount = (key) => {
console.log(key)
this.setState(state => {
return {
// `state.children.splice(key, 1)`, aside from mutating the state,
// will not work as expected after the first unmount as the ids and
// array positions won't stay aligned
children: state.children.slice().filter(child => child.id !== key)
}
})
}
// Consolidate the two addBlock functions, given we're determining the type
// of component to render in the render function.
addBlock(blockType) {
this.setState(state => {
return {
children: [...state.children, { id: state.nextID, type: blockType }]
nextID: state.nextID + 1
}
})
}
render () {
const {classes} = this.props;
return (
<div className={classes.container}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.title} color="primary">
Config
</Typography>
<div>
{this.state.children.map(child => {
// determine the component to render here rather than in the handlers
if (child.type === 'server') {
return <ServerBlock key={child.id} id={child.id} unmountMe={this.handleChildUnmount(child.id)} />
} else if (child.type === 'upstream') {
return <UpstreamBlock key={child.id} id={child.id} unmountMe={this.handleChildUnmount(child.id)} />
}
})}
</div>
</CardContent>
<CardActions>
<Button variant="contained" color="primary" className={classes.button} onClick={this.addBlock('server')}>
Server
<AddIcon />
</Button>
<Button variant="contained" color="primary" className={classes.button} onClick={this.addBlock('upstream')}>
Upstream
<AddIcon />
</Button>
</CardActions>
</Card>
</div>
);
}
}
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'm receiving a cannot read props of undefined. I'm trying to destructure props but I need the hook calls. Is there a way for me to destructure props in a function or another way to resolve this issue?
ProductBrowse.jsx is formatting the products:
const ProductBrowse = () => {
const { id, name, img, store, price, desc, inCart } = this.props.product;
const [open, setOpen] = React.useState(false);
const openModal = () => {
setOpen(!open);
};
const closeModal = () => {
setOpen(!open);
};
return (
<Box border={1} borderRadius={3}>
<Card>
<CardActionArea>
<ProductModal
open={open}
onClick={() => openModal()}
onClose={() => closeModal()}
onSave={() => closeModal()}
productName={name}
productDesc={desc}
/>
<CardHeader
title={name}
subheader={formatCurrency(price)}
/>
<CardMedia
image={img}
alt={desc}
/>
<CardContent>
<Typography variant='body2' color='textSecondary' component='p'>
{desc}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button size='small' /*To Checkout*/>BUY NOW</Button>
<Button
size='small'
onClick={() => {
console.log('Added to Cart');
}}
>
ADD TO CART
</Button>
<Button size='small'>REVIEW</Button>
</CardActions>
</Card>
</Box>
);
}
You can convert your class based component to a functional component like this:
const ProductBrowse = ({ product }) => {
const { id, name, img, store, price, desc, inCart } = product;
...
}
export default ProductBrowse;
As you can see, the product props are being destructured. The entire props object is available if you were to provide more props and want to use them as well.
i.e.
const ProductBrowse = (props) => {
const { id, name, img, store, price, desc, inCart } = props.product;
...
}
export default ProductBrowse;
You are trying to use hooks in class based components. Please refer converted functional component
const ProductBrowse = props => {
const { id, name, img, store, price, desc, inCart } = props.product;
const [open, setOpen] = useState(false);
const classes = useStyles();
const openModal = () => {
setOpen(!open);
};
const closeModal = () => {
setOpen(!open);
};
return (
<Box border={1} borderRadius={3}>
<Card>
<CardActionArea>
{<ProductModal
open={open}
onClick={() => openModal()}
onClose={() => closeModal()}
onSave={() => closeModal()}
productName={name}
productDesc={desc}
/> }
<CardHeader title={name} subheader={formatCurrency(price)} />
<CardMedia image={img} alt={desc} />
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">
{desc}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button size="small" /*To Checkout*/>BUY NOW</Button>
<Button
size="small"
onClick={() => {
console.log("Added to Cart");
}}
>
ADD TO CART
</Button>
<Button size="small">REVIEW</Button>
</CardActions>
</Card>
</Box>
);
};
Also while using this components pass product as it's props as you are destructuring in ProductBrowse component. It should be like this:
<ProductBrowse products={this.products} />
I'm trying to access "props" from a component for which I'm passing an object. I'm a bit lost with JS here ; basically what I'm trying to do is to build a Master/Detail view (so show/hide 2 different components based on user clicks on a table).
How can I access "props" from the object rowEvent once a user clicks on a table row ?
const rowEvents = {
onClick: (e, row, rowIndex) => {
console.log(row.itemId);
//this.currentItemId= row.itemId; //////////// THIS DOESNT WORK...
}
};
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
return (
<Card>
<CardBody>
<h4 className="header-title">Search and Export</h4>
<p className="text-muted font-14 mb-4">A Table</p>
<ToolkitProvider
bootstrap4
keyField="itemId"
data={props.data}
columns={columns}
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}>
{props => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton {...props.csvProps} className="btn btn-primary">
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
rowEvents={ rowEvents }
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
And the component looks like this :
render() {
let show;
if (this.props.currentItemId === null){
show = (<TableWithSearch data={this.props.data} />)
}
else {
show = (<DisplayItem />)
}
return (
<React.Fragment>
<Row>
<Col>
{ show }
</Col>
</Row>
</React.Fragment>
)
}
}
Your issue is a bit complex because you seem to be needing to update the prop currentItemId from parent's parent.
You can solve your issue by doing the following:
Move the declaration of rowEvents objects in side TableWithSearch functional component.
In TableWithSearch component, receive a callback say updateCurrentItemId from parent which updates the currentItemId in the parent
In parent component, the currentItemId is being passed from parent(again). So maintain a state for it.
TableWithSearch Component
const TableWithSearch = (props) => {
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const {updateCurrentItemId} = props; //<--------- receive the prop callback from parent
const rowEvents = {
onClick: (e, row, rowIndex) => {
console.log(row.itemId);
updateCurrentItemId(row.itemId) // <--------- use a callback which updates the currentItemId in the parent
//this.currentItemId= row.itemId; //////////// THIS DOESNT WORK...
},
};
return (
<Card>
<CardBody>
<h4 className="header-title">Search and Export</h4>
<p className="text-muted font-14 mb-4">A Table</p>
<ToolkitProvider
bootstrap4
keyField="itemId"
data={props.data}
columns={columns}
search
exportCSV={{ onlyExportFiltered: true, exportAll: false }}
>
{(props) => (
<React.Fragment>
<Row>
<Col>
<SearchBar {...props.searchProps} />
</Col>
<Col className="text-right">
<ExportCSVButton
{...props.csvProps}
className="btn btn-primary"
>
Export CSV
</ExportCSVButton>
</Col>
</Row>
<BootstrapTable
{...props.baseProps}
bordered={false}
rowEvents={rowEvents}
defaultSorted={defaultSorted}
pagination={paginationFactory({ sizePerPage: 5 })}
wrapperClasses="table-responsive"
/>
</React.Fragment>
)}
</ToolkitProvider>
</CardBody>
</Card>
);
};
Parent Component
class ParentComp extends React.Component {
state = {
curItemId: this.props.currentItemId
}
updateCurrentItemId = (udpatedCurId) => {
this.setState({
curItemId: udpatedCurId
})
}
render() {
let show;
// if (this.props.currentItemId === null){
if (this.state.curItemId === null){
show = (<TableWithSearch data={this.props.data} updateCurrentItemId={this.updateCurrentItemId}/>)
}
else {
show = (<DisplayItem />)
}
return (
<React.Fragment>
<Row>
<Col>
{ show }
</Col>
</Row>
</React.Fragment>
)
}
}
}
this.props should give you access for class components
In addition you should create a bind to the click function so it can correctly resolve this, in the constuctor of the rowEvent
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 trying to write a function that will save a specific object in an array that the user will click I am unsure of how to grab the data I need from the object
this is the function at the moment:
saveArticle = event => {
const userId = localStorage.getItem("userId")
event.preventDefault();
const article = {
title: this.title,
summary: this.summary,
link: this.link,
image: this.image,
userId: userId
}
console.log(article)
// API.saveArticle()
}
and this is the component where I map through the array
const articleCard = props => {
const { classes } = props
return (
<div>
{props.articles.map((article, i) => {
console.log(article);
return (
<div key={i}>
<Card className={classes.card}>
<CardActionArea>
<CardMedia
className={classes.media}
image={article.image}
title={article.title}
href={article.link}
/>
<CardContent>
<Typography gutterBottom variant="headline">
{article.title}
</Typography>
<Typography component="p">
{article.summary}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button href={article.link} size="small" color="primary">
Read Article
</Button>
<Button onClick={props.saveArticle} size="small" color="primary">
Favorite
</Button>
</CardActions>
</Card>
</div>
)
})}
</div>
)
}
I cant seem to grab the objects properties that I'd like to get and I am pretty lost as too how!
any help would be much appreciated thanks guys!
Try this
<Button onClick={(e) => props.saveArticle(article, e)} size="small" color="primary">
Favorite
</Button>
and
saveArticle = (article, event) => {
const userId = localStorage.getItem('userId');
event.preventDefault();
const article = {
title: article.title,
summary: article.summary,
link: article.link,
image: article.image,
userId: userId
};
console.log(article);
// API.saveArticle()
};
Reference: Passing Arguments to Event Handlers