React add new input have same value for all items - reactjs

I have a input form that can dynamically add new inputs like this:
const [inputVariationFields, setInputVariationFields] = useState<AddProductVariationForm[]>([])
const addVariationFields = () => {
let newfield = AddProductVariationInitialValue
setInputVariationFields([...inputVariationFields, newfield])
}
let removeVariationFields = (i: number) => {
let newFormValues = [...inputVariationFields];
newFormValues.splice(i, 1);
setInputVariationFields(newFormValues)
}
{
inputVariationFields.map((element, index) => (
<div className="my-3 p-3 border border-gray-lighter rounded" key={index}>
<div className='w-full'>
<label htmlFor={'name_1_' + index.toString} className='font'>
Variation Name 1
<BaseInput
key={'name_1_' + index.toString}
id={index.toString()}
name={'name_1_' + index.toString}
type='text'
value={element.name_1 || ''}
onChange={(e) => {
let newFormValues = [...inputVariationFields];
console.log(newFormValues)
newFormValues[index].name_1 = e.target.value;
setInputVariationFields(newFormValues);
}}
value={element.name_1 || ''}
placeholder='Product variation name 1'
/>
</label>
</div>
...
{
index ?
<div className='mt-5 mb-2'>
<OutlinedButton key={index} onClick={() => removeVariationFields(index)}>
Remove
</OutlinedButton>
</div>
: null
}
</div>
))
}
<div className="my-3">
<PrimaryButton
type='button'
onClick={() => addVariationFields()}
>
Add variation
</PrimaryButton>
</div>
But when I add the new variation field, and insert the value of name_1, it change the value of the new variation name_1 field as well. How to insert name_1 with different value of each variation?
Note: I'm using typescript and the InputVariationField is an array of object that have keys like name_1 and more.

You are spreading the same object ("newField") into inputVariationFields every time you call addVariationFields. This causes every element in the array to be a pointer to the same object. So everytime you change a property of one element it changes the same property of every other element in the array.
To solve this mistake add a new object when calling addVariationFields instead of the same each time.
You can do this by creating a new object with {...AddProductVariationInitialValue}
This new object will have the same properties as .AddProductVariationInitialValue.
const addVariationFields = () => {
let newfield = {...AddProductVariationInitialValue}
setInputVariationFields([...inputVariationFields, newfield])
}
I hope this solves your problem

I mean, the init value of inputVariationFields like:
const [inputVariationFields, setInputVariationFields] = useState([{ name_1: "" }]);
In addVariationFields function, you must init a object with name_1 is empty, like:
const addVariationFields = () => {
const newfield = { name_1: "" };
setInputVariationFields([...inputVariationFields, newfield]);
};
Error in your onChange handle and value of input. I make a new function to handle onChange of input.
const onChangeInput = (e, index) => {
e.preventDefault();
const newArray = inputVariationFields.map((ele, i) => {
if (index === i) {
ele.name_1 = e.target.value;
}
return ele;
});
setInputVariationFields(newArray);
}
{
inputVariationFields.map((element, index) => (
<div className="my-3 p-3 border border-gray-lighter rounded" key={index}>
<div className='w-full'>
<label htmlFor={'name_1_' + index.toString} className='font'>
Variation Name {index + 1}
<BaseInput
key={'name_1_' + index.toString}
id={index.toString()}
name={'name_1_' + index.toString}
type='text'
onChange={(e) => onChangeInput(e, index)}
value={element.name_1 || ""}
placeholder={`Product variation name ` + (index + 1)}
/>
</label>
</div>
{
index ?
<div className='mt-5 mb-2'>
<OutlinedButton key={index} onClick={() => removeVariationFields(index)}>
Remove
</OutlinedButton>
</div>
: null
}
</div>
))
}
Check out:
https://codesandbox.io/s/friendly-forest-38esp3?file=/src/Test.js
Hope that it helps

Related

Dynamically add or remove text input field and set values to an array of the main state in React JS

