I built a multistep form using react-hook-form with a dynamic fields array using useFieldArray.
Documentation: useFieldArray documentation
Here is the full working code link: React Multi-step form with useFieldArray
In the 2nd step when I add new fields using add a dog button, everything works fine, the new data of step is saved to localstorage using little state machine.
But when I click the previous button, the added fields disappear whereas data is still in localstorage.
code for 2nd step:
import { useForm, useFieldArray } from "react-hook-form";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";
import { useNavigate } from "react-router-dom";
function Step2(props) {
const {
register,
control,
handleSubmit,
watch,
formState: { errors },
} = useForm({
defaultValues: {
test: [{ nameOfDog: "Bill", ageOfDog: "2", sizeOfDog: "small" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
shouldUnregister: true,
name: "test",
});
const elements = watch("test");
console.log(elements, fields);
const { actions, state } = useStateMachine({ updateAction });
const navigate = useNavigate();
const onSubmit = (data) => {
// console.log(fields);
actions.updateAction(data);
navigate("/step3");
};
let dta;
if (state.date2) {
dta = new Date(state.date2);
} else {
dta = new Date();
dta.setDate(dta.getDate() + 1);
}
return (
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="stepn stepn-active" data-step="1">
{fields.map((item, index) => {
return (
<div className="row" key={item.id}>
<div className="col">
<label htmlFor="nameOfDog">Name:</label>
<input
id="nameOfDog"
{...register(`test.${index}.nameOfDog`, {
required: true,
})}
defaultValue={item.nameOfDog}
/>
{errors.nameOfDog && (
<span>This field is required</span>
)}
</div>
<div className="col">
<label htmlFor="ageOfDog">Age:</label>
<input
id="ageOfDog"
type="number"
{...register(`test.${index}.ageOfDog`, {
required: true,
})}
defaultValue={item.ageOfDog}
/>
{errors.ageOfDog && (
<span>This field is required</span>
)}
</div>
<div className="col">
<label htmlFor="sizeOfDog">Size in Lbs:</label>
<select
id="sizeOfDog"
{...register(`test.${index}.sizeOfDog`, {
required: true,
})}
defaultValue={item.sizeOfDog || ""}
>
<option value="small">Small (40)</option>
<option value="large">Large (40+)</option>
</select>
{errors.sizeOfDog && (
<span>Please Select an option</span>
)}
</div>
<div className="col">
<button
onClick={(e) => {
e.preventDefault();
remove(index);
}}
style={{ padding: "26px 62px" }}
>
Delete
</button>
</div>
</div>
);
})}
<div className="row">
<div className="col">
<button
onClick={(e) => {
e.preventDefault();
append({
nameOfDog: "Bill2",
ageOfDog: "5",
sizeOfDog: "large",
});
}}
>
Add a Dog
</button>
</div>
</div>
</div>
{/* <input type="submit" /> */}
<div className="row">
<button className="prev" onClick={() => navigate("/")}>
Previous
</button>
<button className="next">Next</button>
</div>
</form>
);
}
export default Step2;
{fields.map((item, index) =>
whenever the previous button is clicked, fields array resets to default.
All the remaining steps of the form except 2nd step is being saved when we go back to previous step.
How do i keep the fields in the 2nd step saved when I click the previous button.
There are two problems here:
you don't update your state in "Step 2" when you click on the "Previous" button. So you have to pass the current form data to your state machine. Additionally you also have no form validation for "Step 2" right now, when you want to go a previous step. To add support for validation you should move handleSubmit from the <form /> element and instead pass it to your two <button /> elements. This way you can get rid of the watch call as you have the current form data inside the handleSubmit callback.
const onPrevious = (data) => {
actions.updateAction(data);
navigate("/");
};
const onNext = (data) => {
actions.updateAction(data);
navigate("/step3");
};
<div className="row">
<button className="prev" onClick={handleSubmit(onPrevious)}>
Previous
</button>
<button className="next" onClick={handleSubmit(onNext)}>
Next
</button>
</div>
If you want to keep handleSubmit in the <form /> element, you should use watch and pass the data to your state machine before you navigate back to the previous step.
const test = watch("test");
const onPrevious = (data) => {
actions.updateAction({ test });
navigate("/");
};
as you reinitialise each step component on a step change you have to pass the current defaultValues to useForm for each step. For "Step 2" it would look like this:
const {
register,
control,
handleSubmit,
watch,
formState: { errors }
} = useForm({
defaultValues: {
test: state.test ?? [
{ nameOfDog: "Bill", ageOfDog: "2", sizeOfDog: "small" }
]
}
});
The important thing to change is, that when you pass the defaultValues for your fields within the useForm config, you should remove it from the <Controller /> components. I made it for "Step 1", so you have an example there.
It's very long but maybe we can figure it out.
The use is correct, the problem in my opinion is that you're not checking the state and just printing the default values everytime
Related
I am using Formik form in my React application to create a new post and I have an input field of type "file". The problem I face now is I cannot implement a cancelation of the action. With the code I am proving I do update the form values and the preview image disappears but I still can see the name of the file on the screen which should be "no file chosen" instead. I went through Formik documentation but did not find a solution. Any ideas will help. Thank you.
Setting value={values.image} gives these errors:
Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string
A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
import { Formik, Field } from "formik";
import { createPost } from "../actions";
const NewPost = () => {
const dispatch = useDispatch();
const history = useHistory();
const handleImageUpload = async (event, setFieldValue) => {
// .... some code where I obtain the image URLs
setFieldValue("image", file.secure_url);
setFieldValue("largeImage", file.eager[0].secure_url);
};
return (
<div>
<Formik
initialValues={{ image: "", largeImage: "", title: "", body: "" }}
validate={(values) => {
const errors = {};
//... validation
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
const { title, body, image, largeImage } = values;
dispatch(
createPost(
{
title,
body,
image,
largeImage,
},
history
)
);
setSubmitting(false);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
setFieldValue,
isSubmitting,
/* and other goodies */
}) => (
<form onSubmit={handleSubmit} className="new__post-form">
<div>
<label htmlFor="image">Image</label>
<div>
<div>
<label htmlFor="image">Upload File</label>
<input
type="file"
type="file"
name="image"
id="image"
placeholder="Upload an image"
onChange={(e) => handleImageUpload(e, setFieldValue)}
onBlur={handleBlur}
/>
</div>
<div>
{values.image && (
<>
<img src={values.image} alt="Upload Preview" />
<button
type="button"
onClick={() => {
setFieldValue("image", "");
setFieldValue("largeImage", "");
}}
>
X
</button>
</>
)}
</div>
</div>
</div>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
</Formik>
</div>
);
};
export default NewPost;
I have created dynamic fields from JSON data, and I am successfully rendering on UI
Initially all the fields are disabled.
Once I click on edit I am making particular row editable which is working fine
On click of cancel what I want to do is make the fields disabled again and it should take the previous (initial value)
Issue
When I click on cancel I am setting the initial data aging but it is not taking, I am using react-form-hook for form validation, there we have reset() function but that too is not working.
What I am doing is
Getting data from main component and setting it to some state variable like below
useEffect(() => {
if (li) {
setdisplayData(li);
setCancelData(li);
}
}, [li]);
Now using displayData to render the elements
On click of Edit I am doing this
const Edit = () => {
setdisabled(false);
};
and on click of cancel I am doing below
const cancel = () => {
setdisabled(true); //disbaled true
console.log(cancelData);
setdisplayData(cancelData); setting my main data back to previous one
reset(); // tried this reset of react hook form but it did not work
};
I am using defaultValue so that when I click on Edit the field should allow me to edit.
Here is my full working code
To fix this issue I changed up your code to use value instead of defaultValue. Additionally added an onChange event handler which updates the displayData state whenever <input> changes value. Moreover, you do not need the cancelData state at all since the li prop has the original values.
Now when the onClick for the cancel button is fired, it resets the value of displayData state to whatever li originally was. Here is the modified code:
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
function component({ li, index }) {
const [disabled, setdisabled] = useState(true);
const [displayData, setdisplayData] = useState(null);
const { register, reset, errors, handleSubmit, getValues } = useForm();
useEffect(() => {
if (li) {
setdisplayData(li);
}
}, [li]);
const Edit = () => {
setdisabled(false);
};
const cancel = () => {
setdisabled(true);
console.log(li);
// Reset displayData value to li
setdisplayData(li);
reset();
};
return (
<div>
<div>
{disabled ? (
<button className="btn btn-primary" onClick={Edit}>
Edit
</button>
) : (
<button className="btn btn-warning" onClick={cancel}>
Cancel
</button>
)}
</div>
<br></br>
{displayData !== null && (
<>
<div className="form-group">
<label htmlFor="fname">first name</label>
<input
type="text"
name="fname"
disabled={disabled}
value={displayData.name}
// Update displayData.name everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, name: value }))
}
/>
</div>
<div className="form-group">
<label htmlFor="lname">last name</label>
<input
type="text"
name="lname"
disabled={disabled}
value={displayData.lname}
// Update displayData.lname everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, lname: value }))
}
/>
</div>
</>
)}
<hr></hr>
</div>
);
}
export default component;
Hope this helps. Drop a comment if it's still not clear :)
I am working on a login form where each input is created dynamically as a field.
This is my Login.js file:
import _ from 'lodash';
import React, { Component } from 'react';
import {reduxForm, Field } from 'redux-form';
import{ Link } from 'react-router-dom';
import FIELDS from './loginFields';
import LoginField from './LoginField'
import { connect } from 'react-redux';
import * as actions from '../../actions'
class LoginForm extends Component {
constructor(){
super();
this.state={
username: '',
password: ''
};
};
handleChange = (e)=>{
this.setState({username: e.target.value, password: e.target.value});
};
renderFields(){
return _.map(FIELDS, ({ label, name, type })=> {
return <Field onChange={this.handleChange} className='purple-text' key={name} component={ LoginField } type={type} label={label} name={name} />
});
};
render(){
const { username, password } = this.state;
const isEnabled = username.length > 0 && password.lenth>7;
return (
<div className='valign-wrapper row login-box' style={{marginTop:'100px'}}>
<div className='col card hoverable s10 pull-s1 m6 pull-m3 l4 pull-l4'>
<form method='POST' action='/api/login'>
<div className = 'card-content'>
<span className='card-title purple-text' style={{textAlign:'center'}}>Login<a href='/register'> Not a member? sign up!</a></span>
<div className='center-align row'>
<li key='google' style={{marginLeft: '30px'}} className='col m6 center-align white-text darken-3'><a className='white-text' href='/auth/google'><img alt="" src="https://img.icons8.com/cute-clipart/64/000000/google-logo.png"/></a></li>
<li key='facebook' className='col center-align white-text darken-3'><a className='white-text' href='/auth/facebook'><img alt = "" src="https://img.icons8.com/cute-clipart/64/000000/facebook-new.png"/></a></li>
</div>
<div className='row input-field col s12'>
{this.renderFields()}
<Link to='/' className='purple btn-flat left white-text'>Back</Link>
<button disabled={!isEnabled} type='submit' className='purple btn-flat right white-text'>Login
<i className='material-icons right'>done</i>
</button>
</div>
</div>
</form>
</div>
</div>
);
};
};
function validate(values){
const errors = {};
_.each(FIELDS, ({name})=>{
if(!values[name]){
errors[name] = 'You must enter a value!'
}
});
return errors;
};
const form = reduxForm({
validate,
form: 'LoginForm'
});
export default connect(null, actions)(form(LoginForm));
Here is loginFields.js
export default
[
{ label: 'Username', name: 'username', type: 'text'},
{ label: 'Password', name: 'password', type: 'password'}
];
and here is LoginField.js
import React from 'react';
export default ({ input, label, type, meta })=>{
return(
<div>
<label className='purple-text'>{label}</label>
<input {...input} type={type} style= {{marginBottom: '5px'}}/>
<div className = "red-text" style={{ marginBottom: '20px'}}>
{meta.touched && meta.error}
</div>
</div>
);
};
I am having trouble properly setting onChange and my constructor to disable the login button until all fields are filled. I have been able to disable the button until a single input has started to be filled in, not disabled at all, and not enabled at all. but have not been able to achieve the desired outcome.
I have tried using lodash to map over each field grabbing values by the input name property, and moving functions around.
Any help would be greatly appreciated, if i can provide any more information for this question please let me know.
The initial problem I see is the onChange function will update state for both password and username whenever either of them is changed. The function takes the event and does not distinguish as to which input is the target. You can pass an additional parameter from the Field that includes the field name, or you can check the target's id or something so you know which input's state should be updated.
In LoginForm
handleChange = (e, name)=>{
this.setState({[name]: e.target.value});
};
You also need to pass the onChange callback down to the actual input in LoginField.js
import React from 'react';
export default ({ name, label, type, meta, onChange, ...props })=>{
return(
<div>
<label className='purple-text'>{label}</label>
<input onChange={(e) => onChange(e, name)} {...props} type={type} style= {{marginBottom: '5px'}}/>
<div className = "red-text" style={{ marginBottom: '20px'}}>
{meta.touched && meta.error}
</div>
</div>
);
};
Here's a codeSandbox.
just adding this as an answer in case anyone else comes across this issue.
after tons of digging I finally found documentation. in redux form has a built in prop called {invalid} which checks against the validate function. instead of messing with state all i had to do was add
const {invalid} = this.props;
inside the render method. constructor and handle change and onChange were no longer necessary.. then.
<button disabled={invalid}>
I have a form inside which i am showing edit,save and cancel button logically, so initially edit button is visible and all inputs are disabled, and when I click on edit I am making my save and cancel button visible and edit not visible.
So after filling some data when user click on save I am checking the validation like required fields, so if error then user can see.
After then on click of edit if user do not want to save then I am filling the data in site for to the initial values, but if there is error on click of save and I am clicking cancel still the error is there it is not going away,
What I am doing wrong
I think on click when i am filling my formdata to initial value.
if above point is correct then why error is still visible
my code
import React, { useState, useEffect } from "react";
import "./styles.css";
import { useForm } from "react-hook-form";
// mock for useQuery
const useQuery = query => {
const [loading, setIsLoading] = useState(true);
const [data, setData] = useState({});
useEffect(() => {
setTimeout(() => {
setIsLoading(false);
setData({ firstname: "steve", lastname: "smith" });
}, 1000);
}, []);
return { loading, data };
};
export default function App() {
const { register, handleSubmit, errors } = useForm();
const [disabled, setdisabled] = useState(true);
const [editBtn, seteditBtn] = useState(true);
const [initialData, setinitialData] = useState({});
const { loading, data } = useQuery("some qraphql query here"); // getting data from graphql
const [formData, setFormData] = useState({});
useEffect(() => {
setFormData(data);
setinitialData(data);
}, [data]);
const edit = () => {
setdisabled(false);
seteditBtn(false);
};
const cancel = () => {
setFormData(initialData);
setdisabled(true);
seteditBtn(true);
};
const onSubmit = () => {
console.log(formData);
};
const handleChange = e => {
const name = e.target.name;
const value = e.target.value;
console.log(name, value);
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<div className="container-fluid">
<form onSubmit={handleSubmit(onSubmit)}>
{editBtn === true && (
<div align="right">
<button
className="btn white_color_btn"
type="button"
onClick={edit}
>
Edit
</button>
</div>
)}
{editBtn === false && (
<div>
<button className="btn white_color_btn" type="submit">
Save
</button>
<button
className="btn white_color_btn"
type="submit"
onClick={cancel}
>
Cancel
</button>
</div>
)}
<div className="row">
<div className="form-group col-6 col-sm-6 col-md-6 col-lg-4 col-xl-4">
<input
type="text"
id="firstname"
name="firstname"
onChange={handleChange}
value={formData.firstname}
disabled={disabled}
ref={register({ required: true })}
/>
{errors.firstname && (
<span className="text-danger">first name required</span>
)}
<br />
<label htmlFor="emp_designation">First name</label>
</div>
<div className="form-group col-6 col-sm-6 col-md-6 col-lg-4 col-xl-4">
<input
type="text"
id="lastname"
name="lastname"
value={formData.lastname}
onChange={handleChange}
disabled={disabled}
ref={register({ required: true })}
/>
{errors.lastname && (
<span className="text-danger">last name required</span>
)}
<br />
<label htmlFor="lastname">Lastname</label>
</div>
</div>
</form>
</div>
);
}
To check the issue follow this points
click on edit -> empty the field -> then click save -> it will throw error -> then click cancel.
on cancel click I want error should go away
Working code codesandbox
The errors are present because they're managed by useForm. The hook exposes a function reset that should fix your problem. Here is an example that leverage the function.
const { register, handleSubmit, reset, errors } = useForm();
// ...
const cancel = () => {
setFormData(initialData);
setdisabled(true);
seteditBtn(true);
reset();
};
A simple pattern is you set errors in state and clear the values of errors object to null or empty upon clicking cancel Button or when a valid input is typed. Here you can initialize errors and reset on cancel button's click. So you should update errors every time input value is changed or cancel button is clicked.
I am using the bootstrap-typehead:https://github.com/ericgio/react-bootstrap-typeahead and I cannot figure out why this package is freaking out. Whats wrong with this code that it gives me this error:
Failed prop type: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.
import React, { Component } from "react";
import Dropdown from "../../bootstrap/Dropdown";
import RealmAPIs from "../../../API/realms/RealmAPI";
import {Typeahead} from 'react-bootstrap-typeahead';
import AutoComplete from "../../bootstrap/AutoComplete";
// import RealmAPI from '../../../API/realms/RealmAPI';
var options = [
'John',
'Miles',
'Charles',
'Herbie',
];
export default class FindCharacter extends Component {
state = {
realmName: "",
characterName: "",
realms: []
};
setRealmName = value => {
this.setState({ realmName: value });
};
componentDidMount() {
// let realms = [...this.state.realms];
// RealmAPIs.getAllRealms().then(response =>
// console.log(response.realms.map(value => {}))
// );
}
render() {
return (
<div>
<form className="form-inline justify-content-md-center">
<div className="form-group mb-2">
{/* <Dropdown setRealmName={this.setRealmName}/> */}
<Typeahead
labelKey="name"
placeholder="Type a realm"
onChange={selected => {
console.log(selected);
}}
options={
options
}
/>
</div>
<div className="form-group mx-sm-3 mb-2">
<input
type="text"
className="form-control"
id="characterName"
placeholder="Enter Character Name"
/>
</div>
<button
type="submit"
className="btn btn-primary mb-2"
onClick={e => {
e.preventDefault();
console.log(this.state.name);
}}
>
Submit
</button>
</form>
</div>
);
}
}
Upgrading to v3.2.3 of react-bootstrap-typeahead should solve the issue.
I'm not sure why, but React v16.5 (I think?) started triggering the warning. There was an issue tracking it as well as a PR fixing it.