I'm trying to figure out how to submit form data to firestore from a formik form in a react app.
I've used this tutorial to try to learn how to make the form and then tried to add the firebase form submission to the submit handler.
The form has:
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import firebase from '../../../../firebase';
// import firestore from '../../../../firebase';
import { withStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
// import axios from 'axios';
import {
Formik, Form, Field, ErrorMessage,
} from 'formik';
import * as Yup from 'yup';
// import { DisplayFormikState } from './formikHelper';
const styles = {
};
function Contact(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>
<Link
// component="button"
className="footerlinks"
onClick={handleClickOpen}
>
Contact
</Link>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
{!isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Contact Us</DialogTitle>
<DialogContent>
<DialogContentText>
Thanks for your interest.
</DialogContentText>
<Formik
initialValues={{ email: '', name: '', comment: '' }}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
firebase.firestore.collection("contact").doc.set({
name: "name",
email: "email",
comment: "comment"
})
// axios.post(contactFormEndpoint,
// values,
// {
// headers: {
// 'Access-Control-Allow-Origin': '*',
// 'Content-Type': 'application/json',
// }
// },
.then(() => {
setSubmitionCompleted(true);
});
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.email()
.required('Required'),
name: Yup.string()
.required('Required'),
comment: Yup.string()
.required('Required'),
})}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
return (
<form onSubmit={handleSubmit}>
<TextField
label="Name"
name="name"
className={classes.textField}
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.name && touched.name) && errors.name}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
error={errors.email && touched.email}
label="Email"
name="email"
className={classes.textField}
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.email && touched.email) && errors.email}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="Let us know how we can help"
name="comment"
className={classes.textField}
multiline
rows={4}
value={values.comment}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.comment && touched.comment) && errors.comment}
margin="normal"
style={{ width: "100%"}}
/>
<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>
Thanks
</DialogContentText>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleClose}
>
Back to app
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</DialogContent>
</React.Fragment>}
</Dialog>
</React.Fragment>
);
}
export default withStyles(styles)(Contact);
The firestore config file has:
When I try this, I get a warning in the console when I press submit (no error messages and the form just hangs with data that I entered in the form.
instrument.ts:129 Warning: An unhandled error was caught from
submitForm() TypeError:
firebase__WEBPACK_IMPORTED_MODULE_2_.default.firestore.collection is not a function
Note, I've dried both .doc.set and .doc.add in the firestore method - neither works.
Found it eventually -the doc has to be doc()
Related
I want to create dynamic form with react-hook-form.
Below is my code.
I want to create a dialog for entering a detailed profile in a separate component and use it in MainForm.
I can display the DetailForm, but the values entered in it are not reflected.
Data on the DetailForm component is not included when submitting.
Any guidance on how to do this would be greatly appreciated.
MainForm
import React from 'react';
import {
useForm,
Controller,
useFieldArray
} from 'react-hook-form';
import {
Button,
TextField,
List,
ListItem,
IconButton,
} from '#material-ui/core';
import DetailForm from '#components/DetailForm'
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
function MainForm(props:any) {
const { control, handleSubmit, getValues } = useForm({
mode: 'onBlur',
defaultValues: {
profiles: [
{
firstName: '',
lastName: '',
email: '',
phone: ''
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'profiles',
});
const onSubmit = () => {
const data = getValues();
console.log('data: ', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<List>
fields.map((item, index) => {
return (
<ListItem>
<Controller
name={`profiles.${index}.firstName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="First Name"
/>
}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="Last Name"
/>
}
/>
<DetailForm index={index} />
</ListItem>
)
})
</List>
<IconButton onClick={() => append({})}>
<AddCircleOutlineIcon />
</IconButton>
<Button
type='submit'
>
SAVE
</Button>
</form>
)
}
DetailForm
import React from 'react';
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '#material-ui/core';
export default function DetailForm(props: any) {
const [dialogState, setDialogState] = React.useState<boolean>(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button
onClick={handleOpen}
>
Set Detail Profile
</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<TextField
name={`profiles.${props.index}.email`}
label="Email Address"
/>
<TextField
name={`profiles.${props.index}.phone`}
label="Phone Number"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleClose}>
Add
</Button>
</DialogActions>
</Dialog>
</>
)
}
You have to register those fields inside your <Dialog /> component - just pass control to it. It's also important to set an inline defaultValue when using <Controller /> and useFieldArray. From the docs:
inline defaultValue is required when working with useFieldArray by integrating with the value from fields object.
One other minor thing: You should also pass the complete fieldId to your <Dialog /> component instead of just passing the index. This way it would be easier to change the fieldId in one place instead of editing all fields in your <Dialog /> component.
MainForm
<ListItem key={item.id}>
<Controller
name={`profiles.${index}.firstName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="First Name" />
)}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Last Name" />
)}
/>
<DetailForm control={control} fieldId={`profiles.${index}`} />
</ListItem>
DetailForm
export default function DetailForm({ control, fieldId }) {
const [dialogState, setDialogState] = React.useState(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button onClick={handleOpen}>Set Detail Profile</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<Controller
name={`${fieldId}.email`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Email Address" />
)}
/>
<Controller
name={`${fieldId}.phone`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Phone Number" />
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Add</Button>
</DialogActions>
</Dialog>
</>
);
}
I am building a login form with Formik with Materail UI, but Formik doesn't recognize Button of Material UI. If I replace the Button with html button everything works. Could anybody explain why it's not working with the Button component. Below is my code:
import React, { ReactElement } from "react";
import TextField from "#material-ui/core/TextField";
import FormControl from "#material-ui/core/FormControl";
import { Formik, Form } from "formik";
import { makeStyles } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
const useStyles = makeStyles((theme) => ({
root: {
margin: theme.spacing(1),
width: 200,
display: "flex",
},
input: {
marginTop: 5,
marginBottom: 5,
},
}));
interface Props {}
export default function loginForm({}: Props): ReactElement {
const classes = useStyles();
return (
<Box className={classes.root}>
<Formik
initialValues={{ username: "", password: "" }}
validate={(values) => {
const errors: { username?: string; password?: string } = {};
if (!values.username) {
errors.username = "Required";
} else if (!values.password) {
errors.password = "Required";
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
console.log("values: ", values);
setSubmitting(false);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
}) => (
<form onSubmit={handleSubmit}>
<FormControl>
<TextField
className={classes.input}
error={errors.username ? true : false}
type="username"
name="username"
onChange={handleChange}
onBlur={handleBlur}
value={values.username}
label="Username"
variant="outlined"
helperText={
errors.username && touched.username && errors.username
}
/>
<TextField
className={classes.input}
error={errors.password ? true : false}
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
label="Password"
variant="outlined"
helperText={
errors.password && touched.password && errors.password
}
/>
</FormControl>
<Box display="flex" justifyContent="space-between">
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
>
Submit
</Button>
<Button
variant="contained"
color="secondary"
disabled={isSubmitting}
>
Cancel
</Button>
</Box>
</form>
)}
</Formik>
</Box>
);
}
please note the code above doesn't work, but if you replace Button with button, the form works.
In most browsers, a HTML button by default has the type=submit which means that Formik's submit handler will be called. A Material-UI button does not have this default so the submit handler will never be called. Try adding type=submit to your <Button> props.
(Also, check out Formik's Material-UI integration examples)
You should add the id to the form.
<form onSubmit={handleSubmit} id="myForm">
And bind the form id with submit button
<Button
type="submit"
form="myForm"
>
I have a customer form which takes two values.
Customer name
Customer Emails (Which can be multiples)
I've given an add button besides the email field through which user can add more emails to the form. now i want to validate each email that's been added. also if it's added it is required too. empty email is not allowed.
The question is i have only one validation schema to validate email field. how can i use the same field to validate multiple emails?
Even after adding the correct email. it still gives error !
Please see the sandbox link to see the code. code sandbox link
Here is the working code with multiple email validation and errors.
I've used Formik FieldArray to handle multiple emails.
You can replace your code with this in your sandbox to test.
import React from "react";
import { TextField, IconButton } from "#material-ui/core";
import {
AddCircleOutlined as AddCircleOutlinedIcon,
IndeterminateCheckBox as IndeterminateCheckBoxIcon
} from "#material-ui/icons";
//FORMIK
import { Formik, FieldArray, getIn, ErrorMessage } from "formik";
import * as Yup from "yup";
export default function UnitsDrawer(props) {
const callAPI = e => {
console.log(e.name);
console.log(e.email);
};
const testSchema = Yup.object().shape({
name: Yup.string().required("Customer name is required"),
email: Yup.array().of(
Yup.string()
.email("Enter a valid email")
.required("Email is required")
)
});
const initialValues = {
name: "",
email: [""]
};
const formRef = React.useRef();
return (
<div>
<Formik
innerRef={formRef}
validationSchema={testSchema}
initialValues={initialValues}
onSubmit={(values, actions) => {
actions.setSubmitting(false);
callAPI(values);
}}
>
{({
handleChange,
handleBlur,
values,
errors,
touched,
handleSubmit,
isSubmitting,
}) => {
return (
<>
<div>
<TextField
label="Customer Name"
name="name"
margin="normal"
variant="outlined"
error={errors.name && touched.name}
onChange={handleChange}
onBlur={handleBlur}
value={values.name}
fullWidth
/>
<ErrorMessage name="name" component="div" />
</div>
<FieldArray name="email">
{({ push, remove }) =>
values.email.map((field, index) => (
<div key={`${index}`} className="dynamic-fields">
<div>
<TextField
label="Email"
variant="outlined"
className="input-item"
error={
getIn(touched, `email.${index}`) &&
getIn(errors, `email.${index}`)
}
name={`email.${index}`}
value={values.email[index]}
onChange={handleChange}
onBlur={handleBlur}
fullWidth
/>
<ErrorMessage name={`email.${index}`} component="div" />
</div>
<IconButton
aria-label="filter list"
className="add-icon"
onClick={() => {
push("");
}}
>
<AddCircleOutlinedIcon color="primary" />
</IconButton>
{values.email.length > 1 && (
<IconButton
aria-label="filter list"
className="add-icon"
onClick={() => {
remove(index);
}}
>
<IndeterminateCheckBoxIcon />
</IconButton>
)}
</div>
))
}
</FieldArray>
</>
);
}}
</Formik>
</div>
);
}
I'm trying to submit form onSubmitbut its not firing the this.commentSubmit function, if i take <form></form> out, use <Button onSubmit> function it works however i need the form wrapped around the Textfield for the backend to read the req.body.comment_body to work.
Comment.js
import React from "react";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
const Comment = (props) => (
<form onSubmit={props.onSubmit}>
<TextField
type="text"
id="outlined-multiline-static"
label="Write A Comment"
multiline
name="comment_body"
value={props.commentBody}
rows="10"
fullWidth
margin="normal"
variant="outlined"
onChange={props.commentChange}
/>
<Button type="submit" variant="outlined" component="span" color="primary">
Post A Comment
</Button>
</form>
)
export default Comment;
Image Container Component
import React from "react";
import Button from '#material-ui/core/Button';
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography';
import Paper from '#material-ui/core/Paper';
import Divider from '#material-ui/core/Divider';
import Image from './Image';
import Axios from '../Axios';
import moment from 'moment';
import Comment from './Comment';
class ImageContainer extends React.Component{
state = {
isComment: false,
comment_body: ""
}
handleCommentChange = (e) => {
this.setState({
comment_body: e.target.value
})
}
writeComment = (id) => {
this.setState({
isComment: this.state.isComment ? '' : id // check if you state is filled to toggle on/off comment
})
}
commentSubmit = (e) => {
e.preventDefault();
console.log(this.state.comment_body); // doesn't get console.log
Axios.post('/images/newComment', this.state.comment_body).then( (response )=> {
const newComment = { ...response.data};
console.log(newComment);
this.setState({
comment_body: ''
})
})
}
render(){
const { img, deleteImg } = this.props
return(
<Grid item sm={12} md={12} style={{ margin: '30px 0px'}}>
<Paper style={{padding:'20px 20px'}}>
{/* // empty image_title */}
<Typography style={{ padding: '30px 5px', letterSpacing:'8px', textTransform:'uppercase'}} variant="h4" align="center">{img.image_title}</Typography>
<Divider style={{ width: '150px', margin:'10px auto', backgroundColor:'#000000'}} variant="middle" />
<Image image_url={img.img_url} />
<Typography variant="h6" align="center">{img.user.username}</Typography>
<Typography variant="h6" align="center">{moment(img.created_at).calendar()}</Typography>
<Button onClick ={() => this.writeComment(img.id)} variant="outlined" component="span" color="primary">
{this.state.isComment === img.id ? "Close" : "Write A Comment"}
</Button>
{/* here were prevent comments being selected for all items in the array, renders the comment form you clicked on. */}
{this.state.isComment === img.id ?
<Comment onSubmit={this.commentSubmit}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
: null}
{/* hide delete button when user enters comment */}
{!this.state.isComment ? <Button style={{margin: '0px 20px'}} onClick={deleteImg} variant="outlined" component="span" color="primary">
Delete
</Button> : null}
</Paper>
</Grid>
)
}
}
export default ImageContainer
Alternatively this works but i don't think the back end reads the comment_body value
import React from "react";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
const Comment = (props) => {
// <form onSubmit={props.onSubmit}>
return(
<div>
<TextField
type="text"
id="outlined-multiline-static"
label="Write A Comment"
multiline
name="comment_body"
value={props.commentBody}
rows="10"
fullWidth
margin="normal"
variant="outlined"
onChange={props.commentChange}
/>
<Button onClick={props.onSubmit} type="submit" variant="outlined" component="span" color="primary">
Post A Comment
</Button>
</div>
);
// </form>
}
export default Comment;
backend
router.post('/newComment', async (req, res) => {
const comment = new Comment({
comment_body: req.body.comment_body,
user_id: req.user.id
})
comment.save().then( (comment) => {
return res.status(200).json(comment);
})
})
The problem lies with <Button> from Material-ui not being an actual button but rather a combination of a <span>. So if you have a type="submit" the form doesn't do anything.
If you change your material-ui <Button> to a native <button> it works as expected.
Here's an example: https://codesandbox.io/embed/56615445-fj6sc
I get an error message I didn't figure out how to get rid off:
yup__WEBPACK_IMPORTED_MODULE_12__.object(...).shap is not a function
import React from 'react'
import Button from '#material-ui/core/Button'
import Dialog from '#material-ui/core/Dialog'
import DialogActions from '#material-ui/core/DialogActions'
import DialogContent from '#material-ui/core/DialogContent'
import DialogContentText from '#material-ui/core/DialogContentText'
import DialogTitle from '#material-ui/core/DialogTitle'
import TextField from '#material-ui/core/TextField'
import { Link } from 'gatsby'
import DisplayOutput from '../pages/DisplayOutput'
import { Formik, Field, Form, ErrorMessage } from 'formik'
import * as Yup from 'yup'
class DialogFormWithFormik extends React.Component {
constructor(props) {
super(props)
this.state = {
open: false,
dialogModal: 'none',
}
}
handleClickOpen = () => {
this.setState({ open: true })
this.setState({ dialogModal: 'login' })
}
handleRegisterClickOpen = () => {
this.setState({ open: true })
this.setState({ dialogModal: 'register' })
}
handleClose = () => {
this.setState({ dialogModal: false })
}
onSubmit = values => {
console.log(values)
alert('values submitted')
}
form = props => {
return (
<div>
<Button
variant="outlined"
color="primary"
onClick={() => this.handleClickOpen()}
>
Login
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => this.handleRegisterClickOpen()}
>
Register
</Button>
<Dialog
onClose={() => this.handleClose()}
aria-labelledby="customized-dialog-title"
open={this.state.dialogModal === 'login'}
>
<DialogTitle id="form-dialog-title">
To Display Student Data
</DialogTitle>
<DialogContent />
<form onSubmit={props.handleSubmit}>
<TextField
label="Username"
type="text"
margin="normal"
name="userName"
/>
<ErrorMessage name="userName" />
<br />
<TextField
label="Password"
type="password"
autoComplete="current-password"
margin="normal"
/>
<ErrorMessage name="password" />
<DialogActions>
<nav>
<Button color="primary">Login</Button>
</nav>
<br />
<Button onClick={() => this.handleClose()}>Cancel</Button>
</DialogActions>
</form>
</Dialog>
</div>
)
}
schema = () => {
const schema = Yup.object().shap({
userName: Yup.string().required(),
password: Yup.string().required(),
})
return schema
}
render() {
return (
<div align="center">
<Formik
initialValues={{
userName: '',
password: '',
}}
onSubmit={this.onSubmit}
render={this.form}
validationSchema={this.schema()}
/>
</div>
)
}
}
export default DialogFormWithFormik