I am trying to figure out how to submit an arbitrary amount of array elements, so at least one to infinity.
This is a state that gets passed at the click of a submit button:
const [gallery, setGallery] = useState({
title: "",
description: "",
image_url: [],
});
I am looping the following state...
const [linkInput, setLinkInput] = useState([{ id: 1, link: "" }]);
...like so:
<div className="form-group mx-sm-3 mb-2">
<label htmlFor="image_url">Image URLs*</label>
{Array.isArray(linkInput) &&
linkInput.map((input, i) => (
<div className="input-group mb-3" key={input.id}>
<input
className="form-control"
id="image_url"
type="text"
value={input.link}
required
onChange={(e) =>
onChangeLink({ ...input, link: e.target.value })
}
/>
<br />
<div style={{ color: "#ffffff" }}>__</div>
{linkInput.length !== 1 && (
<>
<button
type="button"
className="btn btn-primary"
onClick={() => removeLink(input.id)}
>
Remove Image
</button>
</>
)}
</div>
))}
<br />
<button
type="button"
className="btn btn-primary"
onClick={() => addInput()}
>
Add another image
</button>
</div>
These are functions I am using to add or remove input fields by modifying the "linkInput" state:
const handleRemoveLink = (id) => {
setLinkInput(linkInput.filter((el) => el.id !== id));
};
const handleAddInputField = () => {
const lastItem = linkInput[linkInput.length - 1];
setLinkInput([...linkInput, { id: Number(lastItem.id + 1), link: "" }]);
};
I am trying to take the "link" value from each input element and place it in "image_url: []" upon clicking on submit button on my form. Currently I am not even close, the form behaves as such: it totally disappears when placing even a single character. What should I do?
You can do it by mapping over the existing linkInput array with map which returns a new array.
const handleLink = (e, index) => {
const result = linkInput.map((linkObj, i) => {
if (i === index) {
// create a new object with the values of the previous link object
// set the link prop to the new value
return {
...linkObj,
link: e.target.value,
};
}
return linkObj;
});
setLinkInput(result);
};
A common mistake is to not return a new object and mutate the linkObj directly. When doing so you'll change the original object.
const linkInput = [{ id: 1, link: "" }];
let result = [...linkInput];
result = result.map((x, i) => {
if (i === 0) x.link = "mutated";
return x;
});
console.log('linkInput',linkInput);
console.log('result',result);
As you can see both of the array's have the "mutated" value while did copy the list.
The solution was to specify exactly what event corresponds to what array element:
const handleLink = (e, index) => {
let result = [...linkInput];
result = result.map((x, i) => {
if (i === index) x.link = e.target.value;
return x;
});
setLinkInput(result);
};
NOTE: I dropped "id" property on linkInput in favor of indexes.

React.js working code breaks on re-render

I have a weird bug, where my code works on first attempt, but breaks on page re-render.
I've created a filter function using an object with filter names and array of filter values:
const filterOptions = {
'size': ['s', 'm', 'l'],
'color': ['black', 'white', 'pink', 'beige'],
'fit': ['relaxed fit','slim fit', 'skinny fit', 'oversize'],
'pattern': ['patterned', 'spotted', 'solid color'],
'material': ['wool', 'cotton', 'leather', 'denim', 'satin']
}
The idea was to create a separate object with all the values and corresponding 'checked' attribute and than use it to check if checkbox is checked:
const [checkedValue, setCheckedValue] = useState({})
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))}, [])
FilterValue here is array of values from FilterOptions:
<div className='popper'>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
There is onChange function as wel, which could be a part of problem:
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}}
I've tried keeping filterOptions in parent component and in Context, but it gives exactly the same result. It always work as planned on first render, and on next render it shows this error, until you delete the checked attribute of input. I've noticed that on re-render the 'checkedValue' object returns as empty, but I can't find out why. Would be really helpful if somebody could explain me a reason.
Uncaught TypeError: Cannot read properties of undefined (reading 'checked')
Edit: full code looks like this:
Parent Component
const Filter = () => {
return (
<div className='filter'>
<div className="price-filter">
<p>Price: </p>
<Slider onChange={handleSliderChange} value={[min, max]} valueLabelDisplay="on" disableSwap style={{width:"70%"}} min={0} max={250} />
</div>
<Divider />
<ul className='filter-list'>
{Object.entries(filterOptions).map((filter, i) => {
return (
<Fragment key={`${filter[0]}${i}`}>
<FilterOption className='filter-option' filterName={filter[0]} filterValue={filter[1]} />
<Divider key={`${i}${Math.random()}`} />
</Fragment>
)
})}
</ul>
</div>
)
}
Child Component
const FilterOption = ({ filterName, filterValue }) => {
const { checkedValue, setCheckedValue, activeFilters, setActiveFilters, filterOptions } = useContext(FilterContext)
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))
}, [])
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}
}
return (
<div className='popper' key={filterName}>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
)

