How can I add ID and inputs array to my state? - arrays

I'm trying to update my state which is :
inputData: {
id: '',
inputArr: ['']
}
From a form I'm getting from my state and I generate him like so :
inner = arr.input.map((inputs, index) => (
<li key={index} name={index} id={inputs.id}>
<label>{inputs.inputLabel}</label>
<input
// value={inputs.inputValue}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
))
It will have mulitple inputs, so I wish to add all their inputs in the array by the order so i can extract it later, but my handleChange function doesn't seem to work, anyone know why (I cant get the ID that I'm passing with the event, nor to update the array)?
handleChangeInp = e => {
const id = e.target.id;
console.log(`the id is ${id}`);
const index = Number(e.target.name);
const inputData = this.state.inputData.inputArr.map((ing, i) => {
console.log(i);
i == index ? e.target.value : ing;
});
}
thanks for the help!

e.target will reference to the element and to get its props, like id, name, etc. you all need to have the props on the element itself.
To get the id, you'll need to pass the id props:
<input
id={inputs.id}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
Now, e.target.id will get the inputs id. To get the name, do the same as above.
In this example, e.target is <input /> element. And to get its properties, you just need to add the props.

I think the solution is simple. Instead of putting the id and name attribute on the li element, add it to the input. This should allow you to get name from e.target.
UPDATED: Based on additional research:
The Synthetic onChange event in React does not pass the id attribute along. You need to come up with another solution. If you need to keep both the name and index of the input within an array, you can combined them with a special character and split them in the event handler. Looking back on some forms I have created, I see that I found it easy to use the pipe character |, since it is usually not used, but even still, I know it will be the last pipe in my string.
Updated Map of Inputs Array
inner = arr.input.map((inputs, index) => {
console.log({inputs});
return (
<li key={index}>
<label>{inputs.inputLabel}</label>
<input
name={inputs.id + "|" + index}
value={inputs.inputValue} /* try not commenting this out */
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
)
});
Updated onChange handler
handleChangeInp = e => {
const name = e.target.name.split("|");
// index will be last element of array, pop it off
const index = Number(name.pop());
// join the name array back with the pipe character just in case it's used in the name
const id = name.join("|");
console.log(`the id is ${id}`);
console.log(`the index is ${index}`);
const inputData = this.state.inputData.inputArr.map((ing, i) => {
console.log(i);
i == index ? e.target.value : ing;
});
}

First update:
Proper parameters for generated inputs:
let inner = arr.input.map((inputs, index) => (
<li key={index} >
<label>{inputs.inputLabel}</label>
<input
id={index}
name={inputs.id}
// value={inputs.inputValue}
type={inputs.inputType}
onChange={this.handleChangeInp}
/>
</li>
))
This way we'll have id and name available in event but deep state structure forces us to write sth like this:
handleChangeInp = e => {
const id = e.target.id;
console.log(`the id is ${id}`);
const index = Number(e.target.id);
// deep state update
const newInputArr = this.state.inputData.inputArr.map((ing, i) => {
console.log(i, e.target.name);
return (i == index) ? e.target.value : ing;
});
const newInputData = {...this.state.inputData, inputArr: newInputArr}
this.setState({ inputData: newInputData }, ()=>{console.log(this.state.inputData)})
}
It 'works' ... but this method of state update has 2 issues/drawbacks:
values are stored in array by index - we have only index-value pairs;
the second issue is more serious - it stores only first value! For map usage ('searching') we should have array initialized for each index - otherwise it doesn't store values.
My advise - use object (map) to store values:
this.state = {
inputData: {
id: '',
inputArr: {}
}
};
and handler like this:
handleChangeInp = e => {
const {name, value} = e.target;
console.log(name, value);
// deep state update
const newInputArr = {...this.state.inputData.inputArr,[name]:value};
const newInputData = {...this.state.inputData, inputArr: newInputArr}
this.setState({ inputData: newInputData }, ()=>{console.log(this.state.inputData)})
}
This way we have id-value pairs and only for 'touched' fields.
Working example.
Of course value for inputs can be read from state (if defined):
value={this.state.inputData.inputArr[inputs.id]
? this.state.inputData.inputArr[inputs.id] : ''}
// or
value={this.state.inputData.inputArr[inputs.id]===undefined ? ''
: this.state.inputData.inputArr[inputs.id] }
// f.e. when we're using type conversions
// and numerical value `0` could be used as valid input

Related

ReactJS, update State with handler but data is empty

I have this situation:
I want to update some state (an array) which is used to map different React components.
Those componentes, have their own handleUpdate.
But when I call to handleUpdate the state that I need to use is empty. I think is because each handler method was mounted before the state was filled with data, but then, how could I ensure or use the data in the handler? In other words, the handler needs to update the state that fill it's own state:
const [data, setData] = useState([]);
const [deliver, setDeliver] = useState({items: []});
const handleUpdate = (value, position) => {
// This set works
setDeliver({
items: newItems
});
// This doesn't work because "data" is an empty array - CRASH
setData(data[position] = value);
};
useEffect(() => {
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered}
key={i}
onUpdate={(value) => handleUpdate(value, i)}
/>
}));
setData(dataWithComponent); // This is set after <SelectComponent is created...
}
}, [originalData]);
The value that you pass don't come from originalData, so the onUpdated don't know what it's value
You run on originalData using map, so you need to pass item.somthing the the onUpdate function
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered} // you can't use items use deliver.length > 0 ? [i].delivered : ""
key={i}
onUpdate={() => handleUpdate("here you pass item", i)}
/>
}));
I'm not sure, but I think you can do something like that. I hope you get some idea to work it.
// This doesn't work because "data" is an empty array - CRASH
let tempData = [...data].
tempData[position] = value;
setData(tempData);

