How do I handle onChange of objects inside an array (React State)? - javascript

community,
I'm trying to build a table where its rows are rendered based on an array of objects:
this.state = {
tableContent: [
{
id: this.state.tableContent.length,
firstname:"",
lastname:""
},
...
]
Each row displays one object. (2 columns in this case, n rows)
Each cell is also an input field, so users can manipulate the table:
<tbody>
{this.state.tableContent.map((row) => {
return(
<tr key={row.id}>
<td><input value={row.firstname} onChange={this.handleFirstNameChange}> </input></td>
...
</tr>
)
})}
</tbody>
I want each cell / input field to display the change when the user changes the input value and as such the state. Because I'm prepopulating the input field with value={row.firstname} I need to define an onChange handler function that changes the state/value of the target object's firstnameproperty.
So how does my onChange handler function look like?
I tried using spreads, but to no avail so far...
Some thoughts:
Using the standard procedure doesn't work because I have a nested state (Array of objects):
handleChange = (event) => { this.setState({ firstname: event.target.value }) }
Trying to use the spread operator results in some weird mess as well:
(this code here is somewhat wrong: maybe you can fix it?)
handleFirstNameChange = (event) => {
const {tableContent} = {...this.state};
const currentState = tableContent;
const { firstname, value } = event.target;
currentState[0] = value;
this.setState({ tableContent: currentState});
}
I appreciate any help!
edit:
The code below seems to nearly work. (thanks #Nikhil ) However now, whenever the user types into the input field, every letter they type will replace the existing letter / value of 'row.firstname'. Also, state doesn't refresh automatically so only the last-typed letter would show up / persist. What I need the input field to have is a functionality just like any casual input field.
event.persist(); seems to be needed to keep the event value.
handleFirstNameChange = (id, event) => {
event.persist();
this.setState(prevState => {
tableContent : prevState.tableContent.forEach((row) => {
if(row.id === id) { row.firstname = event.target.value}
})
})
}
input looks like this:
onChange={(event) => this.handleWNRChange(row.id,event)}

I think something like this would work.
const handleFirstNameChange = (id, event) => {
event.preventDefault();
const {tableContext} = this.state;
const myRowIndex = tableContext.findIndex((row) => row.id === id);
tableContext[myRowIndex].firstname = event.target.value;
this.setState({ tableContext });
}
This should be all you need. Just assign this method to the onChange of the input element. Like so:
onChange={(event) => this.handleFirstNameChange(row.id, event)}

May be this will help
{this.state.tableContent.map((row, index) => {
return(
<tr key={row.firstname}>
<td><input value={row.firstname}
onChange={(event)=>this.handleFirstNameChange(event, index)> </input></td>
</tr>
)
})
}
handleFirstNameChange(event, index){
this.setState((prevState => {
tableContent : prevState.tableContent.forEach((row,i)=>{
if(index === i) row.firstname = event.target.value
})
})
}

Related

Selecting and deseleting all checkboxes using hooks

I have an array state for some checkboxes where I am catching the labels for those that are true (checked). Must ignore the false.
I am able to generate a list of checked checkboxes thanks to some of you in another thread. But I'm hitting another wall with the select all toggle.
const handleSwitch = (e) => {
if(e.target.checked) {
setActive(true);
const updatedCheckedState = checkedState.map(element => element.checked = true);
setCheckedState([...updatedCheckedState]);
} else {
setActive(false)
const updatedCheckedState = checkedState.map(element => element.checked = false);
setCheckedState([...updatedCheckedState]);
}
}
This function above in particular. Likewise, if I manually check all of the checkboxes inside, it needs to know that all are selescted and make the active state = true. If I can get help with at least the first part, I'm confident I can solve the other part myself.
Here's a sandbox if you want to mess around with it. Thanks
Your sandbox is quite broken. The way you are tracking checked state is internally inconsistent.)
The main culprits (in Filter.js) are:
on line 119, you treat checkedState like a dictionary, but in handleSwitch and handleOnChange you treat it like an array (but the logic inside is still non-functional for the array approach as far as I can tell.
if you want it to be an array, let it be a string-valued "checkedLabels" array, and set checked on your checkbox component to checkedLabels.includes(item.label)
if you want it to be a dictionary:
handleOnChange needs to simply toggle the currently clicked element, like so [e.target.name]: !checkedState[e.target.name]
handleSwitch needs to add an entry for every element in data, set to true or false as appropriate.
Example (codesandbox):
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedState(
Object.fromEntries(data.map((item) => [item.label.toLowerCase(), true]))
);
} else {
setActive(false);
setCheckedState({});
}
};
const handleOnChange = (e) => {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedState[item.label.toLowerCase()] ?? false}
onChange={handleOnChange}
/>
EDIT from OP
I tweaked the hnadleOnChange function to
const handleOnChange = (e) => {
if (e.target.checked) {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
} else {
const updatedCheckedState = {...checkedState};
delete updatedCheckedState[e.target.name];
setCheckedState(updatedCheckedState);
}
};
Before, it allowed for false values to be added when you unchecked a previously checked checkbox. This removes it
Edit: To do this with an array, you'll want to add to the array when checking, and remove from it when un-checking. Then do an includes to see if an individual checkbox should be checked.
Also, you can do a simple setActive(newCheckedItemLabels.length === data.length); in the handleOnChange to achieve your other requirement.
This codesandbox does everything you need with arrays instead of objects.
Notably:
const [checkedItemLabels, setCheckedItemLabels] = useState([]);
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedItemLabels(data.map((item) => item.label.toLowerCase()));
} else {
setActive(false);
setCheckedItemLabels([]);
}
};
const handleOnChange = (e) => {
const newCheckedItemLabels = checkedItemLabels.includes(e.target.name)
? checkedItemLabels.filter((label) => label !== e.target.name)
: [...checkedItemLabels, e.target.name];
setCheckedItemLabels(newCheckedItemLabels);
setActive(newCheckedItemLabels.length === data.length);
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedItemLabels.includes(
item.label.toLowerCase()
)}
onChange={handleOnChange}
/>
Adding to #JohnPaulR answer. You can add useEffect hoot to achieve additional requirements you have.
if I manually check all of the checkboxes inside, it needs to know that all are selected and make the active state = true.
useEffect(() => {
const checkedTotal = Object.keys(checkedState).reduce((count, key) => {
if (checkedState[key]) {
return count + 1;
}
}, 0);
setActive(data.length === checkedTotal);
}, [checkedState]);
A full working example https://codesandbox.io/s/still-water-btyoly

