React Hook Forms - Adding Dynamic Rows of Fields - reactjs

I'm using React Hook Form library. https://react-hook-form.com
If it's a simple form like Student Name, and Student Age, the application is quite simple.
const { register, handleSubmit, formState: { errors } } = useForm();
I just create those two input fields and register them.
<div className="w-full flex flex-col mt-4">
<label>Name</label>
<input
type="text"
placeholder="Enter Name" {...register("name")}
/>
</div>
<div className="w-full flex flex-col mt-4">
<label>Age</label>
<input
type="text"
placeholder="Enter Age" {...register("age")}
/>
</div>
<div>
<button onClick={handleSubmit(submitData)}>
Update
</button>
</div>
The submitData function will get the formData which can be used. The handleSubmit binds the registered fields to formData
const submitData = async formData => {
console.log(formData)
}
The formData will look as below:
{"name":"test", "age":"27"}
My requirement is to make this form dynamic, by allowing me to add many students. I should be able to repeat a set of these fields with a button called "Add Student". Every time I add a new student, it should create a new row of these two fields where I can add new students names. finally, the output of the formData should look like an array of students:
[{"name":"test1", "age":27},{"name":"test2", "age":28},{"name":"test3", "age":29} ]
I can create the UI to add the new fields, but I don't know how to bind them to the formData in react hook form. In the documentation, I couldn't find how to do this with react hook form.
Please help.

I tried the following:
I created a state as follows:
const [items, setItems] = useState([0,1,2])
I can update this when I add/remove items.
Now, for the fields, I wrapped them into a map as follows:
{items.map(i => (
<div>
<div className="w-full flex flex-col mt-4">
<label>Name</label>
<input
type="text"
placeholder="Enter Name" {...register(`items[${i}].name`)}
/>
</div>
<div className="w-full flex flex-col mt-4">
<label>Age</label>
<input
type="text"
placeholder="Enter Age" {...register(`items[${i}].age`)}
/>
</div>
</div>
))}
This works.

Related

I have made a form I want to put an alert if any of the field is empty and also want the form to not accept data if the field is empty

I have made a form I want to put an alert if any of the field is empty and also want the form to not accept data if the field is empty.
i have not tried anything coz not able to find a solution for it. below is the code i have done till now.
I have linked the form to a database but u want the form not to accept data if the any of the field is empty and send alert that the fields are empty.
`import React,{useState} from 'react'
import Axios from 'axios'
function PostForm(){
const url ="http://localhost:5000/qo"
const [data, setData] = useState({
Employee_name:"",
Employee_id:"",
Employee_address:"",
Employee_post: ""
})
function submit(e){
e.preventDefault();
Axios.post(url,{
Employee_name: data.Employee_name,
Employee_id: data.Employee_id,
Employee_address: data.Employee_address,
Employee_post: data.Employee_post
})
.then(res=>{
console.log(res.data)
})
alert('Employee Data Saved')
}
function handle(e){
const newdata = {...data}
newdata[e.target.id] = e.target.value
setData(newdata)
console.log(newdata)
}
return(
<div>
<div>
<h2>Registration Form</h2>
</div>
<form onSubmit={(e)=> submit(e)}>
<label>Employee Name : </label>
<input onChange={(e)=>handle(e)} id="Employee_name" value={data.Employee_name} placeholder ="Employee name" type ="text"></input>
<br/>
<br/>
<label>Employee id : </label>
<input onChange={(e)=>handle(e)} id="Employee_id" value={data.Employee_id} placeholder ="Employee id" type ="number"></input>
<br/>
<br/>
<label>Employee Address : </label>
<input onChange={(e)=>handle(e)} id="Employee_address" value={data.Employee_address} placeholder ="Employee address" type ="text"></input>
<br/>
<br/>
<label>Employee Position : </label>
<input onChange={(e)=>handle(e)} id="Employee_post" value={data.Employee_post} placeholder ="Employee position" type ="text"></input>
<br/>
<br/>
<button>Submit</button>
</form>
</div>
)
}
export default PostForm;`
Just add the required in all inputs
<input ...all_things required={true} />

URL search parameters gets replaced - Remix Run