Dynamically create React.Dispatch instances in FunctionComponents

How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?
if I have the following (untested, simplified example):
interface Foo {
val: string;
setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
const [val, setVal] = useState('')
setAllVals(allVals.concat({val, setVal}))
}
return (
<input type="button" value="Add input" onClick={addVal}>
allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
)
}
I will get the error Hooks can only be called inside of the body of a function component.
How might I dynamically add "watched" elements in the above code, using FunctionComponents?
Edit
I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.
Edit 2
in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:
An add button
Each time the button is pressed, another input element is added.
Each time a new value is entered into the input element, the input element shows the value
There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.
Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
setAllVals(allVals => {
// `i` would be the fixed index of newly added item
// it's captured in closure and would never change
const i = allVals.length
const setVal = (v) => setAllVals(allVals => {
const head = allVals.slice(0, i)
const target = allVals[i]
const tail = allVals.slice(i+1)
const nextTarget = { ...target, val: v }
return head.concat(nextTarget).concat(tail)
})
return allVals.concat({
val: '',
setVal,
})
})
}
return (
<input type="button" value="Add input" onClick={addVal} />
{allVals.map(v =>
<li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
)}
)
}
React hooks cannot be called in callbacks as this breaks the Rules of Hooks.
From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.
interface Foo {
val: string;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([]);
const addVal = () => {
setAllVals((allVals) => allVals.concat({ val: "" }));
};
const updateVal = (index: number) => (e: any) => {
setAllVals((allVals) =>
allVals.map((el, i) =>
i === index
? {
...el,
val: e.target.value
}
: el
)
);
};
return (
<>
<input type="button" value="Add input" onClick={addVal} />
{allVals.map((v, i) => (
<li key={i}>
<input value={v.val} onChange={updateVal(i)} />
</li>
))}
</>
);
}

Filter products based on searchString and product size

