I'm currently working on a form (Material-UI) that users fill in to log a set they did for an exercise. Upon submission, it will run a GraphQL mutation. As I also have login and register functionalities that share similarities, I created a form hook for these. The login and register do not need to be reset, as this is done by redirecting them to the home page. However, for the logging set functionality, I want the modal (where the form is) to close after resetting the form back to it's initial state, so that when they choose to log another set, the form does not contain the values from the previous logged set.
Form
const initialState = {
exerciseName: "",
weight: undefined,
reps: undefined,
notes: "",
};
function MyModal() {
const [errors, setErrors] = useState({});
const { onChange, onSubmit, values } = useForm(registerSet, initialState);
const [addSet] = useMutation(ADD_SET, {
update() {
// need to reset form to initial state here
handleClose();
},
onError(err) {
setErrors(err.graphQLErrors[0].extensions.exception.errors);
},
variables: values,
});
function registerSet() {
addSet();
}
return (
<form
onSubmit={onSubmit}
id="addSetForm"
noValidate
autoComplete="off"
>
<TextField
name="exerciseName"
label="Exercise Name"
select
value={values.exerciseName}
error={errors.exerciseName ? true : false}
onChange={onChange}
>
<MenuItem key="Bench Press" value="Bench Press">
Bench Press
</MenuItem>
<MenuItem key="Deadlift" value="Deadlift">
Deadlift
</MenuItem>
<MenuItem key="Squat" value="Squat">
Squat
</MenuItem>
</TextField>
<Grid container spacing={1}>
<Grid item xs={6}>
<TextField
name="weight"
label="Weight"
type="number"
value={values.weight}
error={errors.weight ? true : false}
onChange={onChange}
/>
</Grid>
<Grid item xs={6}>
<TextField
name="reps"
label="Reps"
type="number"
value={values.reps}
error={errors.reps ? true : false}
onChange={onChange}
/>
</Grid>
</Grid>
<TextField
name="notes"
label="Notes (Optional)"
type="text"
multiline={true}
rows="4"
value={values.notes}
onChange={onChange}
/>
</form>
)
}
useForm Hook
export const useForm = (callback, initialState = {}) => {
const [values, setValues] = useState(initialState);
const onChange = (event) => {
setValues({
...values,
[event.target.name]:
event.target.type === "number"
? parseInt(event.target.value)
: event.target.value,
});
};
const onSubmit = (event) => {
event.preventDefault();
callback();
};
return {
onChange,
onSubmit,
passwordVisibility,
confirmPasswordVisibility,
values,
};
};
I'm not sure how I can access setValues from the useForm hook in the update() handler for useMutation to reset the form back to it's initial state.
Step 1: create a resetValues() function inside your useForm hook and export it
const resetValues = () => {
setValues(initialState)
};
return {
// ... OTHER EXPORTS
resetValues,
};
Step 2: Then use this function inside your component
const { onChange, onSubmit, resetValues, values } = useForm(registerSet, initialState);
const [addSet] = useMutation(ADD_SET, {
update() {
resetValues(); // SEE HERE
handleClose();
},
});
Related
I am working on a workout logger, where users can log their sets for exercises. I am using the MERNG stack for this project. Currently, I am working on the validation for logging a set on the front end. This will ask users for these: exercise name(String), weight(Float), reps(Int) and notes(String - optional) in a form. The problem I am having, is initializing and setting the weight and reps in their respected types. This is the initial states I have set for these fields:
{
exerciseName: "",
weight: "",
reps: "",
notes: "",
}
I realize that this will return the weight and reps as Strings, so I tried this as well (although, I don't want fields to have a weight and rep already entered for them, in case they forget to input these themselves and add it to their logs).
{
exerciseName: "",
weight: 0,
reps: 0,
notes: "",
}
This approach works to a certain degree, as the console logs them as Numbers but when the user changes this in the TextField(Material UI), they end up getting submitted as Strings anyway. Therefore, I receive the 400 status code from my backend, as these should be a Float and an Int. How can I achieve the result of initializing and setting these values as numbers only, so no error fires?
Form
const [errors, setErrors] = useState({});
const { onChange, onSubmit, values } = useForm(registerSet, { // Uses a hook for forms
exerciseName: "",
weight: "",
reps: "",
notes: "",
});
return (
<form
onSubmit={onSubmit}
id="addSetForm"
noValidate
autoComplete="off"
>
<TextField
name="exerciseName"
label="Exercise Name"
variant="outlined"
fullWidth
select
className={classes.formInput}
value={values.exerciseName}
error={errors.exerciseName ? true : false}
onChange={onChange}
>
<MenuItem key="Bench Press" value="Bench Press">
Bench Press
</MenuItem>
<MenuItem key="Deadlift" value="Deadlift">
Deadlift
</MenuItem>
<MenuItem key="Squat" value="Squat">
Squat
</MenuItem>
</TextField>
<Grid container spacing={1} className={classes.formInput}>
<Grid item xs={6}>
<TextField
name="weight"
label="Weight"
type="number"
variant="outlined"
fullWidth
value={values.weight}
error={errors.weight ? true : false}
onChange={onChange}
/>
</Grid>
<Grid item xs={6}>
<TextField
name="reps"
label="Reps"
type="number"
variant="outlined"
fullWidth
value={values.reps}
error={errors.reps ? true : false}
onChange={onChange}
/>
</Grid>
</Grid>
</form>
)
formHooks.js (useForm)
export const useForm = (callback, initialState = {}) => {
const [values, setValues] = useState(initialState);
const onChange = (event) => {
setValues({ ...values, [event.target.name]: event.target.value });
};
const onSubmit = (event) => {
event.preventDefault();
callback();
};
return {
onChange,
onSubmit,
values
};
};
You need to alter onChange function. If you console.log(typeof event.target.value) it will be string.
const onChange = (event) => {
setValues({ ...values, [event.target.name]: e.target.type === 'number' ? parseInt(e.target.value) : e.target.value });
};
I have a TextField thats updates state with useState on change. Im facing an issue where upon state change the entire component is re-rendering, I expect only the TextField to change.
const Uploader = ({ onUploadComplete }) => {
const [fields, setFields] = useState({});
const handleName = (event) => {
const { name, value } = event.target;
setFields((fields) => ({
...fields,
[name]: { value: value },
}
<React.Fragment>
<Grid item xs={4}>
<Card>{console.log('Card Media Rendered')}</Card>
</Grid>
<Grid item xs={8}>
<FormControl fullWidth>
<TextField
value={fields[file.id]?.value || ''}
onChange={handleName}
/>
</FormControl>
</Grid>
</React.Fragment>
}
You need to move the useState inside the functional component like this:
const Uploader = ({ onUploadComplete }) => {
const [fields, setFields] = useState({});
const handleName = (event) => {
const { name, value } = event.target;
setFields((fields) => ({
...fields,
[name]: { value: value },
}))
}
return (
<React.Fragment>
<Grid item xs={4}>
<Card>{console.log('Card Media Rendered')}</Card>
</Grid>
<Grid item xs={8}>
<FormControl fullWidth>
<TextField
value={fields[file.id]?.value || ''}
onChange={handleName}
/>
</FormControl>
</Grid>
</React.Fragment>
)
}
Your component did rerender because the variable that it depends on, wasn't in the react scope. So it couldn't trigger the update only to the Textfield
I am trying when I click on a checkbox it should get selected and save its value true in localstorage so that if the page is refreshed it should get value from the localstorage, similarly for second checkbox if it is selected too then also save its value true in localstorage.
In simple way if I select a both the checkboxes it should retain even after page refresh this is what I am trying for
Here is my code is what I have tried
Link - https://codesandbox.io/s/musing-architecture-p2nrg?file=/src/App.js:0-1760
import React from "react";
import "./styles.css";
import { Form } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
export default function App() {
const data = {
naming: localStorage.getItem("naming") || false,
fullname: localStorage.getItem("fullname") || false
};
const [currentCheckboxId, setCheckboxId] = React.useState(data);
const setCheckbox = event => {
const naming = event.target.checked;
console.log(naming);
localStorage.setItem("naming", naming);
setCheckboxId({
...data,
naming: event.target.checked
});
};
const setCheckbox2 = event => {
const fullname = event.target.checked;
console.log(fullname);
localStorage.setItem("fullname", fullname);
setCheckboxId({
...data,
fullname: event.target.checked
});
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form>
<>
<Form.Check
onChange={setCheckbox}
type="checkbox"
label="Check me out"
id="first"
checked={currentCheckboxId.naming}
/>
<Form.Group controlId="email">
<Form.Label>Email Address</Form.Label>
<Form.Control type="text" placeholder="Enter email" />
</Form.Group>
</>
<>
<Form.Check
onChange={setCheckbox2}
type="checkbox"
label="Check me out"
id="second"
checked={currentCheckboxId.fullname}
/>
<Form.Group controlId="fullname">
<Form.Label>Name</Form.Label>
<Form.Control type="text" placeholder="Enter name" />
</Form.Group>
</>
</Form>
</div>
);
}
Here is what you need to do:
Initialize the state with false
Use useEffect to run at mounted and retrieve checkbox values from LocalStorage and setState accordingly
Use setState with updater function to set new state which depends on current state
export default function App() {
// 1. Initially "false"
const [currentCheckboxId, setCheckboxId] = React.useState({
naming: false,
fullname: false
});
// 2. useEffect to run # mounted:
// get from LS and update the state
React.useEffect(() => {
const data = {
naming: localStorage.getItem('naming') === 'true' ? true : false,
fullname: localStorage.getItem('fullname') === 'true' ? true : false
};
setCheckboxId(data);
}, []);
const setCheckbox = event => {
const naming = event.target.checked;
console.log('naming', naming);
localStorage.setItem('naming', naming);
// 3. use "function" with prevData as first argument to setState
setCheckboxId(prevData => ({
...prevData,
naming: naming
}));
};
const setCheckbox2 = event => {
const fullname = event.target.checked;
console.log('fullname', fullname);
localStorage.setItem('fullname', fullname);
// 3. same as above
setCheckboxId(prevData => ({
...prevData,
fullname: fullname
}));
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form>
<>
<Form.Check
onChange={setCheckbox}
type="checkbox"
label="Check me out"
id="first"
checked={currentCheckboxId.naming}
/>
{/* Rest of your code */}
}
Here is a playground.
I want to reset state's variable to initial only for specific value
I have tried this:
const initialState={
name:'',
location:'',
....
....
....
}
this.state={
...initialState,
spinner:false,
open:false,
errorMessage:''
}
resetToInitialState = () =>{
this.setState(...initialState);
}
I am calling this resetToInitialState inside a function like this.resetToInitialState();
<TextField
id="outlined-name"
label="Machine id"
className={classes.textField}
InputProps={{
classes: {
input: classes.text,
},
}}
value={ !name && null } //tried this not working
onChange={e => this.setState({name: e.target.value})}
margin="normal"
variant="outlined"
/>
You forgot {}
this.setState({...initialState})
When you try to reset state, you need to clone initial state. If you pass in a reference to initialState, that will get mutated when you change state.
this.setState({...initialState});
To create a controlled component, TextField needs the value to be this.state.name.
<TextField
id="outlined-name"
label="Machine id"
className={classes.textField}
InputProps={{
classes: {
input: classes.text,
},
}}
value={ this.state.name }
onChange={e => this.setState({name: e.target.value})}
margin="normal"
variant="outlined"
/>
Edit: Reset TextField component when value is My Value
If you only want to reset the state when name is a specific value, you need to create a different onChange function and set onChangeReset to the onChange prop in the TextField component.
onChangeReset = e => {
if(e.target.value === 'My Value') {
this.resetToInitialState();
else {
this.setState({ name: e.target.value});
}
}
<TextField
onChange={this.onChangeReset}
value={this.state.name}
/>
Edit: Add multiple TextField components with reset function to reset all of the TextField components at the same time.
import React, { Component } from 'react';
class MyGroupedTextFields implemements Component {
constructor(props) {
super(props);
this.state = {
fields: {},
spinner: false,
open: false,
errorMessage: ''
};
}
resetTextFields = () => {
const state = { ...this.state };
state.fields = {};
this.setState({ state });
}
onTextFieldChange = event => {
const fields = {...this.state.fields};
fields[event.target.name] = event.target.value;
this.setState({ fields });
}
submit = async () => {
// perform submit network request
// pseudo code
const response = await submitForm(this.state.fields);
}
render() {
return (
<div>
<TextField
name='textField1'
onChange={this.onTextFieldChange}
value={this.state.fields.textField1}
/>
<TextField
name='textField2'
onChange={this.onTextFieldChange}
value={this.state.fields.textField2}
/>
<TextField
name='textField3'
onChange={this.onTextFieldChange}
value={this.state.fields.textField3}
/>
<button onClick={this.resetTextFields}>
Reset
</button>
<button onClick={this.submit}>
Submit
</button
</div>
);
}
}
I have a form with a field that needs to show suggestions via an api call. The user should be allowed to select one of those options or not and that value that they type in gets used to submit with the form, but this field is required. I am using Formik to handle the form, Yup for form validation to check if this field is required, downshift for the autocomplete, and Material-UI for the field.
The issue comes in when a user decides not to use one of the suggested options and the onBlur triggers. The onBlur always resets the field and I believe this is Downshift causing this behavior but the solutions to this problem suggest controlling the state of Downshift and when I try that it doesn't work well with Formik and Yup and there are some issues that I can't really understand since these components control the inputValue of this field.
Heres what I have so far:
const AddBuildingForm = props => {
const [suggestions, setSuggestions] = useState([]);
const { values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
modalLoading,
setFieldValue,
setFieldTouched,
classes } = props;
const loadOptions = (inputValue) => {
if(inputValue && inputValue.length >= 3 && inputValue.trim() !== "")
{
console.log('send api request', inputValue)
LocationsAPI.autoComplete(inputValue).then(response => {
let options = response.data.map(erlTO => {
return {
label: erlTO.address.normalizedAddress,
value: erlTO.address.normalizedAddress
}
});
setSuggestions(options);
});
}
setFieldValue('address', inputValue); // update formik address value
}
const handleSelectChange = (selectedItem) => {
setFieldValue('address', selectedItem.value); // update formik address value
}
const handleOnBlur = (e) => {
e.preventDefault();
setFieldValue('address', e.target.value);
setFieldTouched('address', true, true);
}
const handleStateChange = changes => {
if (changes.hasOwnProperty('selectedItem')) {
setFieldValue('address', changes.selectedItem)
} else if (changes.hasOwnProperty('inputValue')) {
setFieldValue('address', changes.inputValue);
}
}
return (
<form onSubmit={handleSubmit} autoComplete="off">
{modalLoading && <LinearProgress/>}
<TextField
id="name"
label="*Name"
margin="normal"
name="name"
type="name"
onChange={handleChange}
value={values.name}
onBlur={handleBlur}
disabled={modalLoading}
fullWidth={true}
error={touched.name && Boolean(errors.name)}
helperText={touched.name ? errors.name : ""}/>
<br/>
<Downshift id="address-autocomplete"
onInputValueChange={loadOptions}
onChange={handleSelectChange}
itemToString={item => item ? item.value : '' }
onStateChange={handleStateChange}
>
{({
getInputProps,
getItemProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
}) => (
<div>
<TextField
id="address"
label="*Address"
name="address"
type="address"
className={classes.autoCompleteOptions}
{...getInputProps( {onBlur: handleOnBlur})}
disabled={modalLoading}
error={touched.address && Boolean(errors.address)}
helperText={touched.address ? errors.address : ""}/>
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{suggestions.map((suggestion, index) =>
<MenuItem {...getItemProps({item:suggestion, index, key:suggestion.label})} component="div" >
{suggestion.value}
</MenuItem>
)}
</Paper>
) : null}
</div>
</div>
)}
</Downshift>
<Grid container direction="column" justify="center" alignItems="center">
<Button id="saveBtn"
type="submit"
disabled={modalLoading}
className = {classes.btn}
color="primary"
variant="contained">Save</Button>
</Grid>
</form>
);
}
const AddBuildingModal = props => {
const { modalLoading, classes, autoComplete, autoCompleteOptions } = props;
return(
<Formik
initialValues={{
name: '',
address: '',
}}
validationSchema={validationSchema}
onSubmit = {
(values) => {
values.parentId = props.companyId;
props.submitAddBuildingForm(values);
}
}
render={formikProps => <AddBuildingForm
autoCompleteOptions={autoCompleteOptions}
autoComplete={autoComplete}
classes={classes}
modalLoading={modalLoading}
{...formikProps} />}
/>
);
}
Got it to work. Needed to use handleOuterClick and set the Downshift state with the Formik value:
const handleOuterClick = (state) => {
// Set downshift state to the updated formik value from handleOnBlur
state.setState({
inputValue: values.address
})
}
Now the value stays in the input field whenever I click out.