I'm working on a search UI where I have quite a few filters which I want as URL parameters when someone selects/checks the options. I've used the technique as advised on the Remix.run docs to come up with multiple forms within the filters. Each time a group of Filters gets submitted, the selected old parameters get disappeared. Heres my code,
<Panel header="Status" key="status">
<Form
name="search"
action='/search'
method="get"
onChange={(e) => submit(e.currentTarget, { replace: false })}
>
<ul>
<li>
<Checkbox
name="status"
value="buy_now"
defaultChecked={status.includes('buy_now')}
>
Buy Now
</Checkbox>
</li>
<li>
<Checkbox
name="status"
value="on_auction"
defaultChecked={status.includes('on_auction')}
>
On Auction
</Checkbox>
</li>
</ul>
</Form>
</Panel>
<Panel header="Price" key="price">
<Form name="search" action='/search' method="get">
<Select
name="blockchain"
value={
blockchain
? options.filter((a) => a.value === blockchain)
: undefined
}
options={options}
placeholder="Blockchain"
type="white"
/>
<div className="d-flex align-center price">
<TextInput
value={min ? min : undefined}
name="min"
placeholder="Min"
/>
<span>to</span>
<TextInput
value={max ? max : undefined}
name="max"
placeholder="Max"
/>
</div>
<button
onClick={(e) => {
e.stopPropagation()
submit(e.currentTarget, { replace: false })
}}
className="btn primary-btn btn-lg w-100"
>
Apply
</button>
</Form>
</Panel>
How Can I get around this to have all the parameters without having to manage them on my own using React state?
Edit:- I want the first filter to be submitted automatically and the latter to be submitted on a button click.
Bit of a UI of what I'm trying to achieve,
Answer: After investing enough time to look through for shortcuts, finally understood that it's not one of the magic that remix.run does. use something like formik and update the state imparatively.
When you submit a form, the only values included are the one under the submitted form. The values from any other form are not included (fortunately!).
So I'd use a single Form with all the inputs under it (checkboxes as well as text inputs).
Then instead of a onChange on the Form, you can add something like an onChange handler on the checkboxes and submit the form inside imperatively (using a ref click on the submit button or something, I think using a ref on the form you need to submit all values in the submit function so a button ref click may be simpler).
Keep in mind that if you want to "restore" the field values after submitting, you need to return them from the loader function like this:
// Loader function
const url = new URL(request.url);
return {
results: [...],
values: Object.fromEntries(url.searchParams.entries())
};
Then in the component, use values from useLoaderData:
<input type="text" name="max" defaultValue={values.max || ""}/>
Added benefit: if you come back to this page (by clicking browser back for example), your search parameters and search results are still there!
I actually put up a stackblitz for you but I lost all my changes :(
It seems like you could just keep all fields in a single form and submit that form when the submit button is pressed.
Then onChange, check if the target's name is 'status', and submit the form anyway.
export default function App() {
const submit = (form) => {
form.submit();
};
return (
<form
name="search"
action="/search"
method="get"
onChange={(e) => {
if (e.target.name === "status") {
submit(e.currentTarget);
}
}}
>
<fieldset>
<legend>status</legend>
<label>
<input type="checkbox" name="status" value="buy_now" />
buy_now
</label>
<label>
<input type="checkbox" name="status" value="on_auction" />
on_auction
</label>
</fieldset>
<fieldset>
<legend>price</legend>
<label>
<div>blockchain</div>
<select name="blockchain">
<option value="option_1">Blockchain Option 1</option>
<option value="option_2">Blockchain Option 2</option>
</select>
</label>
<label>
min <input type="number" name="min" />
</label>
<label>
max <input type="number" name="max" />
</label>
</fieldset>
<button type="submit">Apply</button>
</form>
);
}
demo
Note: not sure what your motivation is to want to separate this into separate forms, but I think the magic you're referring to is that server state, URLSearchParams, FormData and UI are all aligned because they are using the same underlying data using only common web APIs.

How do I clear the textarea's value when clicked on the addbtn in reactjs?

I used the following code, but the text does not clear when I click the addbtn.
This is a reactjs project.
function Addpage() {
const[note, setnote] = useState("");
function textchange(e) {
setnote(e.target.value);
console.log(note);
};
function savenote(){
localStorage.setItem('savednotes', note);
setnote("");
};
return (
<>
<Headersmall/>
<br/>
<div className="addcard">
<h1>Add new note.</h1>
<div className="title">
<input type="text" className="titlebox" id="addtitle" placeholder="Title"/>
</div>
<br/>
<div className="note">
<textarea type="text" className="notebox" id="addtxt" placeholder="Note" onChange = {textchange}/>
</div>
<br/>
<button className="addbtn" id='addbtn' onClick = {savenote}>Save</button>
</div>
<br/>
</>
)
}
When you're setting the value of note as an empty string by doing setnote("") inside savenote() function, you ARE changing the value of the state variable note, but it doesn't reflect in the textarea because your textarea input doesn't have a value associated to it. Try adding value={note} which will mean that there will be a "set and fetch" relationship between the textarea and the 'note' state variable
<textarea
value={note}
type="text"
className="notebox"
id="addtxt"
placeholder="Note"
onChange={textchange}
>

onChange using Formik Validation

I'm currently having getting my input values to appear on screen as im typing. For example, I have a form that requires a first name and last name. Upon typing in those values I am trying to display the inputs typed onto the DOM. This was successful earlier with an onChange, and using this.state.firstName, this.state.lastName. I have implemented formik validation and currently my inputs only appear after I upload an image which has a set state.
I have an onChange passing its values
handleChange = event => {
this.setState({ [event.target.name]: event.target.value });
};
<---->
<Formik
initialValues={this.state.formData}
enableReinitialize={true}
validationSchema={userProfileValidation}
onSubmit={this.handleSubmit}
render={formikProps => (
<div className="container-fluid">
<div className="edit-profile">
<div className="row">
<div className="col-lg-4">
<div className="card">
<div className="card-header">
<h3 className="card-title">My Profile</h3>
</div>
<div className="card-body">
<div className="row mb-2">
<div className="col-auto">
<img
className="img-90 rounded-circle"
alt=""
src={
this.state.formData.avatarUrl
? this.state.formData.avatarUrl
: "https://iupac.org/wp-
content/uploads/2018/05/default-avatar.png"
}
/>
</div>
<div className="col">
<h4 className="mb-1">
{this.state.formData.firstName} {""}{" "}
{this.state.formData.mi} {""}{" "}
{this.state.formData.lastName}{" "}
</h4>
</div>
</div>
</div>
</div>
I am able to show what input on a setState but live like it previously shown.
You're trying to mix two separate things. Formik exist so that you don't have to manage your component level form state by yourself doing so is hard and formik does that for you.
You should pass an object containing form initial field values to initialValues prop instead of this.state.formData
To update a DOM value based on a field input somewhere, you can do this
<Field
name="email"
type="email"
onChange={e => {
// call the built-in handleChange for formik
handleChange(e)
// and do something about e
let someValue = e.currentTarget.value
this.updateEmailField(someValue) // Update a DOM element on this function
...
}}
/>

React 'fetch' response successful, but setState not working

I'm new to React, working through a ".Net core CRUD with React" tutorial and trying to tweak it along the way to suit my own needs.
The page I'm dealing with here is an Add/Edit entry page. It works fine for rendering a default form with default values but doesn't render anything if the values are collected from a fetch call.
The important details are below:
interface AddPortfolioProjectDataState {
title: string;
projectData: PortfolioProject;
loading: boolean;
}
The page is told to render as follows:
public render() {
let contents = this.state.loading
? <p><em>Loading Project...</em></p>
: this.renderCreateForm(this.state.projectData.type, this.state.projectData.tech);
return (
<div>
<h1>{this.state.title}</h1>
<h3>Project</h3>
<hr />
{contents}
</div>
)
}
If I want to add a new item, therefore using a default PortfolioProject object with default values, it works fine. However, if I want to edit an old entry, I have to grab it from the server, like so:
fetch('api/StuartAitkenWebsite/GetPortfolioProject/' + projID)
.then(response => response.json() as Promise<PortfolioProject>)
.then(data => {
this.setState({ title: "Edit", loading: false, projectData: data });
});
In the debug console on Firefox, I can see the whole server process runs smoothly:
GET http://localhost:62669/api/StuartAitkenWebsite/GetPortfolioProject/2
Response payload: {"id":2,"name":"Particles Sim","projectDate":"2017-01-01T00:00:00","projectDurationWeeks":1,"type":"Desktop App","tech":"C++, SFML","views":0,"creationDate":"2018-10-22T00:00:00","modifiedDate":"2018-10-22T00:00:00","status":1}`
It gives a JSON output of the payload too, which I can't easily copy-paste here so I'll give a screenshot:
There are no server error responses, no React errors, nothing.
But that's as far as it gets.
The page remains showing 'loading', even though the data is there and ready and wants to be displayed.
From this, I can gather that the final step of the fetch call is not succeeding, because this.setState({ title: "Edit", loading: false, projectData: data }); is clearly not having any effect on the page data.
I have other fetch calls which look exactly the same but work fine. I can't see what I'm missing here.
The one and the only difference I notice is this:
When I use this component to create a fresh 'Add Project' form, the state is set like so:
this.state = {
title: "Create",
loading: false,
projectData: new PortfolioProject,
};
But when I do it from the API, it's set like so:
this.setState({
title: "Edit",
loading: false,
projectData: data
});
The successful version uses this.state, and the unsuccessful version uses this.setState
I don't know what this can mean though. As I said, no errors are being thrown, I'm sticking to the tutorial format, and it works fine in other parts of the project.
Thanks.
UPDATE
I've put a log in at the point where renderCreateForm() is called. It seems setState is actually working. Therefore, the problem must be in renderCreateForm() so I'll post that code below. Sorry it's sort of large.
private renderCreateForm(projectTypes: string, projectTech: string) {
console.log(this.state.loading); // "false"
console.log(this.state.projectData); //"Object { id:1, name: "Muon Detector".. etc
//so the render is getting the data
return (
<form onSubmit={this.handleSave}>
<div className="form-group row" >
<input type="hidden" name="Id" value={this.state.projectData.id} />
</div>
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Name">Name</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Name" defaultValue={this.state.projectData.name} required />
</div>
</div>
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="ProjectDate">Project Date</label>
<div className="col-md-4">
<input className="form-control" type="date" name="ProjectDate" defaultValue={this.state.projectData.creationDate.toDateString()} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="ProjectDurationWeeks">Project Duration (weeks)</label>
<div className="col-md-4">
<input className="form-control" type="text" name="ProjectDurationWeeks" defaultValue={this.state.projectData.projectDurationWeeks.toString()} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Type">Project Type</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Type" defaultValue={this.state.projectData.type} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Tech">Project Tech</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Tech" defaultValue={this.state.projectData.tech} required />
</div>
</div >
<div className="form-group row" >
<input type="hidden" name="Views" value={this.state.projectData.views} />
</div>
<div className="form-group row" >
<input type="hidden" name="CreationDate" value={this.state.projectData.creationDate.toDateString()} />
</div>
<div className="form-group row" >
<input type="hidden" name="ModifiedDate" value={this.state.projectData.modifiedDate.toDateString()} />
</div>
<div className="form-group row" >
<input type="hidden" name="Status" value={this.state.projectData.status} />
</div>
<div className="form-group">
<button type="submit" className="btn btn-default">Save</button>
<button className="btn" onClick={this.handleCancel}>Cancel</button>
</div >
</form>
)
}
UPDATE 2: Added some screenshots showing how things appear so far.
How the main data table page looks:
If I click 'Add New', it works:
(the 'Save' option works there too. Data posts to the server and will list on the main portfolio page)
Clicking Edit for any of the entries does not work, it gets this far:
The 'Loading Project...' text comes from the render() call for this page, as is shown in the code posted at the top of this post.
The page is supposed to look exactly like the 'Create' page (2nd screenshot), but with the title being 'Edit', and with input values populated from the given data.
The solution was absurd, but may certainly help others...
The renderCreateForm() method (as shown in Update 1 of the post) was not working because of the .toDateString() method I was using in a few of the inputs.
I changed it to .toString() and now everything works.
For example, with an input like this:
<input className="form-control" type="date" name="ProjectDate" defaultValue={projectData.creationDate.toDateString()} required />
I changed it to this:
<input className="form-control" type="date" name="ProjectDate" defaultValue={projectData.creationDate.toString()} required />
Note the defaultValue property of the input.
Repeat for all cases of .ToDateString(), and it now works,
Amazing that this didn't bring up an error anywhere. I thought Typescript and all these various frameworks were supposed to get around the issue of Javascript silently failing like that. This has been my longest and most time-wasting 'silent fail error' ever, by a very long margin.

Resources