How to add new input fields dynamically to object in nested array in react js when user clicks on plus sign? dynamically add and remove inputs
I want to add and delete propositionTimes dynamically in the handlepropositionTimeAddClick and handlepropositionTimeRemoveClick methods I shared below. How can I do that? And I want to do the same with propositionResponseTimes and analyzers.
const [issue, setIssue] = useState({
firstResponseDuration: "",
firstResponseOvertime: "",
solutionDuration: "",
solutionOvertime: "",
propositionTimes: [{
propositionTime: ""
}],
propositionResponseTimes: [{ propositionResponseTime: "" }],
analyzers: [{ analyzerName: "", analyzerHuaweiId: "" }],
});
const { firstResponseDuration, firstResponseOvertime,solutionDuration, solutionOvertime, propositionTimes, propositionResponseTimes, analyzers } = issue;
.
.
.
// handle click event of the Remove button
const handlepropositionTimeRemoveClick = index => {
};
// handle click event of the Add button
const handlepropositionTimeAddClick = (i) => {
};
.
.
.
{
issue.propositionTimes.map((item, i) => {
return (
<div key={i} className="form-group" >
<label>
Proposition Time
</label>
<TextField
type="datetime-local"
placeholder="Enter propositionTime"
name="propositionTime"
format="dd-MM-yyyy HH:mm"
value={item.propositionTime}
onChange={e => onInputPropositionTimes(e, i)}
/>
<div>
{issue.propositionTimes.length !== 1 && <button
className="mr10"
onClick={() => handlepropositionTimeRemoveClick(i)}>Remove</button>}
{issue.propositionTimes.length - 1 === i && <button onClick={handlepropositionTimeAddClick(i)}>Add</button>}
</div>
</div>
)
})
}
// handle click event of the Remove button
const handlepropositionTimeRemoveClick = index => {
const issueObj = {...issue};
const filteredIssue = issue.propositionTimes.filter((item, ind) => ind !== index);
issueObj.propositionTimes = filteredIssue;
setIssue(issueObj);
};
// handle click event of the Add button
const handlepropositionTimeAddClick = (i) => {
const issueObj = {...issue};
const newObj = {
propositionTime: "" // Code to add new propositionTime
}
issueObj.propositionTimes.push(newObj)
setIssue(issueObj);
};
Also in your handlepropositionTimeAddClick function, don't call it directly. Just pass the reference
{issue.propositionTimes.length - 1 === i && <button onClick={() => handlepropositionTimeAddClick(i)}>Add</button>}
Related
I am trying to dynamically remove an input field after a button is pressed and also remove the key with its value from Formik. I am using the useFormik hook for implementation. The problem is that when I press the button to remove the input field, it is removed but the key and value stay in useFormik. When I press the button again, another input field is removed and the previous key and value is removed from useFormik. Removing values from useFormik is one step behind. How can I change it so it removes the key and value at the same time as the input field?
Here are initial values for Formik.
const formik = useFormik({
initialValues: {
Produkt1:
"",
Produkt2:
"",
},
validationSchema: frontProductUrls(),
enableReinitialize: true,
onSubmit: (values) => {
console.log(values);
},
setFieldValue: () => {
delete fields.length - 1;
},
});
This array i use to dynamicly add remove and render input fields.
const [fields, setFields] = useState([
{ name: "Produkt1", label: "Produkt 1" },
{ name: "Produkt2", label: "Produkt 2" },
]);
Here is function to remove last input filed in array and also remove key and value from useFormik.
const removeField = (e) => {
formik.setFieldValue(`Produkt${fields.length + 1}`, undefined, false);
let updatedFields = [...fields];
updatedFields.splice(fields.length - 1, 1);
setFields(() => updatedFields);
};
This function is used to add new input field. This work fine it add new input and also it add new key in useFormik.
const addField = () => {
if (fields.length > 3) return;
const newProduct = `Produkt${fields.length + 1}`;
formik.setFieldValue(newProduct, "");
setFields([
...fields,
{
name: newProduct,
label: newProduct,
},
]);
};
Here is code where are fields rendered.
{fields.map((field, index) => {
return (
<div key={index} className="mb-4">
<label
htmlFor={field.name}
className="block text-gray-700 font-medium mb-2"
>
{field.label}
</label>
<input
id={field.name}
name={field.name}
type="text"
onChange={formik.handleChange}
value={formik.values[field.name]}
className="border border-gray-400 p-2 rounded-lg w-full"
/>
</div>
);
})}
```
I had to add this code
formik.setFieldValue(`Produkt${fields.length}`, "");
setFields(fields.filter((_, i) => i !== fields.length - 1));
useEffect(() => {
formik.
formik.setValues({ fields });
}, [fields]);
I have a component renderRoyaltyAccount, that gets rendered x number of times depending on the input that sets royaltyAccount.
In this component I have 2 fields, one for the name of the account, and the second a percentage.
What I wanted to do is depending of the number of accounts to create, create an object with those two fields for each, example :
If he chooses to create two accounts , to have a the end (what I thought but could be not the best choice :) ) :
{
1: {
"account": "test1",
"percentage": 2,
},
2: {
"account": "test#",
"percentage": 0.5
}
}
I tried with a useState and updating it with onChange with inputs, but it was a mess LOL.
If anyone could help me with this state, and specially the logic with objects and hooks. Thank you
export default function FormApp() {
const [royaltyAccount, setRoyaltyAccount] = useState(1);
const [allAccounts, setAllAccounts] = useState ({
{
"account": "",
"percentage": 1,
},
})
const renderRoyaltyAccounts = () => {
let items = [];
for (let i = 0; i < royaltyAccount; i++) {
items.push(
<div key={i}>
<div>
<label>Royalty Account n° {i + 1}</label>
<input onChange={()=> setAllAccounts(???)} type="text"/>
</div>
<div>
<label>Royalty %</label>
<input onChange={()=> setAllAccounts(???)} type="text"/>
</div>
</div>
)
}
return items;
}
return (
<>
<label> Royalty account(s)</label>
<input onChange={(e) => { setRoyaltyAccount(e.target.value)}} type="number"/>
{
renderRoyaltyAccounts()
}
</>
)
}
Dynamically compute the allAccounts state array from the initial royaltyAccount state value. Add an id property to act as a GUID for each account object.
Create a handleRoyaltyAccountChange onChange handler to either append a computed diff of the current allAccounts array length to the new count value, or to slice up to the new count if less.
Create a handleAccountUpdate onChange handler to shallow copy the allAccounts state array and update the specifically matching account object by id.
Give the inputs a name attributeand pass the mappedallAccountselement object's property as thevalue` prop.
Code:
import { useState } from "react";
import { nanoid } from "nanoid";
function FormApp() {
const [royaltyAccount, setRoyaltyAccount] = useState(1);
const [allAccounts, setAllAccounts] = useState(
Array.from({ length: royaltyAccount }).map(() => ({
id: nanoid(),
account: "",
percentage: 1
}))
);
const handleRoyaltyAccountChange = (e) => {
const { value } = e.target;
const newCount = Number(value);
setRoyaltyAccount(newCount);
setAllAccounts((accounts) => {
if (newCount > accounts.length) {
return accounts.concat(
...Array.from({ length: newCount - accounts.length }).map(() => ({
id: nanoid(),
account: "",
percentage: 1
}))
);
} else {
return accounts.slice(0, newCount);
}
});
};
const handleAccountUpdate = (id) => (e) => {
const { name, value } = e.target;
setAllAccounts((accounts) =>
accounts.map((account) =>
account.id === id
? {
...account,
[name]: value
}
: account
)
);
};
return (
<>
<label> Royalty account(s)</label>
<input
type="number"
onChange={handleRoyaltyAccountChange}
value={royaltyAccount}
/>
<hr />
{allAccounts.map((account, i) => (
<div key={account.id}>
<div>
<div>Account: {account.id}</div>
<label>
Royalty Account n° {i + 1}
<input
type="text"
name="account"
onChange={handleAccountUpdate(account.id)}
value={account.account}
/>
</label>
</div>
<div>
<label>
Royalty %
<input
type="text"
name="percentage"
onChange={handleAccountUpdate(account.id)}
value={account.percentage}
/>
</label>
</div>
</div>
))}
</>
);
}
I have a Curriculum Vitae form that does not send the data unless two sections are completely filled. For testing purposes none of the fields are required. Furthermore I am getting the error:
POST http://localhost:8000/api/jobApplications net::ERR_CONNECTION_RESET
The issue is because there are 2 sections in the CV, Education and Work Experience, that have aggregate fields when clicking on a button that adds a new Education/Work field row. If all these fields within these rows aren't filled, it will not send.
The input fields' name and value in the Education and Work sections have a functionality where when you click on add new row of fields, it will replace/change the letter in both the input name and value.
Here are the useState hooks:
const [inputValue, setInputValue] = useState<JobApplicationInterface>(JobApplicationForm);
const [divEducationList, setDivEducationList] = useState<number[]>([]);
const [divWorkList, setDivWorkList] = useState<number[]>([]);
const [divEducation, setDivEducation] = useState<number>(0);
const [divWork, setDivWork] = useState<number>(0);
const [arrayEducation, setArrayEducation] = useState<ArrayLettersInterface[]>(arrayLetters);
const [arrayWork, setArrayWork] = useState<ArrayLettersInterface[]>(arrayLetters);
Here's the Education section (the Work Experience section is basically the same, except has the option to add more fields - Education is limited at 3 rows of fields and Work is limited to 5).
EDUCATION SECTION
<div className='input-group-container education-container'>
<div className='input-group education-group'>
<div className='input-container input-wrapper'>
<Select
title='Education Degree'
name='education_a_degree'
value={inputValue.education_a_degree}
optionList={degreeSelectOptionArray}
handleSelect={handleSelect}
/>
</div>
<div className='input-container input-wrapper'>
<Input
title='University'
type='text'
value={inputValue.education_a_school}
name='education_a_school'
handleInput={handleInput}
/>
</div>
...MORE INPUTS (type=text)...
<div className='btn-group-container education-btns'>
<Button
btnTitle={<Add />}
handleClick={handleClickAddEducation}
/>
</div>
</div>
{divEducationList.map((_, index: number) => (
<div className='input-group education-group' key={index}>
<div className='input-container input-wrapper'>
<Select
title='Education Degree'
value={inputValue[`education_${arrayLetters[index].letter}_degree`]}
name={`education_${arrayLetters[index].letter}_degree`}
optionList={degreeSelectOptionArray}
handleSelect={handleSelect}
/>
</div>
<div className='input-container input-wrapper'>
<Input
title='University'
type='text'
value={inputValue[`education_${arrayLetters[index].letter}_school`]}
name={`education_${arrayLetters[index].letter}_school`}
handleInput={handleInput}
/>
</div>
...MORE INPUTS (type=text)...
<div className='btn-group-container education-btns'>
{!divEducationList?.[1] ? (
<Button
btnTitle={<Add />}
handleClick={handleClickAddEducation}
/>
) : null}
<Button
btnTitle={<Remove />}
handleClick={(event: React.ChangeEvent<HTMLInputElement>) =>
handleClickRemove(event, index, 'education')
}
/>
</div>
</div>
))}
</div>
I have 2 functions for adding a new row of fields, one for Education and the other for Work, and they are the same. This function also adds a new letter ('a', 'b', 'c',...) to all of the row's input names and values each time a new row is added.
HANDLECLICKS
const handleClickAddEducation = () => {
if (divEducationList.length < 2) {
for (let i = 0; i < divEducationList.length; i++) {
if (arrayLetters[i].index === i) {
setArrayEducation((prev: any) => ({
...prev,
[i]: arrayLetters[i].letter,
}));
}
}
setDivEducation((divEducation: number) => divEducation + 1);
return setDivEducationList((divEducationList: any) => [
...divEducationList,
divEducation,
]);
} else {
throw new Error('too many selected');
}
};
const handleClickAddWork = () => {
if (divWorkList.length < 2) {
for (let i = 0; i < divWorkList.length; i++) {
if (arrayLetters[i].index === i) {
setArrayWork((prev: any) => ({
...prev,
[i]: arrayLetters[i].letter,
}));
}
}
setDivWork((divWork: number) => divWork + 1);
return setDivWorkList((divWorkList: any) => [
...divWorkList,
divWork,
]);
} else {
throw new Error('too many selected');
}
};
This is the handle input:
HANDLEINPUTS
const handleInput: React.ChangeEventHandler<HTMLInputElement> | undefined = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const value = event.target.value;
const name = event.target.name;
setInputValue((prev) => ({ ...prev, [name]: value }));
};
For the POST request I use Axios and FormData, and submitting to the server is not an issue, unless, as mentioned above, Education/Work sections are not completely filled.
HANDLESUBMIT
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault();
setIsLoading(true);
let keys = Object.keys(inputValue);
let values = Object.values(inputValue);
for (let i = 0; i < keys.length; i++) {
formData.append(keys[i], values[i] as unknown as Blob);
}
axios
.post(postApiUrl, formData, postHeaders)
.then((response) => {
console.log(response.data);
})
.catch((err) => {
const error =
err.code === 'ECONNABORTED'
? 'A timeout has occurred'
: err.response.status === 404
? 'Resource not found'
: 'An unexpected error has occurred';
setError(error);
setIsLoading(false);
})
.finally(() => setIsLoading(false));
};
If you require more information, please let me know, thanks!
I want to filter items by filter method and I did it but it doesn't work in UI but
when I log it inside console it's working properly
I don't know where is the problem I put 2 images
Explanation of this code:
Looping inside currencies_info by map method and show them when I click on it and this completely working then I want filter the list when user enter input inside it I use filter method and this completely working in console not in UI
import React, { useState } from "react";
// Artificial object about currencies information
let currencies_info = [
{
id: 1,
name: "بیت کوین (bitcoin)",
icon: "images/crypto-logos/bitcoin.png",
world_price: 39309.13,
website_price: "3000",
balance: 0,
in_tomans: 0
},
{
id: 2,
name: "اتریوم (ethereum)",
icon: "images/crypto-logos/ethereum.png",
world_price: 39309.13,
website_price: "90",
balance: 0,
in_tomans: 0
},
{
id: 3,
name: "تتر (tether)",
icon: "images/crypto-logos/tether.png",
world_price: 39309.13,
website_price: "5",
balance: 0,
in_tomans: 0
},
{
id: 4,
name: "دوج کوین (dogecoin)",
icon: "images/crypto-logos/dogecoin.png",
world_price: 39309.13,
website_price: "1000000",
balance: 0,
in_tomans: 0
},
{
id: 5,
name: "ریپل (ripple)",
icon: "images/crypto-logos/xrp.png",
world_price: 39309.13,
website_price: "1,108",
balance: 0,
in_tomans: 0
}
];
export default function Buy() {
// States
let [api_data, set_api_data] = useState(currencies_info);
const [currency_icon, set_currency_icon] = useState("");
const [currency_name, set_currency_name] = useState("");
const [currency_price, set_currency_price] = useState(0);
const [dropdown, set_drop_down] = useState(false);
let [search_filter, set_search_filter] = useState("");
// States functions
// this function just toggle dropdown list
const toggle_dropdown = () => {
dropdown ? set_drop_down(false) : set_drop_down(true);
};
// this function shows all currencies inside dropdown list and when click on each item replace
// the currency info and hide dropdown list
const fetch_currency = (e) => {
set_drop_down(false);
currencies_info.map((currency) => {
if (e.target.id == currency.id) {
set_currency_name(currency.name);
set_currency_icon(currency.icon);
set_currency_price(currency.website_price);
}
});
};
// this function filter items base on user input value
const filter_currency = (e) => {
set_search_filter = currencies_info.filter((currency) => {
return currency.name.indexOf(e.target.value) !== -1;
});
api_data = set_search_filter;
console.log(api_data);
};
return (
<div className="buy-page-input" onClick={toggle_dropdown}>
{/* currency logo */}
<div className="currency-logo">
<img src={currency_icon} width="30px" />
</div>
{/* currency name in persian */}
<span className="currency-name">{currency_name}</span>
{/* currency dropdown icon */}
<div className="currency-dropdown">
<img className={dropdown ? "toggle-drop-down-icon" : ""}
src="https://img.icons8.com/ios-glyphs/30/000000/chevron-up.png"
/>
</div>
</div>
{/* Drop down list */}
{dropdown ? (
<div className="drop-down-list-container">
{/* Search box */}
<div className="search-box-container">
<input type="search" name="search-bar" id="search-bar"
placeholder="جستجو بر اساس اسم..."
onChange={(e) => {
filter_currency(e);
}}/>
</div>
{api_data.map((currency) => {
return (<div className="drop-down-list" onClick={(e) => {
fetch_currency(e);}} id={currency.id}>
<div class="right-side" id={currency.id}>
<img src={currency.icon} width="20px" id={currency.id} />
<span id={currency.id}>{currency.name}</span>
</div>
<div className="left-side" id={currency.id}>
<span id={currency.id}>قیمت خرید</span>
<span className="buy-price" id={currency.id}>
{currency.website_price}تومان</span>
</div>
</div>);})}
</div>) : ("")});}
Your search_filter looks redundant to me.
Try to change the filter_currency function like this:
const filter_currency = (e) => {
const search = e.target.value;
const filtered = currencies_info.filter((currency) => {
return currency.name.includes(search);
});
set_api_data(filtered);
};
It looks like you are never setting the api_data after you set the filter state.
Change the following
api_data = set_search_filter
console.log(api_data)
to
api_data = set_search_filter
set_api_data(api_data)
However, it then looks like set_search_filter is never used and only set so to improve this further you could remove that state and just have it set the api_data direct. Something like this:
const filter_currency = (e) => {
const search_filter = currencies_info.filter((currency) => {
return currency.name.indexOf(e.target.value) !== -1
})
set_api_data(search_filter)
}
Change your state value from string to array of the search_filter like this -
let [search_filter, set_search_filter] = useState([]);
and also it should be like this -
const filter_currency = (e) => {
const filterValues = currencies_info.filter((currency) => {
return currency.name.indexOf(e.target.value) !== -1;
});
set_search_filter(filtervalues);
set_api_data(filterValues);
console.log(filterValues);
};
and use useEffect with search_filter as dependency, so that every time search_filter value is being set, useEffect will trigger re render, for eg:-
useEffect(()=>{
//every time search_filter value will change it will update the dom.
},[search_filter])
In my react app, I have created a page named Add Product with a button named Add Variation to allow adding small, medium, large variations of a product but can't figure out how to remove the small, medium, or large variation object from the state if user changes their mind.
Here's a summary of the problem:
Here's what the component looks like now:
const AddProduct = () => {
const [addVar, setAddVar] = useState(0)
const [values, setValues] = useState({
name: "",
description: "",
categories: [],
category: "",
photo: "",
loading: false,
error: "",
createdProduct: "",
redirectToProfile: false,
variations: [],
formData: ""
});
const {
name,
description,
price,
categories,
category,
photo,
loading,
error,
createdProduct,
redirectToProfile,
variations,
formData
} = values;
const addVariation = (e) => {
e.preventDefault()
setAddVar(addVar + 1)
let oldV = Array.from(variations); // gets current variations
let n = oldV.length; // get current array position
console.log(`Current number of variations is: ${n}`);
let vPost = [{
number: n,
vname: "",
vprice: "",
vquantity: "",
vshipping: ""
}]
let newV = oldV.concat(vPost);
setValues({
...values,
variations: newV,
error: ""
})
}
const handleVariationChange = (name, numberVal) => event => {
// numberVal is the iteration number
// name is the variation property which can be vname, vprice, vshipping, vquantity
// these are tested next in the following if statements
const value = event.target.value;
console.log(`numberVal: `, numberVal);
event.preventDefault()
let newVariations = Array.from(variations)
if(name === "vname") {
newVariations[numberVal].vname = value;
console.log(`newVariations[numberVal].vname value: `, newVariations)
}
if(name === "vprice") {
newVariations[numberVal].vprice = value;
console.log(`newVariations[numberVal].vprice value: `, newVariations)
}
if(name === "vshipping") {
newVariations[numberVal].vshipping = value;
console.log(`newVariations[numberVal].vshipping value: `, newVariations)
}
if(name === "vquantity") {
newVariations[numberVal].vquantity = value;
console.log(`newVariations[numberVal].vquantity value: `, newVariations)
}
setValues({...values, variations: newVariations})
formData.set("variations", JSON.stringify(newVariations));
};
const removeVariation = (e) => {
e.preventDefault()
let newVariations = Array.from(variations)
let popped = newVariations.pop()
setValues({
...values,
variations: newVariations,
error: ""
})
}
const newPostForm = () => (
<form className="mb-3" onSubmit={clickSubmit}>
<h4>Main Photo</h4>
<div className="form-group">
<label className="btn btn-secondary">
<input
onChange={handleChange("photo")}
type="file"
name="photo"
accept="image/*"
/>
</label>
</div>
<div className="form-group">
<label className="text-muted">Main Product Name</label>
<input
onChange={handleChange("name")}
type="text"
className="form-control"
value={name}
placeholder="Add main product name"
/>
</div>
<div className="form-group">
<label className="text-muted">Description</label>
<textarea
onChange={handleChange("description")}
className="form-control"
value={description}
placeholder="Add description"
/>
</div>
<div className="form-group">
<label className="text-muted">Category</label>
<select
onChange={handleChange("category")}
className="form-control"
>
<option>Please select</option>
{categories &&
categories.map((c, i) => (
<option key={i} value={c._id}>
{c.name}
</option>
))}
</select>
</div>
<div>
<button onClick={addVariation}>Add variation</button>
</div>
{variations ? VariationComponent() : null}
<br />
<br />
<button type="submit" className="btn btn-outline-primary">Create Product</button>
</form>
);
return (
<Layout>
<div className="row">
<div className="col-md-8 offset-md-2">
{newPostForm()}
</div>
</div>
</Layout>
);
};
export default AddProduct;
Every time Add variation is clicked, another VariationComponent form is appended to the page . For example, if Add variation button was clicked 3 times, it would result in 3 VariationComponent forms with 3 attached Remove variation buttons. Unfortunately, I do not see how to tell React the position of the #2 item in variations to remove it so I resorted to solving this with .pop(), which is not what I want.
How can I tell React to remove the right array item when Remove variation button is clicked?
If I understand correctly, you can use Arrray.filter() determine which variation to remove. It returns a new array with all but the matching numberVal.
onClick={e=>removeVariation(e)}
const removeVariation = e => {
e.preventDefault();
setValues({
...values,
variations: variations.filter(item => item.name !== e.target.value),
error: ''
});
};
Thanks to #RobinZigmond's and #7iiBob's answers, I was able to solve this by this code:
const removeVariation = (e, num) => {
e.preventDefault();
setValues({
...values,
variations: variations.filter(item => item.number !== num),
error: ''
});
};
Remove variation button:
<button onClick={(e) => removeVariation(e, variations[i].number)} className="btn-danger">
{`Remove Variation`}
</button>
Keep in mind the empty variation object looks like this:
{
number: n,
vname: "",
vprice: "",
vquantity: "",
vshipping: ""
}
and n is coming from addVariation here:
const addVariation = (e) => {
e.preventDefault()
setAddVar(addVar + 1)
let oldV = Array.from(variations); // gets current variations
let n = oldV.length; // get current array position
console.log(`Current number of variations is: ${n}`);
let vPost = [{
number: n,
vname: "",
vprice: "",
vquantity: "",
vshipping: ""
}]
let newV = oldV.concat(vPost);
setValues({
...values,
variations: newV,
error: ""
})
}
Wholehearted thank you as this cost me hours of headache!