I have a special TextField wrapper for a Material-UI field that seems to be working well. For some reason when I use native Material UI fields, they do not update the form data upon submission.
Here is the relevant code (but I've added a code sandbox at the end of the post to help demonstrate the issue)
TestForm.tsx
import { Card, CardActions, CardContent } from "#material-ui/core";
import { Form, Formik, Field } from "formik";
import React from "react";
import SubmitButton from "../../SubmitButton";
import CancelButton from "../../CancelButton";
import TextField from "../../TextField";
import MuiTextField from "#material-ui/core/TextField";
interface TestFormProps {
onSave: (fields: FormFields) => void;
onCancel: () => void;
}
export interface FormFields {
textField: string;
numericField: number;
materialField: string;
materialFieldFromComponent: string;
}
const initialValues: FormFields = {
textField: "Default Text",
numericField: 42,
materialField: "Default Mui",
materialFieldFromComponent: "Default Mui from Component"
};
const TestForm: React.FC<TestFormProps> = ({ onCancel, onSave }) => {
return (
<Formik
initialValues={initialValues}
onSubmit={async (fields, { setSubmitting }) => {
setSubmitting(true);
onSave(fields);
}}
>
{({ isSubmitting }) => (
<Form>
<Card>
<CardContent>
<TextField name="textField" label="Text field" />
<TextField
name="numericField"
type="number"
label="Numeric field"
/>
<div>
<MuiTextField
name="materialField"
label="Mui field"
variant="outlined"
margin="dense"
/>
</div>
<Field
name="materialFieldFromComponent"
label="Mui field from Component"
variant="outlined"
margin="dense"
component={MuiTextField}
/>
</CardContent>
<CardActions>
<SubmitButton disabled={isSubmitting} />
<CancelButton onCancel={onCancel} />
</CardActions>
</Card>
</Form>
)}
</Formik>
);
};
export default TestForm;
TextField.tsx
import React from "react";
import { FieldAttributes, useField } from "formik";
import {
InputBaseComponentProps,
TextField as MuiTextField
} from "#material-ui/core";
interface TextFieldProps {
label: string;
inline?: boolean;
inputProps?: InputBaseComponentProps;
}
const Field: React.FC<FieldAttributes<TextFieldProps>> = (props) => {
const { type, label, placeholder, inputProps } = props;
const [field, meta] = useField<TextFieldProps>(props);
const errorText = meta.error && meta.touched ? meta.error : "";
return (
<MuiTextField
{...field}
label={label}
variant="outlined"
margin="dense"
type={type}
placeholder={placeholder ? placeholder : label}
helperText={errorText}
error={!!errorText}
inputProps={inputProps}
/>
);
};
const TextField: React.FC<FieldAttributes<TextFieldProps>> = ({
inline = false,
...props
}) => {
return inline ? (
<Field {...props} />
) : (
<div>
<Field {...props} />
</div>
);
};
export default TextField;
Here's the code sandbox:
https://codesandbox.io/s/recursing-northcutt-5deen
This is because the TextField that you have created passes the correct field properties, while the other direct implementation of the material UI fields do not.
Passing the correct properties as follow, will do the trick:
{({ isSubmitting, handleChange, handleBlur, values }) => (
...
<div>
<MuiTextField
name="materialField"
label="Mui field"
variant="outlined"
margin="dense"
onChange={handleChange}
onBlur={handleBlur}
value={values.materialField}
/>
</div>
<Field
name="materialFieldFromComponent"
label="Mui field from Component"
variant="outlined"
margin="dense"
render={({ field }) => <MuiTextField {...field} />}
/>
...
The MuiTextField now has onChange and values passed on its properties, thus updating the values correctly.
Complete code: TestForm.tsx
import { Card, CardActions, CardContent } from "#material-ui/core";
import { Form, Formik, Field } from "formik";
import React from "react";
import SubmitButton from "../../SubmitButton";
import CancelButton from "../../CancelButton";
import TextField from "../../TextField";
import MuiTextField from "#material-ui/core/TextField";
interface TestFormProps {
onSave: (fields: FormFields) => void;
onCancel: () => void;
}
export interface FormFields {
textField: string;
numericField: number;
materialField: string;
materialFieldFromComponent: string;
}
const initialValues: FormFields = {
textField: "Default Text",
numericField: 42,
materialField: "Default Mui",
materialFieldFromComponent: "Default Mui from Component"
};
const TestForm: React.FC<TestFormProps> = ({ onCancel, onSave }) => {
return (
<Formik
initialValues={initialValues}
onSubmit={async (fields, { setSubmitting }) => {
setSubmitting(true);
onSave(fields);
}}
>
{({ isSubmitting, handleChange, handleBlur, values }) => (
<Form>
<Card>
<CardContent>
<TextField name="textField" label="Text field" />
<TextField
name="numericField"
type="number"
label="Numeric field"
/>
<div>
<MuiTextField
name="materialField"
label="Mui field"
variant="outlined"
margin="dense"
onChange={handleChange}
onBlur={handleBlur}
value={values.materialField}
/>
</div>
<Field
name="materialFieldFromComponent"
label="Mui field from Component"
variant="outlined"
margin="dense"
render={({ field }) => <MuiTextField {...field} />}
/>
</CardContent>
<CardActions>
<SubmitButton disabled={isSubmitting} />
<CancelButton onCancel={onCancel} />
</CardActions>
</Card>
</Form>
)}
</Formik>
);
};
export default TestForm;
Related
i'm quite new with react and i'm building a form with react-hook and useState to manage my datas after the submit.
I'm not able to use textfield as they are blocked. I think that i make some errors into value/onChange parameters but i don't know what type of error.
import React, { useState } from "react";
import {
TextField,
MenuItem,
Typography,
Checkbox,
Divider,
Button,
} from "#mui/material";
import { MdError } from "react-icons/md";
import { BsArrowRight } from "react-icons/bs";
import "../style/contactform.scss";
import { useForm } from "react-hook-form";
const initialState = {
name: "",
email: "",
};
const ContactForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const [state, setState] = useState(initialState);
const { name, email } = state;
const handleInputChange = (e) => {
const { name, value } = e.target;
setState({ ...state, [name]: value });
};
const onSubmit = (e) => {
e.preventDefault();
console.log("form submit");
setState(initialState);
};
return (
<form className="contact-form" onSubmit={handleSubmit(onSubmit)}>
<Typography variant="h4" className="form-title">
Be the first.
</Typography>
<div className="form-component">
<TextField
id="standard-basic"
label="Nome*"
variant="standard"
name="nome"
value={name}
onChange={handleInputChange}
{...register("nome", {
required: true,
})}
/>
{errors?.nome?.type === "required" && (
<MdError className="form-validation-icon" />
)}
</div>
<Divider className="form-hr" />
<div className="form-component">
<TextField
id="standard-basic"
label="Email*"
variant="standard"
name="email"
value={email}
onChange={handleInputChange}
{...register("email", {
required: true,
pattern: {
value:
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+#[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
},
})}
/>
{errors?.email?.type === "required" && (
<MdError className="form-validation-icon" />
)}
{errors?.email?.type === "pattern" && (
<Typography variant="p" className="form-validation-email">
Inserisci un indirizzo email valido.
</Typography>
)}
</div>
<Divider className="form-hr" />
<Button className="form-submit" type="submit" variant="contained">
<BsArrowRight />
</Button>
</form>
);
};
export default ContactForm;
Textfields are completely block but initial state is actually working, do i miss something?
Can you help me?
To assign initial values using the useForm hook, you pass it under the defaultValues parameter passed to the hook like so:
const {
register,
handleSubmit,
reset
formState: { errors },
} = useForm({
defaultValues: initialState
});
Then just pass the ...register name and email to the inputs. There is no need to assign values to them again:
<TextField
id="standard-basic"
label="Name*"
variant="standard"
name="name"
{...register("name", {
required: true,
})}
/>
// for the email..
<TextField
id="standard-basic"
label="Email*"
variant="standard"
name="email"
{...register("email", {
required: true,
pattern: {
value: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+#[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,},
})}
/>
If you'll notice, the values are off the text fields already and there's also no need for the handleInputChange function. The useForm hook takes care of that.
Edit:
In addition to the onSubmit function, the handleSubmit from useForm passes a data object into the function like this:
const onSubmit = (data) => {
console.log("form submitted", data);
reset(); // this can be destructured of the `useForm` hook.
};
For more info check their documentation
When I'm launching my project, I was faced with the error:
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
App.js
import React from "react";
import {
Formik,
Field,
Form,
useField,
FieldAttributes,
FieldArray
} from "formik";
import {
TextField,
Button,
Checkbox,
Radio,
FormControlLabel,
Select,
MenuItem
} from "#material-ui/core";
import * as yup from "yup";
type MyRadioProps = { label: string } & FieldAttributes<{}>;
const MyRadio: React.FC<MyRadioProps> = ({ label, ...props }) => {
const [field] = useField<{}>(props);
return <FormControlLabel {...field} control={<Radio />} label={label} />;
};
const MyTextField: React.FC<FieldAttributes<{}>> = ({
placeholder,
...props
}) => {
const [field, meta] = useField<{}>(props);
const errorText = meta.error && meta.touched ? meta.error : "";
return (
<TextField
placeholder={placeholder}
{...field}
helperText={errorText}
error={!!errorText}
/>
);
};
const validationSchema = yup.object({
firstName: yup
.string()
.required()
.max(10),
pets: yup.array().of(
yup.object({
name: yup.string().required()
})
)
});
const App: React.FC = () => {
return (
<div>
<Formik
validateOnChange={true}
initialValues={{
firstName: "",
lastName: "",
isTall: false,
cookies: [],
yogurt: "",
pets: [{ type: "cat", name: "jarvis", id: "" + Math.random() }]
}}
validationSchema={validationSchema}
// validate={values => {
// const errors: Record<string, string> = {};
// if (values.firstName.includes("bob")) {
// errors.firstName = "no bob";
// }
// return errors;
// }}
onSubmit={(data, { setSubmitting }) => {
setSubmitting(true);
// make async call
console.log("submit: ", data);
setSubmitting(false);
}}
>
{({ values, errors, isSubmitting }) => (
<Form>
<MyTextField placeholder="first name" name="firstName" />
<div>
<Field
placeholder="last name"
name="lastName"
type="input"
as={TextField}
/>
</div>
<Field name="isTall" type="checkbox" as={Checkbox} />
<div>cookies:</div>
<Field
name="cookies"
type="checkbox"
value="chocolate chip"
as={Checkbox}
/>
<Field
name="cookies"
type="checkbox"
value="snickerdoodle"
as={Checkbox}
/>
<Field name="cookies" type="checkbox" value="sugar" as={Checkbox} />
<div>yogurt</div>
<MyRadio name="yogurt" type="radio" value="peach" label="peach" />
<MyRadio
name="yogurt"
type="radio"
value="blueberry"
label="blueberry"
/>
<MyRadio name="yogurt" type="radio" value="apple" label="apple" />
<FieldArray name="pets">
{arrayHelpers => (
<div>
<Button
onClick={() =>
arrayHelpers.push({
type: "frog",
name: "",
id: "" + Math.random()
})
}
>
add pet
</Button>
{values.pets.map((pet, index) => {
return (
<div key={pet.id}>
<MyTextField
placeholder="pet name"
name={`pets.${index}.name`}
/>
<Field
name={`pets.${index}.type`}
type="select"
as={Select}
>
<MenuItem value="cat">cat</MenuItem>
<MenuItem value="dog">dog</MenuItem>
<MenuItem value="frog">frog</MenuItem>
</Field>
<Button onClick={() => arrayHelpers.remove(index)}>
x
</Button>
</div>
);
})}
</div>
)}
</FieldArray>
<div>
<Button disabled={isSubmitting} type="submit">
submit
</Button>
</div>
<pre>{JSON.stringify(values, null, 2)}</pre>
<pre>{JSON.stringify(errors, null, 2)}</pre>
</Form>
)}
</Formik>
</div>
);
};
export default App;
index.tsx
import React, {Component} from 'react';
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
I have googled it and don't find an appropriate answer
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
}
I have tried too many times for this below form (React-with-redux), my textbox is not taking any inputs, please suggest me,
Thanks for help, Buddy
import React from 'react';
import './login.css';
import {connect} from 'react-redux'
import { change, Field, reduxForm } from 'redux-form';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import { login } from './Actions';
export const renderTextField = ({
input,
label,
id,
multiLine,
rowsMax,
fullWidth,
disabled,
hintText,
defaultValue,
onChange,
maxLength,
loader,
meta: { touched, error },
customError,
autoFocus,
floatingLabelFixed,
...custom
}) => {
return (
<TextField
id={id}
defaultValue={defaultValue}
autoFocus={autoFocus}
floatingLabelText={label}
floatingLabelFixed={floatingLabelFixed}
errorText={touched && (error || customError)}
multiLine={multiLine}
hintText={hintText}
rowsMax={rowsMax}
disabled={disabled}
fullWidth={true}
className="valuefont"
autoComplete='new-type'
onChange={(event) => onChange}
maxLength={maxLength}
floatingLabelStyle={{ top: '30px', color: '#7a7a7a' }}
floatingLabelFocusStyle={{ color: '#01B9C1' }}
style={{ height: '62px ' }}
inputStyle={{ marginTop: '10px' }}
{...input}
{...custom}
/>
);
};
class Login extends React.Component{
constructor(props){
super(props);
}
handleChange = (e)=>{
// setValues({ ...values, [e.target.name]: e.target.value });
}
onSubmit = (formProps)=>{
debugger
}
render(){
const { handleSubmit } = this.props;
return(
<form noValidate onSubmit={handleSubmit(this.onSubmit)}>
<div>
<Field
name="firstName"
component={renderTextField}
label="First Name"
/>
</div>
<Button variant="contained" color="primary" type="submit">
Login
</Button>
</form>
)
}
}
const mapStateToProps = (state)=>{
return {
loading : state.loginReducer.loading,
error : state.loginReducer.error
}
}
export default connect(mapStateToProps,{login})(reduxForm({
form: 'LoginForm',
keepDirtyOnReinitialize: true,
enableReinitialize: true,
})(Login))
Use renderTextField instead of Field
import React from 'react';
import './login.css';
import {connect} from 'react-redux'
import { change, Field, reduxForm } from 'redux-form';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import { login } from './Actions';
export const renderTextField = ({
input,
label,
id,
multiLine,
rowsMax,
fullWidth,
disabled,
hintText,
defaultValue,
onChange,
maxLength,
loader,
meta: { touched, error },
customError,
autoFocus,
floatingLabelFixed,
...custom
}) => {
return (
<TextField
id={id}
defaultValue={defaultValue}
autoFocus={autoFocus}
floatingLabelText={label}
floatingLabelFixed={floatingLabelFixed}
errorText={touched && (error || customError)}
multiLine={multiLine}
hintText={hintText}
rowsMax={rowsMax}
disabled={disabled}
fullWidth={true}
className="valuefont"
autoComplete='new-type'
onChange={(event) => onChange}
maxLength={maxLength}
floatingLabelStyle={{ top: '30px', color: '#7a7a7a' }}
floatingLabelFocusStyle={{ color: '#01B9C1' }}
style={{ height: '62px ' }}
inputStyle={{ marginTop: '10px' }}
{...input}
{...custom}
/>
);
};
class Login extends React.Component{
constructor(props){
super(props);
}
handleChange = (e)=>{
// setValues({ ...values, [e.target.name]: e.target.value });
}
onSubmit = (formProps)=>{
debugger
}
render(){
const { handleSubmit } = this.props;
return(
<form noValidate onSubmit={handleSubmit(this.onSubmit)}>
<div>
<renderTextField
name="firstName"
label="First Name"
onChange={this.handleChange}
/>
</div>
<Button variant="contained" color="primary" type="submit">
Login
</Button>
</form>
)
}
}
const mapStateToProps = (state)=>{
return {
loading : state.loginReducer.loading,
error : state.loginReducer.error
}
}
export default connect(mapStateToProps,{login})(reduxForm({
form: 'LoginForm',
keepDirtyOnReinitialize: true,
enableReinitialize: true,
})(Login))
I have a simple component with some inputs, I am trying to verify whether a mock function is called after a button is clicked.
Here is what I tried in my test file:
import React from 'react'
import { mount } from 'enzyme'
import AddUser from '../Components/AddUserWithFormik'
import { Router } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import * as Constants from '../actions/actionTypes'
const hist = createBrowserHistory()
describe('AddUser page', () => {
let wrapper
const mockFetchDetailsActions = jest.fn()
const mockHandleCancel = jest.fn()
const mockHandleInputChangeAction = jest.fn()
const mockHandleSubmit = jest.fn()
const match = {
params: {
id: '12345'
}
}
const location = {
pathname: '/add_user',
search: '',
hash: '',
key: 'ph0ovl'
}
const user = {
first_name: '',
last_name: '',
email: '',
email_secondary: '',
mobile_phone: '',
work_phone: ''
}
beforeEach(() => {
wrapper = mount(
<Router history={hist}>
<AddUser
match={match}
location={location}
user={user}
handleSubmit={mockHandleSubmit}
actions={{
fetchDetailsActions: mockFetchDetailsActions,
handleCancel: mockHandleCancel,
handleInputChangeAction: mockHandleInputChangeAction
}}
/>
</Router>
)
})
describe('#ComponentsRendered', () => {
it('verify simulate change on all input elements', () => {
wrapper
.find('button')
.at(0)
.simulate('click')
expect(mockHandleSubmit).toHaveBeenCalled()
})
})
})
Here is my component:
/* eslint-disable no-invalid-this */
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '#material-ui/core/styles'
import GridContainer from './Grid/GridContainer'
import GridItem from './Grid/GridItem'
import { TextField } from 'formik-material-ui'
import { Field, Form } from 'formik'
import dashboardStyle from '../styles/dashboardStyle'
import Card from './Card/Card'
import CardBody from './Card/CardBody'
import * as Constants from '../actions/actionTypes'
import SaveAndCancelButtons from './Common/saveAndCancelButtons'
class AddUser extends React.Component {
componentDidMount () {
if (this.props.match.params.id) {
this.props.actions.fetchDetailsActions(Constants.FETCH_DETAILS_API_CALL_REQUEST, this.props.match.params.id)
} else {
this.props.actions.handleCancel()
}
}
render () {
const { classes, isFetching } = this.props
return (
<Form>
<Field
name="user"
render={feildProps => (
<Fragment>
<GridContainer>
<GridItem xs={12} sm={12} md={12}>
<Card>
<h2 className={classes.cardTitleWhite}>Add User</h2>
<CardBody isFetching={isFetching}>
<GridContainer>
<GridItem xs={12} sm={12} md={4}>
<Field
label="First Name"
name={`user.first_name`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
<Field
label="Secondary Email"
name={`user.email_secondary`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
</GridItem>
<GridItem xs={12} sm={12} md={4}>
<Field
label="Last Name"
name={`user.last_name`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
<Field
label="Mobile Phone"
name={`user.mobile_phone`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
</GridItem>
<GridItem xs={12} sm={12} md={4}>
<Field
label="Email"
name={`user.email`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
<Field
label="Work Phone"
name={`user.work_phone`}
className={this.props.classes.textField}
margin="normal"
variant="outlined"
component={TextField}
/>
</GridItem>
</GridContainer>
</CardBody>
</Card>
<SaveAndCancelButtons
handleSave={() => {
this.props.handleSubmit()
}}
routingLink="/people"
label="Save"
/>
</GridItem>
</GridContainer>
</Fragment>
)}
/>
</Form>
)
}
}
AddUser.propTypes = {
classes: PropTypes.object.isRequired
}
export default withStyles(dashboardStyle)(AddUser)
Here is my withFormik() wrapper:
import { withStyles } from '#material-ui/core/styles'
import { withFormik } from 'formik'
import * as Yup from 'yup'
import AddUser from './AddUser'
import * as Constants from '../actions/actionTypes'
const styles = theme => ({
textField: {
width: '100%'
}
})
const onSave = props => {
const userDetails = {
user: props.user
}
if (userDetails && userDetails.user.id) {
props.actions.updateDetailsActions(Constants.UPDATE_USER_API_CALL_REQUEST, userDetails.user.id, userDetails)
} else {
props.actions.addNewUserAction(Constants.ADD_USER_API_CALL_REQUEST, userDetails)
}
}
const validations = Yup.object().shape({
user: Yup.object().shape({
first_name: Yup.string().required('Required'),
last_name: Yup.string().required('Required'),
email: Yup.string().required('Required')
})
})
const withFormikWrapper = withFormik({
validationSchema: validations,
enableReinitialize: true,
handleSubmit: props => {
onSave(props)
}
})(AddUser)
export default withStyles(styles)(withFormikWrapper)
Expected result:
I expect that the mockHandleSubmit is called when the simulate click is happening.
Actual Results:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called, but it was not called.
123 | // console.log(wrapper.debug())
124 |
> 125 | expect(mockHandleSubmit).toHaveBeenCalled()
| ^
126 | })
127 | })
128 | })
at Object.toHaveBeenCalled (src/test/AddUser.test.js:125:32)
Here is the saveAndCancelButton component:
import React, { Component } from 'react'
import RoutingButton from './RoutingButton'
import { withStyles } from '#material-ui/core/styles'
import GridContainer from '../Grid/GridContainer'
import GridItem from '../Grid/GridItem'
import ClientContactsStyles from '../../styles/ClientsContactsStyles'
import SaveIcon from '#material-ui/icons/Save'
import Close from '#material-ui/icons/Close'
import classNames from 'classnames'
import Button from '#material-ui/core/Button'
class SaveAndCancelButtons extends Component {
render () {
const { classes } = this.props
const close = <Close />
return (
<GridContainer>
<GridItem xs={12} sm={12} md={6}>
{this.props.submitButtonProps ? (
<GridItem xs={12} sm={12} md={6}>
<Button
variant="contained"
size="large"
className={classes.button}
onClick={this.props.handleSubmit}
>
Submit
</Button>
</GridItem>
) : null}
</GridItem>
<GridItem xs={12} sm={12} md={3}>
<Button
variant="contained"
size="large"
className={classes.button}
onClick={this.props.handleSave}
>
<SaveIcon
className={classNames(classes.leftIcon, classes.iconSmall)}
/>
{this.props.label}
</Button>
</GridItem>
<GridItem xs={12} sm={12} md={3}>
<RoutingButton
variant="contained"
size="large"
className={classes.button}
buttonIcon={close}
routingLink={this.props.routingLink}
label="Cancel"
/>
</GridItem>
</GridContainer>
)
}
}
export default withStyles(ClientContactsStyles)(SaveAndCancelButtons)
The problem here is jest function is not waiting for formik to call its onSubmit() function. To resolve this jest provides 'wait' api. See the below code.
await wait(() => {
expect(mockHandleSubmit).toHaveBeenCalled()
});
Happy Coding :)
I don't think submit is being called because your user is not passing your validation. Rectify that and use below for expectation.
xxxxxx
Given that you have omitted test code in your post that satisfies the schema above is not the solution. The solution is to do expectations against the actions. It is not possible to mock handleSubmit as this is dealt with by the withFormik wrapper.
xxxxxx
it('verify simulate change on all input elements', async () => {
wrapper.find('button').at(0).simulate('click')
await new Promise(resolve=>{
setTimeout(()=>resolve(),0);
})
expect(mockHandleSubmit).toHaveBeenCalled()
})