How to pass a function to state in react router v6 - javascript

I want to share state between two routes when I click on the link for one of the routes (NewUser). The state that I want to share and the logic modifying it are both held in the Users route. I want to pass the logic to change the state to the NewUsers route.
When I pass a string to the state object in router Link, I am able to access it in the NewUsers component. However, I get null when I pass a function.
I know that I can use context/redux, but I would prefer if I can do it this way.
Users route:
function Users() {
const [users, setUsers] = useState([]);
return (
<Card sx={{ padding: "2rem", mt: "2rem" }}>
<MDBox
display="flex"
flexDirection="row"
justifyContent="space-between"
>
<MDTypography variant="body2">{`You currently have ${users.length} users`}</MDTypography>
<MDButton variant="gradient" color="info" size="small">
<Link to="/settings/users/new-user" state={setUsers: setUsers}> //this is how I want to pass the state
<MDBox
display="flex"
alignItems="center"
color="white"
fontWeight="normal"
>
<Icon>add</Icon> Add New User
</MDBox>
</Link>
</MDButton>
</MDBox>
</Card>
NewUsers route:
function NewUser({history}) {
const location = useLocation();
const saveChanges = (e) => {
location.state.setUsers({
fname: values.firstName,
lname: values.lname,
email: values.email,
});
navigate("/settings/users");
};
return(
<MDBox py={3} mb={20} height="62vh">
<Grid
container
justifyContent="center"
alignItems="center"
sx={{ height: "100%", mt: 0 }}
>
<Grid item xs={12} lg={12}>
<Formik
initialValues={initialValues}
validationSchema={currentValidation}
onSubmit={(values) => {
setValues(values);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form id={formId} autoComplete="off">
<Card sx={{ height: "100%", width: "100%" }}>
<MDBox px={3} py={4}>
<MDBox display="flex">
<ButtonWrapper
fullWidth={false}
handleClick={saveChanges}
>
Save Changes
</ButtonWrapper>
</MDBox>
<MDBox>
{getStepsContent({
values,
touched,
formField,
errors,
})}
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
)
}
Routing code:
{
type: "collapse",
name: "Settings",
key: "settings",
icon: <Icon fontSize="small">settings</Icon>,
collapse: [
{
name: "Users",
key: "users",
route: "/settings/users",
// icon: <Icon fontSize="small">users</Icon>,
component: <Users />,
},
{
name: "Companies",
key: "companies",
route: "/settings/companies",
component: <Companies />,
},
{
name: "Billing",
key: "billing",
route: "/settings/billing",
component: <Billing />,
},
{
name: "Integrations",
key: "integrations",
route: "/settings/integrations",
component: <Integrations />,
},
],
},
{
name: "New User",
key: "new user",
route: "/settings/users/new-user",
noCollapse: true,
component: <NewUser />,
},
{
type: "collapse",
name: "Sign Out",
key: "signout",
route: "/sign-out",
icon: <Icon fontSize="small">logout</Icon>,
component: <SignOut />,
noCollapse: true,
},
];
function that renders the routes:
const getRoutes = (allRoutes) =>
allRoutes.map((route) => {
if (route.collapse) {
return getRoutes(route.collapse);
}
if (route.route) {
return <Route exact path={route.route} element={route.component} key={route.key} />;
}
return null;
});
<Routes>
{getRoutes(routes)}
{/* <Route path="*" element={<Navigate to="/dashboard" />} /> */}
<Route path="*" element={<Console />} />
</Routes>

The state value sent via the Link component needs to be JSON serializable. Javascript functions are not serializable. Instead of trying to pass a function through to a target component I recommend lifting the state up to a common ancestor so the state and callback function is accessible to both components.
I would suggest using a React context to hold the users state and provide out the state value and an updater function to add a user object. react-router-dom has a "built-in" way to do this via a layout route component that renders an Outlet component that wraps nested routes.
Example:
import { Outlet } from 'react-router-dom';
const UsersProvider = () => {
const [users, setUsers] = useState([]);
const addUser = (user) => {
setUsers((users) => users.concat(user));
};
return <Outlet context={{ users, addUser }} />;
};
...
<Routes>
...
<Route path="/settings/users" element={<UsersProvider />}>
<Route index element={<Users />} />
<Route path="new-user" element={<NewUser />} />
</Route>
...
</Routes>
Users
const Users = () => {
const { users } = useOutletContext();
return (
<Card sx={{ padding: "2rem", mt: "2rem" }}>
<Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">
You currently have {users.length} users
</Typography>
<Button variant="gradient" color="info" size="small">
<Link to="/settings/users/new-user">
<Box
display="flex"
alignItems="center"
color="white"
fontWeight="normal"
>
<Icon>add</Icon>
Add New User
</Box>
</Link>
</Button>
</Box>
</Card>
);
};
NewUser
function NewUser({history}) {
const navigate = useNavigate();
const { addUser } = useOutletContext();
const saveChanges = (e) => {
addUser({
fname: values.firstName,
lname: values.lname,
email: values.email,
});
navigate("/settings/users");
};
return(
<MDBox py={3} mb={20} height="62vh">
<Grid
container
justifyContent="center"
alignItems="center"
sx={{ height: "100%", mt: 0 }}
>
<Grid item xs={12} lg={12}>
<Formik
initialValues={initialValues}
validationSchema={currentValidation}
onSubmit={(values) => {
setValues(values);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form id={formId} autoComplete="off">
<Card sx={{ height: "100%", width: "100%" }}>
<MDBox px={3} py={4}>
<MDBox display="flex">
<ButtonWrapper
fullWidth={false}
handleClick={saveChanges}
>
Save Changes
</ButtonWrapper>
</MDBox>
<MDBox>
{getStepsContent({
values,
touched,
formField,
errors,
})}
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
)
}

Related

How to send/receive props to BasicLayout (#devexpress/dx-react-scheduler)

I'm from Angular and new to React. Im doing well but here is a problem I'm stuck at. As you can see I have BasicLayout and AppointmentForm, both are in one file. BasicLayout is being used inside AppointmentForm but not like an element i.e <BasicLayout/> so I'm not able to understand how to pass props or its even possible now. I want to trigger commitChanges(inside AppointmentForm) function when onSubmit(inside Basic Layout) function is triggered. How can I pass props between these components?
const BasicLayout = (props) => {
const formik = useFormik({
initialValues: {
title: '',
agenda: '',
description: '',
participants: [],
host: user?.id,
guest: '',
location: '',
},
validationSchema,
onSubmit: async (values) => {
values.startDate = props.appointmentData.startDate;
values.endDate = props.appointmentData.endDate;
values.guest = values.guest?._id;
createAppointment(values);
console.log(values);
},
});
return (
<Container>
<Typography sx={{ fontSize: 24, fontWeight: 'bold' }} color="text.primary" gutterBottom>
Create Appointment
</Typography>
<Box sx={{ flexGrow: 1 }}>
<FormikProvider value={formik}>
<Form autoComplete="off" onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Grid item xs={6} md={6}>
<TextField
label="Title"
color="secondary"
id="title"
type="text"
key="title"
value={formik.values.title}
onChange={formik.handleChange}
{...getFieldProps('title')}
error={Boolean(touched.title && errors.title)}
helperText={touched.title && errors.title}
fullWidth
/>
</Grid>
<Grid item container xs={12} md={12} direction="row" justifyContent="center" alignItems="center">
<LoadingButton size="medium" type="submit" variant="contained" loading={isSubmitting}>
Create
</LoadingButton>
</Grid>
</Grid>
</Form>
</FormikProvider>
</Box>
<ToastContainer />
</Container>
);
};
const AppointmentsDashboard = (props) => {
const commitChanges = ({ added, changed, deleted }) => {
console.log(props);
console.log({ added, changed, deleted });
if (added) {
if (!isValidate) {
notify('Please fill all required fields', 'error');
return;
}
const startingAddedId = data.length > 0 ? data[data.length - 1].id + 1 : 0;
setData([...data, { id: startingAddedId, ...added }]);
}
if (changed) {
setData(
data.map((appointment) =>
changed[appointment.id] ? { ...appointment, ...changed[appointment.id] } : appointment
)
);
}
if (deleted !== undefined) {
setData(data.filter((appointment) => appointment.id !== deleted));
}
return data;
};
return (
<>
<Paper>
<Scheduler data={data} height={660}>
<ViewState currentDate={currentDate} />
<EditingState
onCommitChanges={commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={changeEditingAppointment}
onAppointmentFormClosing={() => {
console.log('asdasd');
}}
allowAdding={true}
/>
<WeekView startDayHour={9} endDayHour={17} />
<AllDayPanel />
<EditRecurrenceMenu />
<ConfirmationDialog />
<Appointments />
<AppointmentTooltip showOpenButton showDeleteButton />
<AppointmentForm basicLayoutComponent={BasicLayout} />
</Scheduler>
</Paper>
</>
);
};
export default AppointmentsDashboard;

React router : update props

I am new to react, I have a component UpdateInvoice which has as props idInvoice and a boolean isNew. this component do two things if isNew is true the component will add a new invoice otherwise, it will update an existing invoice identied by its idInvoice.
I have also ListInvoices component. I want when I click on a button "NEW INVOICE" in this component I will be able to call my UpdateInvoice so I used React Router.
The problem
the props I send from ListInvoices to UpdateInvoice are empty!
in index.tsx
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="UpdateInvoice" element={<UpdateInvoice {...{idInvoice:'', isNew: null}} />} />
</Routes>
</BrowserRouter>,
);
in ListInvoices.tsx
<Button
variant="contained"
className="add"
onClick={() => navigate("/UpdateInvoice", {state: {idInvoice: '', isNew: true }})}
startIcon={<AddIcon />}>
NEW INVOICE
</Button>
I can't send the right props from index.tsx because ListInvoices who has idInvoice and isNew informations
UPDATE :
ListInvoices.tsx
const [data, setData] = React.useState([] as any[]);
//when the user is authenticated using an api, I update headers value
const [headers, setHeaders] = React.useState(
{
'Authorization': ``,
'My-Custom-Header': ''
});
useEffect(() => {
if(headers.Authorization != ''){
getData(); // I fetch data from an api
}
}, [headers])
const columns: GridColDef[] = [
{ field: 'idInvoice', headerName: 'ID', minWidth: 160 },
//more columns..
]
return (
<div>
<Button
variant="contained"
className="add"
onClick={() => navigate("/UpdateInvoice", {state: {idInvoice: '', isNew: true }})}
startIcon={<AddIcon />}>
NEW INVOICE
</Button>
<Paper sx={{width: '100%', p:1}} component="ul">
<div style={{ height: 400, width: '100%'}}>
{data.length != 0 ?
<DataGrid
rows={data}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
/> : <CircularProgress className='progress'/>}
</div>
</Paper>
</div>
);
All I had to do is using useLocation hook
in UpdateInvoice
const location = useLocation();
const [invoiceState, setInvoiceState] = React.useState<InvoiceProps>(location.state as InvoiceProps);
React.useEffect(() => {
setInvoiceState(location.state as InvoiceProps);
}, [location])
location variable contains the state I set here
<Button
variant="contained"
className="add"
onClick={() => navigate("/UpdateInvoice", {state: {idInvoice: '', isNew: true }})}
startIcon={<AddIcon />}>
NEW INVOICE
</Button>
This tutorial helped me

I need a nested route in react router V5

my task is to want a nested route in the access/ route means I have a parent route access/ so I need a nested in this route like /access/add-team this nested I want to do in one click of a button mean I'm my access/ route component I have I one button called Add Team when someone clicks on that button I am pushing to that user on this /access/add-team route so the route is getting change based on click but my add team component is net getting render what I am missing I am not sure I have added that every this in Layout.js file my component are present in Layout.js let me know what I need to add to work fine this also I added complete code link bellow
AppRoutes.js
const Layout = lazy(() => import("./Layout"));
const PageNotFound = lazy(() => import("./PageNotFound"));
const isLoggedIn = true;
const PrivateRoute = ({ component: Component, isLoggedIn }) => {
return (
<Route
render={(props) =>
isLoggedIn ? <Component {...props} /> : <Redirect to="/login" />
}
/>
);
};
export const AppRoutes = () => {
return (
<HashRouter>
<React.Suspense fallback={""}>
<Switch>
<PrivateRoute path="/" isLoggedIn={isLoggedIn} component={Layout} />
<Route
path="*"
name="Not Found"
render={(props) => <PageNotFound {...props} />}
/>
</Switch>
</React.Suspense>
</HashRouter>
);
};
function Layout(props) {
const history = useHistory();
const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const drawer = (
<div>
<Toolbar />
<Divider />
<List sx={{ minWidth: 230 }}>
{newText
?.filter((data) => data.permission)
?.map((value, index) => (
<ListItemButton
key={index}
sx={{ pt: 1, pb: 1, mt: 3.5 }}
onClick={() => history.push(value.route)}
>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary={value.label} />
</ListItemButton>
))}
</List>
<Divider />
</div>
);
const container =
window !== undefined ? () => window().document.body : undefined;
return (
<Box sx={{ display: "flex" }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` }
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: "none" } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Responsive drawer
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
<Drawer
variant="permanent"
sx={{
display: { xs: "none", sm: "block" },
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: drawerWidth
}
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
}}
>
<Toolbar />
<Suspense fallback={""}>
<Switch>
{ROUTES.map((route, idx) => {
return route.component ? (
<Route
key={idx}
path={route.path}
exact={route.exact}
name={route.name}
render={(props) => <route.component {...props} />}
/>
) : null;
})}
<Redirect exact path="/" to="access" />
<Route
path="*"
name="Not Found"
render={(props) => <PageNotFound {...props} />}
/>
</Switch>
</Suspense>
</Box>
</Box>
);
}
Within the Switch component path order and specificity matters! You want to order the routes from more specific paths to less specific paths. In this case you are rendering the "/access" path prior to any of the sub-route "/access/***" paths, so it is matched and rendered instead of the one really matching the path in the URL.
To fix, move the "/access" route config below the more specific routes.
export const ROUTES = [
// move "/access" route from here
{
name: "addTeam",
path: "/access/add-team",
component: lazy(() => import("./AddTeam"))
},
{
name: "addUser",
path: "/access/add-user",
component: lazy(() => import("./AddUser"))
},
// to here
{
name: "access",
path: "/access",
component: lazy(() => import("./Access"))
},
{
name: "admin",
path: "/admin",
component: lazy(() => import("./Admin"))
}
];

MUI Persistent Drawer - Routing Issues

I want to create a persistent drawer using the material-ui library on react. I have created a partial navbar and i was testing if i could route to other pages if i click the buttons. The address routes properly if i click on the buttons, however, i am unable to see the content of those pages. This is my first time working with react in general, so i am not sure where im going wrong and i would really appreciate some assistance.
This is the code i have so far:
App.js:
function App() {
return (
<div>
<Box sx={{display: 'flex'}}>
<NavBar>
<Routes>
<Route exact path='/' element={<Navigate to='/home' />} />
<Route exact path='/home' element={<HomePage />} />
<Route exact path='/clients' element={<ClientsPage />} />
<Route exact path='/resources' element={<ResourcesPage />} />
<Route exact path='/projects' element={<ProjectPage />} />
</Routes>
</NavBar>
</Box>
</div>
)
}
export default App;
index.js:
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
NavBar.js:
const drawerWidth = 240;
const useStyles = makeStyles({
drawer: {
width: drawerWidth
},
drawerPaper: {
width: drawerWidth
},
root: {
display: 'flex'
}
})
export const NavBar = () => {
const classes = useStyles()
const navigate = useNavigate()
const [open, setOpen] = useState(true)
const menuItems = [
{
text: 'Home',
icon: <Home sx={{ color: orange[500] }} />,
path: '/home',
component: <HomePage />
},
{
text: 'Projects',
icon: <Work sx={{ color: orange[500] }} />,
path: '/projects',
component: <ProjectPage />
},
{
text: 'Clients',
icon: <People sx={{ color: orange[500] }} />,
path: '/clients',
component: <ClientsPage />
},
{
text: 'Resources',
icon: <Settings sx={{ color: orange[500] }} />,
path: '/resources',
component: <ResourcesPage />
}
]
return (
<>
<Grid container>
<div className={classes.root}>
<Drawer className={classes.drawer} variant="persistent" anchor="left" open={open} classes={{paper: classes.drawerPaper}}>
<div>
<Typography variant="h5">
Navigation
</Typography>
<List>
{menuItems.map(item => (
<ListItem key={item.text} button onClick={() => navigate(item.path)}>
<ListItemIcon> {item.icon} </ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
))}
</List>
</div>
</Drawer>
</div>
</Grid>
</>
)
}
The HomePage, and all the other pages being called is just a header tag which says Hello . I cannot see the Hello when i click on the buttons. Thank you guys for the help!
After looking at my code for a while, i figured what the error was. The Navbar was routing to the page, which was right, but i was not displaying the contents of the page. The great part was I was unconsciously wrapping my router and all the routes in NavBar component, so all i had to do was pass a prop called children like this:
export const NavBar = ({children}) => {
//code here
}
and then after i finish with the drawer, just add a small div where i display the children. So it would look like this:
</Drawer>
<div>
{children}
</div
The UI is still a bit messy, but the content of the pages are being shown.

React, Formik Field Arrays - mapping over repeatable fields

I'm trying to figure out how to use Formik field arrays in a react project.
I have one form (glossary) that has 3 Field Arrays within it (one for each of relatedTerms, templates and referenceMaterials).
Each of the field arrays is set out in a separate component. When I only used one of them, I had this working. Adding the next one has caused a problem that I can't solve.
My form has:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import {render} from 'react-dom';
import { Link } from 'react-router-dom';
import firebase, {firestore} from '../../../../firebase';
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
Divider,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
Formik, Form, Field, ErrorMessage, FieldArray,
} from 'formik';
import * as Yup from 'yup';
import {
Autocomplete,
ToggleButtonGroup,
AutocompleteRenderInputParams,
} from 'formik-material-ui-lab';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
import RelatedTerms from "./RelatedTerms";
import ReferenceMaterials from "./ReferenceMaterials";
import Templates from "./Templates";
const allCategories = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'},
];
function UpperCasingTextField(props: TextFieldProps) {
const {
form: {setFieldValue},
field: {name},
} = props;
const onChange = React.useCallback(
event => {
const {value} = event.target;
setFieldValue(name, value ? value.toUpperCase() : '');
},
[setFieldValue, name]
);
return <MuiTextField {...fieldToTextField(props)} onChange={onChange} />;
}
function Glossary(props) {
const { classes } = props;
const [open, setOpen] = useState(false);
const [isSubmitionCompleted, setSubmitionCompleted] = useState(false);
function handleClose() {
setOpen(false);
}
function handleClickOpen() {
setSubmitionCompleted(false);
setOpen(true);
}
return (
<React.Fragment>
<Button
// component="button"
color="primary"
onClick={handleClickOpen}
style={{ float: "right"}}
variant="outlined"
>
Create Term
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
{!isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Create a defined term</DialogTitle>
<DialogContent>
<DialogContentText>
</DialogContentText>
<Formik
initialValues={{ term: "", definition: "", category: [], context: "", relatedTerms: [], templates: [], referenceMaterials: [] }}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
firestore.collection("glossary").doc().set({
...values,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(() => {
setSubmitionCompleted(true);
});
}}
validationSchema={Yup.object().shape({
term: Yup.string()
.required('Required'),
definition: Yup.string()
.required('Required'),
category: Yup.string()
.required('Required'),
context: Yup.string()
.required("Required"),
// relatedTerms: Yup.string()
// .required("Required"),
// templates: Yup.string()
// .required("Required"),
// referenceMaterials: Yup.string()
// .required("Required"),
})}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
return (
<form onSubmit={handleSubmit}>
<TextField
label="Term"
name="term"
// className={classes.textField}
value={values.term}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.term && touched.term) && errors.term}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="Meaning"
name="definition"
multiline
rows={4}
// className={classes.textField}
value={values.definition}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="In what context is this term used?"
name="context"
// className={classes.textField}
multiline
rows={4}
value={values.context}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.context && touched.context) && errors.context}
margin="normal"
style={{ width: "100%"}}
/>
<Box margin={1}>
<Field
name="category"
multiple
component={Autocomplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autocomplete'] && !!errors['autocomplete']}
helperText={touched['autocomplete'] && errors['autocomplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<Divider style={{marginTop: "20px", marginBottom: "20px"}}></Divider>
<Box>
<Typography variant="subtitle2">
Add a related term
</Typography>
<FieldArray name="relatedTerms" component={RelatedTerms} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a reference document
</Typography>
<FieldArray name="referenceMaterials" component={ReferenceMaterials} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a template
</Typography>
<FieldArray name="templates" component={Templates} />
</Box>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</Button>
<Button type="submit" disabled={isSubmitting}>
Submit
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</form>
);
}}
</Formik>
</DialogContent>
</React.Fragment>
}
{isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Thanks!</DialogTitle>
<DialogContent>
<DialogContentText>
We appreciate your contribution.
</DialogContentText>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleClose}
>
Close
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</DialogContent>
</React.Fragment>}
</Dialog>
</React.Fragment>
);
}
export default Glossary;
Then, each subform is as follows (but replacing relatedTerms for templates or referenceMaterials).
import React from "react";
import { Formik, Field } from "formik";
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
const initialValues = {
title: "",
description: "",
source: ""
};
class RelatedTerms extends React.Component {
render() {
const {form: parentForm, ...parentProps} = this.props;
return (
<Formik
initialValues={initialValues}
render={({ values, setFieldTouched }) => {
return (
<div>
{parentForm.values.relatedTerms.map((_notneeded, index) => {
return (
<div key={index}>
<TextField
label="Title"
name={`relatedTerms.${index}.title`}
placeholder=""
// className="form-control"
// value={values.title}
margin="normal"
style={{ width: "100%"}}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.title`,
e.target.value
);
}}
>
</TextField>
<TextField
label="Description"
name={`relatedTerms.${index}.description`}
placeholder="Describe the relationship"
// value={values.description}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.description`,
e.target.value
);
}}
// onBlur={handleBlur}
// helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<Button
variant="outlined"
color="secondary"
size="small"
onClick={() => parentProps.remove(index)}
>
Remove this term
</Button>
</div>
);
})}
<Button
variant="contained"
color="secondary"
size="small"
style={{ marginTop: "5vh"}}
onClick={() => parentProps.push(initialValues)}
>
Add a related term
</Button>
</div>
);
}}
/>
);
}
}
export default RelatedTerms;
Then when I try to render the data submitted in the form, I have:
import React, { useState, useEffect } from 'react';
import {Link } from 'react-router-dom';
import Typography from '#material-ui/core/Typography';
import ImpactMetricsForm from "./Form";
import firebase, { firestore } from "../../../../firebase.js";
import { makeStyles } from '#material-ui/core/styles';
import clsx from 'clsx';
import ExpansionPanel from '#material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '#material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '#material-ui/core/ExpansionPanelSummary';
import ExpansionPanelActions from '#material-ui/core/ExpansionPanelActions';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import Chip from '#material-ui/core/Chip';
import Button from '#material-ui/core/Button';
import Divider from '#material-ui/core/Divider';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
marginTop: '8vh',
marginBottom: '5vh'
},
heading: {
fontSize: theme.typography.pxToRem(15),
},
heading2: {
fontSize: theme.typography.pxToRem(15),
fontWeight: "500",
marginTop: '3vh',
marginBottom: '1vh',
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
textTransform: 'capitalize'
},
icon: {
verticalAlign: 'bottom',
height: 20,
width: 20,
},
details: {
alignItems: 'center',
},
column: {
flexBasis: '20%',
},
columnBody: {
flexBasis: '70%',
},
helper: {
borderLeft: `2px solid ${theme.palette.divider}`,
padding: theme.spacing(1, 2),
},
link: {
color: theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
}));
const Title = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "4vw",
marginBottom: '2vh'
};
const Subhead = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "calc(2vw + 1vh + .5vmin)",
marginBottom: '2vh',
marginTop: '8vh',
width: "100%"
};
function useGlossaryTerms() {
const [glossaryTerms, setGlossaryTerms] = useState([])
useEffect(() => {
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
setGlossaryTerms(glossaryTerms)
})
}, [])
return glossaryTerms
}
const GlossaryTerms = () => {
const glossaryTerms = useGlossaryTerms()
const classes = useStyles();
return (
<div style={{ marginLeft: "3vw"}}>
<div className={classes.root}>
{glossaryTerms.map(glossaryTerm => {
return (
<ExpansionPanel defaultcollapsed>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1c-content"
id="panel1c-header"
>
<div className={classes.column}>
<Typography className={classes.heading}>{glossaryTerm.term}</Typography>
</div>
<div className={classes.column}>
{glossaryTerm.category.map(category => (
<Typography className={classes.secondaryHeading}>
{category.label}
</Typography>
)
)}
</div>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.details}>
<div className={clsx(classes.columnBody)}>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Meaning</Typography>
<Typography>{glossaryTerm.definition}</Typography>
</div>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Context</Typography>
<div>
<Typography>{glossaryTerm.context}</Typography>
</div>
<div className={clsx(classes.helper)}>
<div>
<Typography variant="caption">Related Terms</Typography>
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Typography variant="body2" className="blogParagraph" key={relatedTerm.id}>
{relatedTerm.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption">Related Reference Materials</Typography>
{glossaryTerm.referenceMaterials.map(referenceMaterial => (
<Typography variant="body2" className="blogParagraph" key={referenceMaterial.id}>
{referenceMaterial.title}
</Typography>
))}
</div>
</div>
</ExpansionPanelDetails>
<Divider />
<ExpansionPanelActions>
{glossaryTerm.attribution}
</ExpansionPanelActions>
</ExpansionPanel>
)
})}
</div>
</div>
);
}
export default GlossaryTerms;
When I try this using only the relatedTerms field array, I can submit data in the form and render the list.
When I add in the next two Field Array components for Templates and ReferenceMaterials, I get an error that says:
TypeError: glossaryTerm.referenceMaterials.map is not a function
Each of the 3 field arrays is a duplicate, where I've only changed the name of the value in the main form. You can see from the screen shot attached that the data within each map from the form fields is the same for each of relatedTerms, templates and referenceMaterials. When I comment out templates and referenceMaterials from the rendered output, everything renders properly. When I comment out relatedTerms and try to render either templates or referenceMaterials, I get the error I reported.
If I remove the templates and referenceMaterials map statements from the rendered output, I can use the form with all 3 field arrays in it. They save properly in firebase. I just can't display them using the method that works for relatedTerms.
Everything seems ok with your code. I suspect that the problem is in the data coming from firebase in useGlossaryTerms, some entries in the glossary collection may not have referenceMaterials or templates fields (maybe from a previous form submit that did not have those yet).
You could :
Run a migration script on the collection to add defaults for those fields if they don't exist.
Add defaults on client side :
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
...data,
referenceMaterials: data.referenceMaterials || [],
templates: data.templates || []
};
}
setGlossaryTerms(glossaryTerms)
})
On the client side, check if those fields exists before rendering :
{
glossaryTerm.templates ? (
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
) : null
}

Categories

Resources