I'm rather new to React and still learning. I'm trying to filter my overview of products based on a search string and/or with a filter based on the size state.
Right now, the searching works and I'm able to search within this product view
My search works this way: I take the value from the input and insert it into the {this.state.plants.filter you can see in the bottom of the sample code below.
My question is: How do I filter the products by this.state.size AND this.state.searchString at the same time?
OR: Leave the this.state.searchString empty and only applying this.state.size?
I hope my question makes sense.
this.state = {
plants: '',
title: '',
size: '',
searchString: '',
filterSize: ''
};
handleSearch = (event) => {
this.setState({
searchString: event.target.value
})
}
handleSizeFilter = (event) => {
this.setState({
filterSize: event.target.value
})
}
// Search input
<input type="text" className="search" placeholder="Søg" onChange={this.handleSearch} value={this.state.searchString} />
// Size Filter Button
<button onClick={handleSizeFilter} value="22">Size 22</button>
{this.state.plants.filter(plant => plant.title.includes(this.state.searchString)).map(plant => (
{plant.title} - {plant.size}
))}
What I would do here is something like:
let filteredPlants = this.state.plants;
const { searchString, filterSize } = this.state;
if (searchString) {
filteredPlants = filteredPlants.filter(plant => plant.title.includes(searchString);
}
if (typeof filterSize === 'number') {
filteredPlants = filteredPlants.filter(plant => plant.size === filterSize);
}
NOTE: Since this is a potentially expensive operation depending on how big your arrays are, best to create a method inside your class component and wrap it in a utility function like memoizeOne.
If you put this inside your render method, the component will iterate through the array every time you re-render - so in your case every single time you type or delete something in your input fields

React useState, value of input doesnt update

I wish to generate inputs based on json, so first I set it to initial state, then in child componenet I want to modify it's field, thing is that component doesnt update... It renders once and have no idea how to make it be updated each time when input onChange change it's value. Any idea how to make value of input be updated each time when I type something?
PARENT
function App() {
const [inputValue, setInputValue] = useState(chunkingRow);
const handleChunkingChange = (e, index) => {
let inputContent = inputValue;
const reg = new RegExp(/^[0-9]+$/);
if (e.target.value.match(reg)) {
inputContent[index] = {
...inputContent[index],
content: e.target.value,
};
setInputValue(inputContent);
console.log(inputValue);
} else console.log('not a number')
};
return (
<div>
<Wrapper>
{Chunk(inputValue, handleChunkingChange)}
</Wrapper>
</div>
);
}
CHILD
const Chunk = (inputValue, handleChunkingChange) => {
return(
<div>
{inputValue.map((el, index) => (
<div key={index}>
<p>{el.title}</p>
{console.log(el.content)}
<input
type="text"
onChange={(e, i) => handleChunkingChange(e, index)}
value={el.content}
/>
</div>
))}
</div>
);
}
link to demo
https://codesandbox.io/s/stoic-mirzakhani-46exz?file=/src/App.js
Not completely sure why this happens, but probably because of the way you handle the input change. It seems to me that component doesn't recognize that array changed. How I managed to fix your code is replacing line 9 in App component with following code:
let inputContent = [...inputValue];
By doing that, array's reference is changed and components are updated.
Just update your code as follow:
let inputContent = [ ...inputValue ];
You are mutating the state object.
let inputContent = inputValue;
That's why the state is not re-rendered. Change it to
let inputContent = [...inputValue];
An example of mutating objects. React compares previous state and current state and renders only if they are different.
const source = { a: 1, b: 2 };
const target = source;
console.log(target);
console.log(target === source); = true
target.b = 99;
console.log({target});
console.log({source}); //source == target due to mutation
console.log(source === target); = true
Remember, never mutate.

failed to get some values from dynamic multiple input fields using reactjs

i have implemented dynamic input fields that are created whenever u click add Product button but i dont why i can only pick itemAmount values but not input values for productId and itemQuantity.
iam using react v16.0.2
productItems: Array(2)
0: {"": "450000"} //productId is empty
1: {"": "670000"} //why does it only pick itemAmount but not the quantity and productId
i happen to use react-bootstarp for styling.
constructor(){
this.state = {
invoice_notes: '',
itemValues: [] //suppose to handle the productItems that are being added.
}
//for the product items
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
}
addProductItem(){
const item = {
productId: '',
itemQty:'',
itemAmount: ''
}
this.setState({itemValues: [...this.state.itemValues, item]})
}
createItemsUI(){
//Use #array.map to create ui (input element) for each array values.
//while creating the fields, use a remove button with each field,
return this.state.itemValues.map((elem,i)=> //recall arrow functions do not need return statements like {}
<div key = {i}>
<FormGroup controlId="formControlsSelect">
<ControlLabel>Product</ControlLabel>
<FormControl
componentClass="select"
name = "productId"
bsClass="form-control"
value = {this.state.itemValues[i].productId}
onChange = {this.handleItemChange.bind(this,i)}>
<option value = "" disabled>{'select the product'}</option>
{productOptions.map((item,i)=>{
return(
<option
key = {i} label= {item} value = {item}
>
{item}
</option>
)
})}
</FormControl>
</FormGroup>
<FormInputs
ncols={["col-md-4","col-md-4"]}
proprieties={[
{
label: "Quantity",
type: "number",
bsClass: "form-control",
defaultValue:this.state.itemValues[i].itemQty,
onChange: this.handleItemChange.bind(this,i)
},
{
label: "Amount",
type: "number",
bsClass: "form-control",
defaultValue: this.state.itemValues[i].itemAmount,
onChange: this.handleItemChange.bind(this,i)
}
]}
/>
<Button onClick = {this.removeProductItem.bind(this,i)}>Remove</Button>
</div>
)
}
}
please i have read many similar questions so i think iam on the right track.. I prefer to be inquisitive
So after #steve bruces comment, i made some changes to the handleItemChange function but i happen to get almost the same behaviour of not picking the productId and itemQty input values.
handleItemChange(i,evt){
const name = evt.target.getAttribute('name');
console.log('let see name', name) //i getting the name of the atrribute --> productId, itemQty, ItemAmount
const items = [...this.state.itemValues]
items[i] = {[name]: value}
this.setState({itemValues: items})
}
This is what happens if i try using setState({items}) as suggested by similar questions
items[i] = {[name]: value}
this.setState({items})
RESULT of the productItems when u click the submit button
productItems: Array(2)
0: {productId: "", itemQty: "", itemAmount: ""}
1: {productId: "", itemQty: "", itemAmount: ""}
but when i use this.setState({itemValues: items}) atleast i can get the last value of the itemAmount
productItems: Array(2)
0: {itemAmount: "25000"}
1: {itemAmount: "45000"}
length: 2
Any help is highly appreciated
I believe your error is coming up in your handleItemChange(evt,i) function.
It looks like you want to return "placeholder" by destructuring evt.target. This does not work because evt.target doesn't understand "name". I think you want to try evt.target.attribute('name') to return the name. evt.target.value will return the value of that input and evt.target.name does not exist so it returns undefined.
A small tip. You no longer have to bind functions in your constructor. You can can get rid of your constructor and update your functions to be arrow functions.
For example this:
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
becomes this:
handleItemChange = (i,evt) => {
// const {name,value} = evt.target;
//updated that to this here
const { value } = evt.target;
const name = evt.target.attribute("name");
...
And the call to in your onChange would look like this:
onChange = {() => { this.handleItemChange(evt,i)}
you must add the () => in your onChange so the handleItemChange is not invoked on every render before it's actually changed. It's invoked the moment you pass the params evt, and i. So the () => function is invoked onChange and then handleItemChange(evt,i) is invoked.
Hope this helps.

Resources