How to update key value inside array when state changes?

I am using React.js and in render method I have <Range/> component. What I want to do, is to be able to change the value and update the state's array accordingly. This is what I am talking about:
...
{
this.state.tableData.map((i, k) => {
return (
<>
<tr key={k}>
<td>
<div>
<Range
values={[i.s_rate]}
min={MIN}
max={MAX}
onChange={(values) => this.onSliderChangeS(values, k)}
...
So I trigger the onSliderChangeS function, which takes the new value of s_rate as values from the user, and it has to update and show my new this.state.tableData accordingly. I am trying to do it this way:
onSliderChangeS = (values, key) => {
this.setState({
valuesS: values,
tableData[key].s_rate: parseInt(values)
})
}
But this line this.state.tableData[key].s_rate: parseInt(values) doesn't seem to work at all. How can I do that?
setState takes an object with key/value entries. this.state.tableData[key].s_rate itself is not a key name in your state. See if this helps:
onSliderChangeS = (values, key) => {
const newTableData = [...this.state.tableData]
newTableData[key].s_rate = parseInt(values)
this.setState({
valuesS: values,
tableData: newTableData
})
}
Also note the missing comma as pointed out by #FaFa

How can I use this React component to collect form data?

I've created two components which together create a 'progressive' style input form. The reason I've chosen this method is because the questions could change text or change order and so are being pulled into the component from an array stored in a JS file called CustomerFeedback.
So far I've been trying to add a data handler function which will be triggered when the user clicks on the 'Proceed' button. The function should collect all of the answers from all of the rendered questions and store them in an array called RawInputData. I've managed to get this to work in a hard coded version of SurveyForm using the code shown below but I've not found a way to make it dynamic enough to use alongside a SurveyQuestion component. Can anybody help me make the dataHander function collect data dynamically?
There what I have done:
https://codesandbox.io/s/angry-dew-37szi2?file=/src/InputForm.js:262-271
So, we can make it easier, you just can pass necessary data when call handler from props:
const inputRef = React.useRef();
const handleNext = () => {
props.clickHandler(props.reference, inputRef.current.value);
};
And merge it at InputForm component:
const [inputData, setInputData] = useState({});
const handler = (thisIndex) => (key, value) => {
if (thisIndex === currentIndex) {
setCurrentIndex(currentIndex + 1);
setInputData((prev) => ({
...prev,
[key]: value
}));
}
};
// ...
<Question
// ...
clickHandler={handler(question.index)}
/>
So, you wanted array (object more coninient I think), you can just save data like array if you want:
setInputData(prev => [...prev, value])
Initially, I thought you want to collect data on button clicks in the InputForm, but apparently you can do without this, this solution is simpler
UPD
Apouach which use useImperativeHandle:
If we want to trigger some logic from our child components we should create handle for this with help of forwarfRef+useImperativeHandle:
const Question = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(
ref,
{
getData: () => ({
key: props.reference,
value: inputRef.current.value
})
},
[]
);
After this we can save all of our ref in parent component:
const questionRefs = React.useRef(
Array.from({ length: QuestionsText.length })
);
// ...
<Question
key={question.id}
ref={(ref) => (questionRefs.current[i] = ref)}
And we can process this data when we want:
const handleComplete = () => {
setInputData(
questionRefs.current.reduce((acc, ref) => {
const { key, value } = ref.getData();
return {
...acc,
[key]: value
};
}, {})
);
};
See how ref uses here:
https://reactjs.org/docs/forwarding-refs.html
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
I still strongly recommend use react-hook-form with nested forms for handle it

Getting back the value of a `cleared` input element with React Select

I have a several ReactSelect components, and one piece of global state which is responsible for holding all my selected values in an array.
Select
<Select
styles={inputStyles}
className="basic-single"
classNamePrefix="select"
isClearable={true}
isSearchable={false}
placeholder={'Select Your Most Convenient Time Slot'}
options={newHoursArr}
isMulti={false}
onChange={(values) => handleChange(values)}
defaultValue={clientServiceReferral.selectedTimeSlots.map((referral) => (
referral.timeSlot === timeWithDate ? (
{ ['label']: referral.value, ['value']: referral.value }
) : null
))}
/>
handleChange function
const handleChange = (value) => {
const found = clientServiceReferral.selectedTimeSlots.find(element => element.timeSlot === timeWithDate);
if (found) {
clientServiceReferral.selectedTimeSlots.splice(found, 1)
}
const newValue = {
timeSlot: timeWithDate,
value: value.value
}
setClientServiceReferral({
...clientServiceReferral,
selectedTimeSlots: [...clientServiceReferral.selectedTimeSlots, newValue]
})
}
ReactSelect has an isClearable prop. Which allows users to clear the input with a button click. This returns a value of null when values is logged in the onChange function, but is there a way to return the actual value inside the select that is getting cleared when the clear button is clicked?
There's an optional second parameter passed to the onChange event. It's of this type:
export interface ActionMetaBase<Option> {
option?: Option | undefined;
removedValue?: Option;
removedValues?: Options<Option>;
name?: string;
}
Now, I've never used this library, but it looks like removedValue or removedValues could be helpful? idk.
Anyway, I got that from their docs. Hope it works out for you:
For anyone interested, Via Joshua Wood's answer, the value of any cleared item(s) can be found as so:
onChange={(values, removedValue) => handleChange(values, removedValue)}
const handleChange = (value, removedValue) => {
if (removedValue.action === 'clear') {
console.log('removed', removedValue.removedValues[0])
}
// removedValues returns an array

Visit each child in props.children and trigger a function

I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.

Categories

Resources