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.
Related
So I have a submit form where the user needs to create a task by typing in a task name. I want it to be empty at the beginning and have a placeholder of "you must enter a task" when the user click add without entering anything. Now I can achieve it to display the placeholder but it's either always there or I encounter unreachable code. I know how to clean the submission & return to the add function, just need to be able to display the placeholder conditionally. Here's what my code looks like atm:
import { useState } from "react";
export default function Todos() {
const [todos, setTodos] = useState([{ text: "hey" }]);
const [todoText, setTodoText] = useState("");
const [isEmpty, setEmpty] = useState("false");
const addTodo = (e) => {
e.preventDefault();
if (todoText){
setTodos([...todos, { text: todoText }]);
setTodoText("");
} else {
setEmpty(true)
setTodoText("");
return
}
}
return (
<div>
{todos.map((todo, index) => (
<div key={index}>
<input type="checkbox" />
<label>{todo.text}</label>
</div>
))}
<br />
<form onSubmit={addTodo}>
<input
value={todoText}
onChange={(e) => setTodoText(e.target.value)}
type="text"
></input>
<button type="submit">Add</button>
{isEmpty &&<span style={{ color: "red" }}>Enter a task</span>}
</form>
</div>
);
}
I could change your code with the following:
You need to initialize isEmpty by false instead of string "false".
And you can use this flag on showing placeholder texts.
Note that I renamed isEmpty by showError.
import { useState } from "react";
export default function Todos() {
const [todos, setTodos] = useState([{text: "hey"}]);
const [todoText, setTodoText] = useState("");
const [showError, setShowError] = useState(false);
// #ts-ignore
const addTodo = (e) => {
e.preventDefault();
if (todoText) {
setTodos([...todos, {text: todoText}]);
setTodoText("");
setShowError(false);
} else {
setTodoText("");
setShowError(true);
return
}
}
return (
<div>
{todos.map((todo, index) => (
<div key={index}>
<input type="checkbox"/>
<label>{todo.text}</label>
</div>
))}
<br/>
<form onSubmit={addTodo}>
<input
value={todoText}
onChange={(e) => setTodoText(e.target.value)}
type="text"
></input>
<button type="submit">Add</button>
{(showError && !todoText) && <span style={{color: "red"}}>Enter a task</span>}
</form>
</div>
);
}
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
So the problem I am facing is this. Here I have created PaymentForm with Stripe. So when I am not entering the input value of CardHolder name, and when I press the Purchase button it should display the <h1>Please enter your cardholder name</h1> but it is not doing it. I just want to create a test of Cardholder name, I know the documentation of Stripe is showing real approaches. Where could the error be located?
Payment form
import React,{useContext, useEffect, useState} from 'react'
import {CardElement, useStripe, useElements } from"#stripe/react-stripe-js"
import { CartContext } from '../../context/cart'
import { useHistory, Link } from "react-router-dom";
const PaymentForm = () => {
const { total} = useContext(CartContext)
const {cart, cartItems}= useContext(CartContext)
const history = useHistory()
const {clearCart} = useContext(CartContext)
const [nameError, setNameError]=useState(null)
const [name, setName] = React.useState("");
const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState('');
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState('');
const stripe = useStripe();
const elements = useElements();
useEffect(() => {
// Create PaymentIntent as soon as the page loads
window
.fetch("http://localhost:5000/create-payment-intent", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({items: [{cart}]})
})
.then(res => {
return res.json();
})
.then(data => {
setClientSecret(data.clientSecret);
});
}, [cart]);
const cardStyle = {
style: {
base: {
color: "#32325d",
fontFamily: 'Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#32325d"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a"
}
}
};
const handleChange = async (event) => {
setDisabled(event.empty);
setError(event.error ? event.error.message : "");
};
const handleChangeInput = async (event) => {
setDisabled(event.empty);
setNameError(event.nameError ? event.nameError.message : "");
setName(event.target.value)
};
const handleSubmit = async ev => {
ev.preventDefault();
setProcessing(true);
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement)
}
});
if (payload.error) {
setError(`Payment failed ${payload.error.message}`);
setProcessing(false);
} else {
setNameError(null)
setError(null);
setProcessing(false);
setSucceeded(true)
clearCart()
}
};
useEffect(()=>{
setTimeout(() => {
if(succeeded){
history.push('/')
}
}, 3000);
},[history, succeeded])
console.log(name)
return (
<form id="payment-form" onSubmit={handleSubmit}>
<h2>Checkout</h2>
<div className='payment__cont'>
<label>Cardholder Name </label>
<input
placeholder='Please enter your Cardholder name'
type="text"
id="name"
value={name}
onChange={handleChangeInput}
/>
</div>
<div className="stripe-input">
<label htmlFor="card-element">
Credit or Debit Card
</label>
<p className="stripe-info">
Test using this credit card : <span>4242 4242 4242 4242</span>
<br />
enter any 5 digits for the zip code
<br />
enter any 3 digits for the CVC
</p>
</div>
<CardElement id="card-element" options={cardStyle} onChange={handleChange} />
<div className='review__order'>
<h2>Review Order</h2>
<h4>Total amount to pay ${total}</h4>
<h4>Total amount of items {cartItems}</h4>
<button
className='purchase__button'
disabled={processing || disabled || succeeded}
id="submit"
>
<span id="button-text">
{processing ? (
<div className="spinner" id="spinner"></div>
) : (
"Complete purchase"
)}
</span>
</button>
<button className='edit__button'onClick={()=> {history.push('/cart')}}>Edit Items</button>
</div>
{error && (
<div className="card-error" role="alert">
{error}
</div>
)}
{nameError && (
<div className="card-error" role="alert">
<h1>Please enter yor card holder name</h1>
</div>
)}
<p className={succeeded ? "result-message" : "result-message hidden"}>
Payment succeeded
{''}
<h1>Redirecting you yo the home</h1>
</p>
</form>
);
}
export default PaymentForm
You still need to validate your name input yourself. Stripe doesn't do that for you
Your handleChangeInput handler only fires when you write to your name input, and you're treating the event as if it's fired from a Stripe component, but it's not, so try this:
// Validates name input only
const handleChangeInput = async (event) => {
const { value } = event.target;
// Disable when value is empty
setDisabled(!value);
// Set the error if value is empty
setNameError(value ? "" : "Please enter a name");
// Update form value
setName(value)
};
I'm not sure what you want to do is possible with Stripe elements as cardholder name isn't actually part of it. You'll need to handle it yourself.
An alternative way of ensuring the name field is entered prior to the card element being pressed would be to force input into cardholder name field before displaying/enabling the Stripe Card Element. This way you can be more certain the the name has been entered (and then you can do what you like with it) before the card element is pressed. You could do this a lot of ways, but for example in your render:
{name.length >= 10 ? ( //Check length of name
<CardElement id="card-element" options={cardStyle} onChange={handleChange} />
) : (
<span>Please enter your cardholder name.</span> // do nothing if check not passed
)}
This is a really simple example but it checks the length of name and then if greater than or equals ten characters makes the card element visible. You could instead use your handleChangeInput to set a boolean state (true or false) on button press or something; it would be better to make this more robust.
edit: some clarifications.
I have two buttons on a page and based on a state in that component, a particular button should be displayed, I have tried for hours, still not working
Here is my code
const App = () =>{
const [edit,setEdit] = useState(false)
const updateUser =() =>{
//update action
setEdit(false)
}
return(
<div>
<form>
<input type="text"/>
<input type="text"/>
{edit ? (<button onClick={()=>updateUser()}>Save</button>) : (<button onClick={()=>{setEdit(true)}}>Edit</button>)}
</form>
</div>
)
}
export default App;
when the page loads the button shows Edit, but after clicking, i expect it to change to save since edit is now true, but it still remains same
you have a side effect in this situation caused by edit so you should make use of good old pal useEffect also those prevenDefaults will prevent your form from refreshing and are necessary. I made a livedemo at codeSandbox and here is the code itself:
import React, { useState, useEffect } from "react";
const App = () => {
const [edit, setEdit] = useState(false);
const updateUser = (e) => {
e.preventDefault();
//update action
setEdit(false);
};
const editUser = (e) => {
e.preventDefault();
//stuffs you wanna do for editing
setEdit(true);
};
useEffect(() => {}, [edit]);
return (
<div>
<form onSubmit={(e) => updateUser(e)}>
<input type="text" />
<input type="text" />
{edit ? (
<button type="submit">Save</button>
) : (
<button onClick={(e) => editUser(e)}>Edit</button>
)}
</form>
</div>
);
};
export default App;
P.S: Although it works, I don't approve of the approach
You can try this approach also, Here I used e.preventDefault on the event when submitting the form to prevent a browser reload/refresh.
const App = () =>{
const [edit,setEdit] = useState(false)
const updateUser =(e) =>{
//update action
e.preventDefault();
setEdit(!edit);
}
return(
<div>
<form>
<input type="text"/>
<input type="text"/>
{edit ? (<button onClick={updateUser}>Save</button>) :
(<button onClick={updateUser}>Edit</button>)}
</form>
</div>
)
}
export default App;
Not need to using useEffect just check if edit is true or not !
EDIT : I made it this way so u can see what happend and i think u should learn more about react and hooks , i prefer watching youtube!
const App = () => {
const [edit, setEdit] = React.useState(true);
const clickHandler =(e)=>{
e.preventDefault();
if(!edit){
// update user information here not need to make
// different function and state or somthing else.
console.log("updateUser")
}
setEdit(prev=>!prev);
}
return(
<div>
<form>
<input type="text"/>
<input type="text"/>
<button onClick={clickHandler} disabled={!edit}>Edit</button>
<button onClick={clickHandler} disabled={edit}>Save</button>
</form>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Issue
As I see it, your issue is that the buttons don't have a type specified to them, so they actually inherit an initial value of type="submit" and this causes your form to take the default submit action and reload the page. You likely aren't seeing this reload as it may be occurring very quickly.
type
The default behavior of the button. Possible values are:
submit: The button submits the form data to the server. This is the default if the attribute is not specified for buttons associated
with a <form>, or if the attribute is an empty or invalid value.
reset: The button resets all the controls to their initial values, like <input type="reset">. (This behavior tends to annoy users.)
button: The button has no default behavior, and does nothing when pressed by default. It can have client-side scripts listen to the
element's events, which are triggered when the events occur.
Solution
Provide/specify the button types to not be "submit and valid, i.e. use type="button".
const App = () => {
const [edit, setEdit] = React.useState(false);
const updateUser = () => {
//update action
setEdit(false);
};
return (
<div>
<form>
<input type="text" />
<input type="text" />
{edit ? (
<button type="button" onClick={updateUser}>
Save
</button>
) : (
<button
type="button"
onClick={() => {
setEdit(true);
}}
>
Edit
</button>
)}
</form>
</div>
);
};
Here is an example that also, IMO, make the code a bit more DRY.
const App = () => {
const [edit, setEdit] = React.useState(false);
const updateUser = () => {
//update action
setEdit(false);
};
return (
<div>
<form>
<input type="text" />
<input type="text" />
<button
type="button"
onClick={() => {
edit ? updateUser() : setEdit(true);
}}
>
{edit ? "Save" : "Edit"}
</button>
</form>
</div>
);
};
Demo
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 :)