Next JS - Checkbox Select All

I'm a beginner in Next Js. and I'm trying to implement select all on the checkboxes.
I following this reference https://www.freecodecamp.org/news/how-to-work-with-multiple-checkboxes-in-react/
what I expect is, if the checkbox select all is checked then sum all the prices.
but I don't know how to start it.
Here is my sandbox https://codesandbox.io/s/eager-feather-2ieme9
Any help with this would be greatly appreciated, been working on this for a while and exhausted all avenues!
You can check the below logic with some explanation
You also can check this sandbox for the test
import { useState } from "react";
import { toppings } from "./utils/toppings";
// import InputTopings from "./InputTopings";
const getFormattedPrice = (price) => `$${price.toFixed(2)}`;
export default function TopingApp() {
const [checkedState, setCheckedState] = useState(
new Array(toppings.length).fill(false)
);
// console.log(checkedState);
const [total, setTotal] = useState(0);
//Separate `updateTotal` logic for avoiding duplication
const updateTotal = (checkboxValues) => {
const totalPrice = checkboxValues.reduce((sum, currentState, index) => {
if (currentState === true) {
return sum + toppings[index].price;
}
return sum;
}, 0);
setTotal(totalPrice);
};
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
//update total
updateTotal(updatedCheckedState);
};
const handleSelectAll = (event) => {
//filled all checkboxes' states with `Check All` value
const updatedCheckedState = new Array(toppings.length).fill(
event.target.checked
);
setCheckedState(updatedCheckedState);
//update total
updateTotal(updatedCheckedState);
};
return (
<div className="App">
<h3>Select Toppings</h3>
<div className="call">
<input
type="checkbox"
name="checkall"
checked={checkedState.every((value) => value)}
onChange={handleSelectAll}
/>
<label htmlFor="checkall">Check All</label>
</div>
<ul className="toppings-list">
{toppings.map(({ name, price }, index) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
type="checkbox"
// id={`custom-checkbox-${index}`}
name={name}
value={name}
checked={checkedState[index]}
onChange={() => handleOnChange(index)}
/>
<label>{name}</label>
</div>
<div className="right-section">{getFormattedPrice(price)}</div>
</div>
</li>
);
})}
<li>
<div className="toppings-list-item">
<div className="left-section">Total:</div>
<div className="right-section">{getFormattedPrice(total)}</div>
</div>
</li>
</ul>
</div>
);
}
You can lift the 'Check all' state to a parent object and on change of this state set all <input/> tags value to that state you can achieve this by dividing your app. First create a component for the items in the list like <Toppings/> and give props to this component like so <Toppings name={name} price={price} checkAll={checkAll}/> inside the toppings component create a state variable like this
const Toppings = ({name,price, checkAll}) => {
const [checked,setChecked] = useState(checkAll)
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
type="checkbox"
// id={`custom-checkbox-${index}`}
name={name}
value={checked}
onChange={setChecked(!checked)}
/>
<label>{name}</label>
</div>
</div>
</li>
)
}
Edit:
inside index.js:
const [checkAll, setCheckAll] = useState(false)
//when rendering inside return method
{toppings.map(({name,price},index) => <Toppings key={index} name={name} price={price} checkAll={checkAll}/> )

Check duplicates from a state hook in React

I'm trying to figure out on how to remove duplicates from an array of objects when entering multiple input skill tags. Maybe I am missing some key points here
const SkillsTags = ({ skillTags, setSkillTags, skill }) => {
const removeSkillTag = (i) => {
setSkillTags([...skillTags.filter((_, index) => index !== i)]);
};
const addSkillTag = (e) => {
e.preventDefault();
var updatedSkills = [...skillTags];
if (skill.current.value.trim().length !== 0) {
updatedSkills = [...skillTags, { SKILL_NAME: skill.current.value }];
}
setSkillTags(updatedSkills);
skill.current.value = "";
};
return (
<>
<label htmlFor="Skills">Skills:</label>
<div className="input-group">
<input
type="text"
placeholder="Enter Skills (Press Enter to add)"
onKeyPress={(e) => (e.key === "Enter" ? addSkillTag(e) : null)}
ref={skill}
/>
<button className="btn btn-outline-primary" onClick={addSkillTag}>
Add
</button>
</div>
<ul style={{ height: "12.5rem" }}>
{skillTags.map((val, index) => {
return (
<li key={index}>
{val.SKILL_NAME}
<button type="button" onClick={() => removeSkillTag(index)}>
Remove
</button>
</li>
);
})}
</ul>
</>
);
};
Demo here: https://codesandbox.io/s/add-skill-tags-nitthd?file=/src/SkillsTags.js
I think you are trying to filter duplicates, You can achieve this with simple Javascript.
const addSkillTag = (e) => {
e.preventDefault();
const isDuplicate = skillTags.some(function (item, idx) {
return item.SKILL_NAME === skill.current.value;
});
if (skill.current.value.trim().length !== 0) {
if (!isDuplicate) {
skillTags = [...skillTags, { SKILL_NAME: skill.current.value }];
}
}
setSkillTags(skillTags);
skill.current.value = "";
};

React: How to reuse components with their own dataset?

Trying to do a dropzone upload component that allows user to tag each image uploaded using an input field..
Problem is the tags used for the first image is also loaded in the tag for the second image...
Ideally, each image should have its own "set" of tags on upload. Not sure what am I missing in terms of reusing the TagInput component.
Screenshot below to show the erroneous behavior:
Dropzone.js
const [tags, setTags] = useState({});
const addTagHandler = (aTag, index) => {
let result;
if (Object.keys(tags).length === 0) {
// if nothing, create.
result = { [index]: [aTag] };
} else {
//check index, if index exists, push to index. else, create index
if (index < Object.keys(tags).length) {
result = { ...tags, [index]: [...tags[index], aTag] };
} else {
result = { ...tags, [index]: [aTag] };
}
}
setTags(result);
};
<div className="file-display-container">
{validFiles.map((aFile, index) => (
<div className="file-status-bar" key={index}>
<div>
{previewUrl && (
<div className="file-preview">
<img src={previewUrl[index]} alt="image" />
</div>
)}
<span
className={`file-name ${aFile.invalid ? "file-error" : ""}`}
>
{aFile.name}
</span>
<span className="file-size">({fileSize(aFile.size)})</span>{" "}
{aFile.invalid && (
<span className="file-error-message">({errorMessage})</span>
)}
</div>
<TagInput
tags={tags[index]}
onAdd={(aTag) => addTagHandler(aTag, index)}
onDelete={deleteTagHandler}
/>
<div
className="file-remove"
onClick={() => removeFile(aFile.name)}
>
X
</div>
</div>
))}
</div>
TagInput.js
const [input, setInput] = useState("");
const _keyPressHandler = (event) => {
if (event.key === "Enter" && input.trim() !== "") {
onAdd(input.trim());
setInput("");
}
};
return (
<div className="taginput">
{tags &&
tags.map((aTag, index) => (
<Tag
key={aTag + index}
label={aTag}
onClickDelete={() => onDelete(index)}
/>
))}
<Input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={_keyPressHandler}
placeholder="Insert tag here"
/>
</div>
);
};
You are passing the same array of tags to each TagInput component. They need to be separated in some fashion.
What you need to do is create a tag array for each input. You could do this by using an object based on the file key.
Something like this should work. Just pass the key of the file to each handler so it updates the appropriate array.
const [tags, setTags] = useState({});
const addTagHandler = (key, tag) => {
setTags({...tags, [key]: [...tags[key], tag]});
};
const deleteTagHandler = (key, i) => {
setTags({...tags, [key]: tags[key].filter((_, index) => index !== i));
};
Update your tag component to use the key i like so.
<TagInput
tags={tags[i]}
onAdd={tag => addTagHandler(i,tag)}
onDelete={tagIndex => deleteTagHandler(i,tagIndex)}
/>
Your issue is in how you are setting the tags state. In your line const [tags, setTags] = useState([]);, you are setting tags as a single array of strings, which is being used by every consequent image with a tag, as seen in the line
<TagInput
tags={tags}
onAdd={addTagHandler}
onDelete={deleteTagHandler}
/>
where you are reusing the same tags state among all the divs in .file-display-container.
A solution for this would be to instead have an array of strings inside the state array. (tags: [["tag1", "tag2"], ["image2Tag1"], ....])
so in your old line where you set tags={tags} for each TagInoput, you would instead set is as tags={tags[i]}(the index is i in your map function).
you would need to adjust the tag handler functions accordingly. (#Todd Skelton's answer provides a fantastic way to handle it)

Resources