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)
Related
I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;
I'm building an application, where there is a form presented with different steps. In all the steps but one, I manage to provide the necessary functions as props to make some operations such as 'handleNext', 'handleBack' or 'handleChange'.
Nevertheless, in the last step, represented in the class SuccessForm, when I try to execute the 'handleDownload' function, I get the following error:
TypeError: this.props.handleDownload is not a function
Here it is the SuccessForm.js class:
export class SuccessForm extends Component {
constructor() {
super();
}
download = e => {
e.preventDefault();
this.props.handleDownload();
}
render() {
return (
<React.Fragment>
<Grid container>
<Grid item xs={12} sm={2}>
<DirectionsWalkIcon fontSize="large" style={{
fill: "orange", width: 65,
height: 65
}} />
</Grid>
<Grid>
<Grid item xs={12} sm={6}>
<Typography variant="h5" gutterBottom>
Route created
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1">
Your new track was succesfully created and saved
</Typography>
</Grid>
</Grid>
<Tooltip title="Download" arrow>
<IconButton
variant="contained"
color="primary"
style={{
marginLeft: 'auto',
// marginRight: '2vh'
}}
onClick={this.download}
>
<GetAppIcon fontSize="large" style={{ fill: "orange" }} />
</IconButton>
</Tooltip>
</Grid>
</React.Fragment>
)
}
}
The entire NewRouteForm.js:
import React, { Component } from 'react'
import { makeStyles, MuiThemeProvider } from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import DataForm from '../stepper/dataform/DataForm';
import ReviewForm from '../stepper/reviewform/ReviewForm';
import MapForm from '../stepper/mapform/MapForm';
import NavBar from '../../graphic interface/NavBar';
import DirectionsWalkIcon from '#material-ui/icons/DirectionsWalk';
import Avatar from '#material-ui/core/Avatar';
import CheckCircleOutlineOutlinedIcon from '#material- ui/icons/CheckCircleOutlineOutlined';
import FilterHdrIcon from '#material-ui/icons/FilterHdr';
import Grid from '#material-ui/core/Grid';
import SuccessForm from '../stepper/success/SuccessForm';
import { withStyles } from '#material-ui/styles';
export class NewRouteForm extends Component {
state = {
activeStep: 0,
name: '',
description: '',
date: new Date(),
photos: [],
videos: [],
points: []
};
handleNext = () => {
const { activeStep } = this.state;
this.setState({ activeStep: activeStep + 1 });
};
handleBack = () => {
const { activeStep } = this.state;
this.setState({ activeStep: activeStep - 1 });
};
handleChange = input => e => {
this.setState({ [input]: e.target.value });
}
handleDateChange = date => {
this.setState({ date: date });
}
handleMediaChange = (selectorFiles: FileList, code) => { // this is not an error, is TypeScript
switch (code) {
case 0: // photos
this.setState({ photos: selectorFiles });
break;
case 1: // videos
this.setState({ videos: selectorFiles });
break;
default:
alert('Invalid media code!!');
console.log(code)
break;
}
}
handleMapPoints = points => {
this.setState({ points: points })
}
// ###########################
// Download and Upload methods
// ###########################
handleDownload = () => {
// download route
console.log("DOWNLOAD")
alert("DOWNLOAD");
}
upload = () => {
// upload route
}
render() {
const { activeStep } = this.state;
const { name, description, date, photos, videos, points } = this.state;
const values = { activeStep, name, description, date, photos, videos, points };
const { classes } = this.props;
return (
<MuiThemeProvider>
<React.Fragment>
<NavBar />
<main className={classes.layout}>
<Paper className={classes.paper}>
<Avatar className={classes.avatar}>
<FilterHdrIcon fontSize="large" />
</Avatar>
<Typography component="h1" variant="h4" align="center">
Create your own route
</Typography>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<React.Fragment>
{activeStep === steps.length ? (
<SuccessForm />
) : (
<React.Fragment>
{getStepContent(activeStep,
values,
this.handleNext,
this.handleBack,
this.handleChange,
this.handleDateChange,
this.handleMediaChange,
this.handleMapPoints,
this.handleDownload
)}
</React.Fragment>
)}
</React.Fragment>
</Paper>
</main>
</React.Fragment>
</MuiThemeProvider>
)
}
}
const steps = ['Basic data', 'Map', 'Review your route'];
function getStepContent(step,
values,
handleNext,
handleBack,
handleChange,
handleDateChange,
handleMediaChange,
handleMapPoints,
handleDownload) {
switch (step) {
case 0:
return <DataForm
handleNext={handleNext}
handleChange={handleChange}
handleDateChange={handleDateChange}
handleMediaChange={handleMediaChange}
values={values}
/>;
case 1:
return <MapForm
handleNext={handleNext}
handleBack={handleBack}
handleMapPoints={handleMapPoints}
values={values}
/>;
case 2:
return <ReviewForm
handleNext={handleNext}
handleBack={handleBack}
values={values}
/>;
case 3:
return <SuccessForm
handleDownload={handleDownload}
/>;
default:
throw new Error('Unknown step');
}
}
const useStyles = theme => ({
layout: {
width: 'auto',
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
[theme.breakpoints.up(600 + theme.spacing(2) * 2)]: {
width: 600,
marginLeft: 'auto',
marginRight: 'auto',
},
},
paper: {
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
padding: theme.spacing(2),
[theme.breakpoints.up(600 + theme.spacing(3) * 2)]: {
marginTop: theme.spacing(6),
marginBottom: theme.spacing(6),
padding: theme.spacing(3),
},
},
stepper: {
padding: theme.spacing(3, 0, 5),
},
buttons: {
display: 'flex',
justifyContent: 'flex-end',
},
button: {
marginTop: theme.spacing(3),
marginLeft: theme.spacing(1),
},
avatar: {
marginLeft: 'auto',
marginRight: 'auto',
backgroundColor: theme.palette.warning.main,
},
icon: {
width: 65,
height: 65,
},
grid: {
marginLeft: theme.spacing(2),
}
});
export default withStyles(useStyles)(NewRouteForm);
Try calling super(props) in the constructor:
constructor(props) {
super(props);
}
and passing function with this instance (this.handleDownload) as it is a class property:
<SuccessForm handleDownload={this.handleDownload} />
Update:
You have a bug on the last step when you not passing a property:
activeStep === steps.length ? <SuccessForm handleDownload={this.handleDownload}/>
Assuming that you have a class in your parent Component, what you're missing is the this keyword in the function reference...
case 3:
return <SuccessForm
handleDownload={this.handleDownload}
/>;
In my Reactjs app, I need to have a parent component (a wizard) named Wizard.js and a number of child components (steps of the wizard) named PrimaryForm.js, SecondaryForm.js etc. They all are Class based components with some local validation functions.
Previous and Next buttons to advance the steps, reside in the Wizard.js.
To advance the next step of the wizard, I'm trying to call a method from PrimaryForm. I checked similar questions in Stackoverflow; tried using ref or forwardRef, but I could not make it work. I currently receive "TypeError: Cannot read property 'handleCheckServer' of null" error.
Below are my parent and child classes. Any help about what I would be doing wrong is appreciated.
Wizard.js:
import React, { Component } from 'react';
...
const getSteps = () => {
return [
'Info',
'Source Details',
'Target Details',
'Configuration'
];
}
class Wizard extends Component {
constructor(props) {
super(props);
this.firstRef = React.createRef();
this.handleNext = this.handleNext.bind(this);
this.state = {
activeStep: 1,
}
}
componentDidMount() {}
handleNext = () => {
if (this.state.activeStep === 1) {
this.firstRef.current.handleCheckServer(); <<<<<<<<<<<<<<<<< This is where I try to call child method
}
this.setState(state => ({
activeStep: state.activeStep + 1,
}));
};
handleBack = () => {
this.setState(state => ({
activeStep: state.activeStep - 1,
}));
};
handleReset = () => {
this.setState({
activeStep: 0,
});
};
render() {
const steps = getSteps();
const currentPath = this.props.location.pathname;
const { classes } = this.props;
return (
<React.Fragment>
<CssBaseline />
<Topbar currentPath={currentPath} />
<div className={classes.root}>
<Grid container spacing={2} justify="center" direction="row">
<Grid container spacing={2} className={classes.grid} justify="center" direction="row">
<Grid item xs={12}>
<div className={classes.topBar}>
<div className={classes.block}>
<Typography variant="h6" gutterBottom>Wizard</Typography>
<Typography variant="body1">Follow the wizard steps to create a configuration.</Typography>
</div>
</div>
</Grid>
</Grid>
<Grid container spacing={2} alignItems="center" justify="center" className={classes.grid}>
<Grid item xs={12}>
<div className={classes.stepContainer}>
<div className={classes.bigContainer}>
<Stepper classes={{ root: classes.stepper }} activeStep={this.state.activeStep} alternativeLabel>
{steps.map(label => {
return (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</div>
<PrimaryForm ref={this.firstRef} />
</div>
</Grid>
</Grid>
<Grid container spacing={2} className={classes.grid}>
<Grid item xs={12}>
<div className={classes.flexBar}>
<Tooltip title="Back to previous step">
<div>
<Button variant="contained"
disabled={(this.state.activeStep === 0)}
className={classes.actionButton}
onClick={this.handleBack}
size='large'>
<BackIcon className={classes.rightIcon} />Back
</Button>
</div>
</Tooltip>
<Tooltip title="Proceed the next step">
<div>
<Button
variant="contained" className={classes.actionButton}
color="primary"
size='large'
disabled={!(!this.state.isFormValid || this.state.isTestWaiting)}
onClick={this.handleNext}>
<ForwardIcon className={this.props.classes.rightIcon}/>Next</Button>
</div>
</Tooltip>
<Tooltip title="Cancel creating new configuration">
<Button variant="contained" color="default" className={classes.actionButton}
component={Link} to={'/configs'} style={{ marginLeft: 'auto' }}>
<CancelIcon className={classes.rightIcon} />Cancel
</Button>
</Tooltip>
</div>
</Grid>
</Grid>
</Grid>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(Wizard));
PrimaryForm.js:
import React, { Component } from 'react';
...
class PrimaryForm extends Component {
constructor(props) {
super(props);
this.handleCheckServer = this.handleCheckServer.bind(this);
this.state = {
hostname: {
value: "localhost",
isError: false,
errorText: "",
},
serverIp: {
value: "127.0.0.1",
isError: false,
errorText: "",
},
isFormValid: true,
isTestValid: true,
testErrorMessage: "",
isTestWaiting: false,
};
}
componentDidMount() { }
handleCheckServer() {
alert('Alert from Child. Server check will be done here');
}
evaluateFormValid = (prevState) => {
return ((prevState.hostname.value !== "" && !prevState.hostname.isError) &&
(prevState.serverIp.value !== "" && !prevState.serverIp.isError));
};
handleChange = event => {
var valResult;
switch (event.target.id) {
case 'hostname':
valResult = PrimaryFormValidator.validateHostname(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
hostname:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
},
});
break;
case 'serverIp':
valResult = PrimaryFormValidator.validateIpAddress(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
serverIp:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
}
});
break;
default:
}
this.setState(prevState => ({
...prevState,
isFormValid: this.evaluateFormValid(prevState),
}));
}
render() {
const { classes } = this.props;
return (
<React.Fragment>
<div className={classes.bigContainer}>
<Paper className={classes.paper}>
<div>
<div>
<Typography variant="subtitle1" gutterBottom className={classes.subtitle1} color='secondary'>
Primary System
</Typography>
<Typography variant="body1" gutterBottom>
Information related with the primary system.
</Typography>
</div>
<div className={classes.bigContainer}>
<form className={classes.formArea}>
<TextField className={classes.formControl}
id="hostname"
label="FQDN Hostname *"
onChange={this.handleChange}
value={this.state.hostname.value}
error={this.state.hostname.isError}
helperText={this.state.hostname.errorText}
variant="outlined" autoComplete="off" />
<TextField className={classes.formControl}
id="serverIp"
label="Server Ip Address *"
onChange={this.handleChange}
value={this.state.serverIp.value}
error={this.state.serverIp.isError}
helperText={this.state.serverIp.errorText}
variant="outlined" autoComplete="off" />
</form>
</div>
</div>
</Paper>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(PrimaryForm));
(ps: I would like to solve this without another framework like Redux, etc if possible)
Example in Typescript.
The idea is that the parent passes its callback to the child. The child calls the parent's callback supplying its own e.g. child callback as the argument. The parent stores what it got (child callback) in a class member variable and calls it later.
import * as React from 'react'
interface ICallback {
(num: number): string
}
type ChildProps = {
parent_callback: (f: ICallback) => void;
}
class Child extends React.Component {
constructor(props: ChildProps) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback: ICallback = (num: number) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
readonly state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun: ICallback) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback: ICallback | undefined = undefined;
}
P.S.
The same code in Javascript. The only difference is that interface, type, readonly and type annotations are taken out. Pasting into here confirms it's a valid ES2015 stage-2 code.
class Child extends React.Component {
constructor(props) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback = (num) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback = undefined;
}
My component data is fetched based on the route entered - /reports/:id i.e. /reports/1
the "1" following the /reports/ is retrieved by match.params.id which I then make a dispatch call to the following url:
fetchDashData(`http://ee-etap.devops.fds.com/api/etap/v1/templates/template/report/${match.params.id}`)
When the user enter an invalid id, i.e. /reports/a - I want to redirect the user back to /reports which displays a landing page and error message, as such:
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
This all works fine until when the user try to visit a valid 'id', i.e. /reports/1 right after the erroneous one - /reports/a, in which the user is immediately redirected back to the /reports page because the fetch call is asynchronous and haven't finished loading the data for /reports/1.
I already have isLoading state defined.. but how can I prevent this from happening?
ReportsDashboard.jsx ( /reports/:id)
class ChartsDashboard extends React.Component {
componentDidMount() {
const { fetchDashData, data, isLoading, hasErrored, match } = this.props;
if ( match.params && match.params.id ) {
fetchDashData(`http://ee-etap.devops.fds.com/api/etap/v1/templates/template/report/${match.params.id}`);
}
}
render() {
const { data, hasErrored, isLoading, classes, match } = this.props;
if ( isLoading ) {
return (
<div style={{ margin: '0 auto', textAlign: 'center' }}>
<CircularProgress size={50} color="secondary" />
</div>
);
}
if ( data && data !== null ) {
const { TemplateReport } = data;
const {
errorBarChart, historyTriggers, historyLineChart, jobs, lastBuildDonutChart, features,
} = TemplateReport;
if (errorBarChart.length === 0) {
// error in data
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
}
const keys = [];
errorBarChart.forEach((errorItem) => {
Object.keys(errorItem).forEach((errorKey) => {
if (errorKey !== 'category') {
keys.push(errorKey);
}
});
});
if (match.params.id) {
return (
<div className="page-container">
<Grid container spacing={24}>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Error By Categories</h4>
<div style={{ height: '350px' }}>
<ResponsiveBar
data={errorBarChart}
keys={keys}
indexBy="category"
margin={{
top: 50,
right: 50,
bottom: 50,
left: 50,
}}
padding={0.1}
colors="paired"
colorBy="id"
axisBottom={{
orient: 'bottom',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'CATEGORY',
legendPosition: 'middle',
legendOffset: 36,
}}
axisLeft={{
orient: 'left',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'ERROR COUNT',
legendPosition: 'middle',
legendOffset: -40,
}}
labelSkipWidth={12}
labelSkipHeight={12}
labelTextColor="inherit:darker(1.6)"
animate
motionStiffness={90}
motionDamping={15}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Pass Rate %</h4>
<div style={{ height: '350px' }}>
<ResponsivePie
colors="paired"
colorBy={this.pieColors}
margin={{
top: 40,
right: 40,
bottom: 40,
left: 40,
}}
data={lastBuildDonutChart}
animate
defs={[
linearGradientDef('gradientRed', [{ offset: 0, color: 'red' }, { offset: 100, color: '#ffcdd2', opacity: 0.3 }]),
linearGradientDef('gradientYellow', [{ offset: 0, color: 'yellow' }, { offset: 100, color: '#f7bf18a3', opacity: 0.3 }]),
linearGradientDef('gradientGreen', [{ offset: 0, color: '#38da3e' }, { offset: 100, color: '#38da3e', opacity: 0.3 }]),
]}
fill={[
{ match: { id: 'Fail' }, id: 'gradientRed' },
{ match: { id: 'Pass' }, id: 'gradientGreen' },
{ match: { id: 'Undefined' }, id: 'gradientYellow' },
]}
radialLabelsSkipAngle={10}
radialLabelsTextXOffset={6}
radialLabelsTextColor="#333333"
radialLabelsLinkOffset={0}
radialLabelsLinkDiagonalLength={8}
radialLabelsLinkHorizontalLength={7}
radialLabelsLinkStrokeWidth={1}
radialLabelsLinkColor="inherit"
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12} lg={4}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Jobs Triggered</h4>
<JobsTable data={jobs} templateId={match.params.id} />
</Paper>
</Grid>
<Grid item xs={12} lg={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Scenarios Table</h4>
<Tooltip title="Scenario Report">
<a href={`/reports/${match.params.id}/scenarioHistory`} rel="noopener noreferrer">
<IconButton aria-label="Scenario Report">
<AssignmentIcon />
</IconButton>
</a>
</Tooltip>
<ScenariosTable data={features} />
</Paper>
</Grid>
<Grid item xs={12} lg={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Execution History</h4>
<div style={{ height: '400px' }}>
<ResponsiveLine
colors="paired"
colorBy="id"
margin={{
top: 20,
right: 20,
bottom: 60,
left: 80,
}}
data={historyLineChart}
enableArea={true}
animate
yScale={{ type: 'linear', stacked: true }}
/>
</div>
</Paper>
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
<h4 className={classes.heading}>Previous Builds</h4>
<PreviousBuildsTable data={historyTriggers} templateId={match.params.id}/>
</Paper>
</Grid>
</Grid>
</div>
);
}
}
// error in data
return <Redirect to={{
pathname: '/reports',
state: { templateId: match.params.id } }}
/>;
}
}
const mapStateToProps = state => ({
data: state.reports.data,
hasErrored: state.reports.hasErrored,
isLoading: state.reports.isLoading,
});
const mapDispatchToProps = dispatch => ({
fetchDashData: url => dispatch(chartDataFetch(url)),
});
export default compose(
withStyles(styles),
withRouter,
connect(
mapStateToProps,
mapDispatchToProps,
),
)(ChartsDashboard);
BrowseReport.jsx (/reports/)
class BrowseReports extends React.Component {
constructor(props) {
super(props);
this.state = {
searchVal: '',
errorMsg: '',
}
this.onSearchChange = this.onSearchChange.bind(this);
this.goToTemplateReport = this.goToTemplateReport.bind(this);
}
componentDidMount() {
if (this.props.location && this.props.location.state && this.props.location.state.templateId) {
this.state.errorMsg = `Template Name "${this.props.location.state.templateId}" does not exist, please try again`;
this.props.history.replace('/reports', null);
}
}
onSearchChange(val) {
this.setState({ searchVal: val });
}
goToTemplateReport(val) {
this.props.history.push(`/reports/${val}`);
}
render() {
const { classes, location } = this.props;
const { searchVal, errorMsg } = this.state;
return (
<div className="page-container" style={{ textAlign: 'center' }}>
<Grid container justify="center" spacing={24}>
<Grid item xs={12} lg={8}>
{/* dashData Error */}
<h4 className={classes.errorMsg}>
{/* ERROR MESSAGE HERE */}
{errorMsg}
</h4>
<SearchBar
value={this.state.searchVal}
placeholder='Search for Template Name'
onChange={(value) => this.onSearchChange(value)}
onRequestSearch={(value) => this.goToTemplateReport(value)}
style={{
margin: '0 auto',
}}
/>
</Grid>
<Grid item xs={12} lg={6}>
<Paper className={classes.paper}>
<CompletedJobsTable></CompletedJobsTable>
</Paper>
</Grid>
<Grid item xs={12} lg={6}>
<Paper className={classes.paper}>
<ActiveJobsTable></ActiveJobsTable>
</Paper>
</Grid>
</Grid>
</div>
)
}
}
export default compose(
withStyles(styles),
withRouter
)(BrowseReports);
actions.jsx
export const chartDataHasErrored = hasErrored => ({
type: CHARTS_DATA_HAS_ERRORED,
payload: { hasErrored },
});
export const chartDataIsLoading = isLoading => ({
type: CHARTS_DATA_IS_LOADING,
payload: { isLoading },
});
export const chartDataFetchSuccess = data => ({
type: CHARTS_DATA_FETCH_SUCCESS,
payload: { data },
});
export const chartDataFetch = url => (dispatch) => {
dispatch(chartDataIsLoading(true));
fetch(url, { mode: 'cors' })
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then(response => response.json())
.then((items) => {
dispatch(chartDataFetchSuccess(items));
})
.catch((error) => {
dispatch(chartDataHasErrored(error));
});
};
reducers.jsx
import { CHARTS_DATA_FETCH_SUCCESS, CHARTS_DATA_IS_LOADING, CHARTS_DATA_HAS_ERRORED } from '../../../store/actions';
const INITIAL_STATE = {
hasErrored: null,
isLoading: true,
data: {},
}
const reportsDashboardReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case CHARTS_DATA_HAS_ERRORED:
return {
...state,
hasErrored: action.payload.hasErrored,
isLoading: false,
};
case CHARTS_DATA_IS_LOADING:
return {
...state,
isLoading: action.payload.isLoading,
hasErrored: null,
};
case CHARTS_DATA_FETCH_SUCCESS:
return {
...state,
isLoading: false,
data: action.payload.data,
};
default:
return state;
}
};
export default reportsDashboardReducer;
You need to save templateId in the global state (set it when data loaded). In component data need to be shown only if templateId from the path equal to the templateId from the global state.
I am using 'react-form-validator-core' package and trying to create a custom form validator that implements 'mui-downshift', a Material UI implementation of PayPal's downshift. This question is mostly about 'react-form-validator-core' package itself. The problem is that the form itself does not register the validator component I've created. Here is my full code of the custom component and the form itself. I've exhausted my debugging skills, but what I noticed is that there's something wrong with the this.context in the form...
Validator component:
import React from 'react';
import PropTypes from 'prop-types';
import MuiDownshift from 'mui-downshift';
import { ValidatorComponent } from 'react-form-validator-core';
class AutocompleteValidator extends ValidatorComponent {
constructor(props) {
debugger;
super(props);
this.originalItems = props.items.map(({key, name}) => ({ text: name, value: key }));
this.handleStateChange = this.handleStateChange.bind(this);
this.errorText = this.errorText.bind(this);
}
componentWillMount() {
if (!this.filteredItems) {
this.setState({filteredItems: this.originalItems});
}
if (!!this.props.value) {
const selectedItem = this.originalItems.filter(
item => item.value.toLowerCase().includes(this.props.value.toLowerCase())
)[0];
this.setState({ selectedItem })
} else {
this.setState({ selectedItem: null})
}
}
componentWillReceiveProps(nextProps) {
// If no filteredItems in sate, get the whole list:
if (!nextProps.value) {
this.setState({ isValid: false })
}
}
handleStateChange(changes) {
// If searching
if (changes.hasOwnProperty('inputValue')) {
const filteredItems = this.originalItems.filter(
item => item.text.toLowerCase().includes(changes.inputValue.toLowerCase())
);
this.setState({ filteredItems })
}
// If something is selected
if (changes.hasOwnProperty('selectedItem')) {
!!changes.selectedItem ? this.setState({isValid: true}) : this.setState({isValid: false})
// If we get undefined, change to '' as a fallback to default state
changes.selectedItem = changes.selectedItem ? changes.selectedItem : '';
this.props.onStateChange(changes);
}
}
errorText() {
const { isValid } = this.state;
if (isValid) {
return null;
}
return (
<div style={{ color: 'red' }}>
{this.getErrorMessage()}
</div>
);
}
render() {
return (
<div>
<MuiDownshift
{...this.props}
items={this.state.filteredItems}
onStateChange={this.handleStateChange}
ref={(r) => { this.input = r; }}
defaultSelectedItem={this.state.selectedItem}
/>
{this.errorText()}
</div>
);
}
}
AutocompleteValidator.childContextTypes = {
form: PropTypes.object
};
export default AutocompleteValidator;
A component where it's used:
render() {
return (
<ValidatorForm
ref='form'
onSubmit={() => {
this.context.router.history.push(this.props.config.urls['step5']);
}}
onError={errors => console.log(errors)}
>
<Row>
<Col md={12}>
<AutocompleteValidator
validators={['required']}
errorMessages={['Cette information doit ĂȘtre renseignĂ©e']}
isRequired={true}
name='bankId'
items={this.props.config.list.bank}
onStateChange={(changes) => {
this.props.loansUpdate('bankId', changes.selectedItem.value);
}}
value={!!this.props.loans.bankId ? this.props.loans.bankId : false}
/>
</Col>
</Row>
<Row>
<Col md={12} style={{ marginTop: '15px' }}>
<Checkbox
label={<Translate value='step4.insuranceProvidedByBank' />}
labelStyle={{ 'top': '0px' }}
name='insuranceProvidedByBank'
value={this.props.loans.insuranceProvidedByBank}
checked={this.props.loans.insuranceProvidedByBank}
onCheck={(event, value) => {
this.props.loansUpdate('insuranceProvidedByBank', value);
}}
/>
</Col>
</Row>
<Row>
<Col sm={6} md={5}>
<Button
fullWidth
style={{ marginTop: '50px', marginBottom: '20px' }}
type='submit'
icon={<i className='fa fa-angle-right' style={{ marginLeft: '10px', display: 'inline-block' }} />}
><Translate value='iContinue' /></Button>
</Col>
</Row>
</ValidatorForm >
)
};
};
you get this problem because you override default componentWillMount and componentWillReceiveProps methods from ValidatorComponent. So, to solve this case you should do something like this
componentWillMount() {
// should call parent method before
super.componentWillMount();
// your code
}