I'm attempting to set the state of a specific element (a checkbox) so that I can later pass this data back to a parent component that will in turn update a JSON object. I am able to set the state of higher level elements but I am not understanding how to access the nested values.
How do I use .setState to set the state of a specific element? Such as this.state.data[0].checked
I'm attempting to set the state with something like this which would only update data at the moment:
handleChange: function(event) {
this.setState({data: event.target.value}});
},
It looks like you could use the immutability helpers, and if you are using a numerical / dynamic key, you should look at my question here.
Your solution would look something like this:
handleChange: function(index, event) {
var data = React.addons.update(this.state.data, {
[index]: {
checked: {$set: event.target.checked}
}
});
this.setState({
data: data
})
},
Notice the use of e.target.checked and not e.target.value for checkboxes to get the boolean state and not the value associated with the checkbox.
This is how you'd attach your function with the i index you'd have to set beforehand:
onChange={this.handleChange.bind(this, i)}
Related
Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
I am using the react-table package (version 7.1.0).
I have a table which displays some invoices like so:
The user should be able to select some or all of these items using the selected checkbox.
The selected field is not part of the data. However, when the user hits a selected checkbox, the field should toggle and an array storing document numbers should be populated.
To store the document numbers, I have a stateful getter and setter:
const [selectedInvoiceIds, setSelectedInvoiceIds] = useState([]);
To populate the field, I am attempting to simply add the document number to the array immutably, from the onChange of the checkbox:
{
Header: "Selected",
accessor: "selected",
Cell: item => {
const { documentNumber } = item.row.values;
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds([...selectedInvoiceIds, documentNumber]);
}}
/>
);
}
}
When a checkbox is clicked for the first time, the selectedInvoiceIds becomes populated:
selectedInvoiceIds: ["706942"]
The problems are:
The table does not update to reflect the state change, despite this prop on the checkbox:
checked={selectedInvoiceIds.includes(documentNumber)}
The selectedInvoiceIds value gets overwritten when another document number is added, instead of being added to, as if the state is re-initialising to [] somewhere in between.
Can it be explained why these state issues are occurring and how to get around it?
I am aware of the useTableState value exposed by react-table, but I don't know how I can apply it to this use case.
Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
There are multiple issues in this code:
// 1) updates to state should should use callback function if it uses prv state
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds(prvSelectedInovicesId => [...prvSelectedInovicesId, documentNumber]);
}}
/>
);
// 2) also columns is using useMemo, however not providing dependencies, so columns are not being updated when the selectedInvociesIds state gets updated
// 3) you are not yet handling the toggle, this mean you are just always just adding to the array and not removing from it
here is a working version code sandbox
https://codesandbox.io/s/react-table-state-not-updating-wkri3?file=/src/App.js
Because of useMemo, add your array to last parameter
const columns = React.useMemo(
// ....
,
[selectedInvoiceIds]
);
And as we need toggle checkboxed it is more reasonable to keep selected ids in object instead of array and update it as this
setSelectedInvoices({
...selectedInovicesIds,
documentNumber: !selectedInovicesIds[documentNumber]}
)
so it will toggle mark
My Parent component retrieves the checked inputs value from its child component.
So I have a function (handleChecked) that takes two parameters : one 'id' and one checked status 'isChecked' :
const handleChecked = useCallback(({ id, isChecked }) => {
const newCheckedValues = checkedValues.filter(current => current !== id);
setCheckedValues(newCheckedValues);
if (isChecked) {
newCheckedValues.push(id);
setCheckedValues(newCheckedValues);
}
}, [checkedValues]);
What my function is supposed to do :
1 - Get id and checked status from the clicked input,
2 - Check the state for duplicate id's,
3 - If present, remove it,
4 - Save the state,
5 - If checked, store the id in a temporary array,
6 - Save the new state.
What it does :
Well, all the task above except adding the value to the new state.
When I click a new input, the parent "checkedValues" state is empty and starts over from nothing.
Meaning that the temporary array created as the result of the filtered state, is also empty.
Right now, this function just adds an id in the state, then replace it by the new clicked input.
And I need to gather all the checked values and store them in the state before sending that to an api later. I manage to present my "expectations" in this sandbox :
https://codesandbox.io/s/react-hooks-filter-state-array-toggle-input-checked-gz504
It works, the only difference is it's in the same component and uses html form event and not props legacy.
Finally, I deconstructed my component in 3 :
A Parent component with the checkedValues state which calls a "ListItem" child, passing datas and states as props ;
A ListItem component which contains the handleCheck function and map the datas to display, passing the infos and the function to each mapped child.
An Item component which displays an input with its datas and the handleCheck function + deals with its own isChecked state to modify CSS dynamically according to its status.
This answer is of course customized to my own purposes and needs. It's not the most general but it answers my question and can maybe help someone. Thanks again #ChrisG for your help
I have a table of data that is rendered via an api call. It displays 4 values from the initial object. I mainly care about the name value as this is what's important later in a post request.
Each row in the table has a checkbox. When the checkbox is selected (true) the name associated with that checkbox is added to an object called selectedFields, as an object. For example If I select the checkbox with name id it creates an object store like:
"selectedFields": {
"id" : {}
}
This works fine and well. However, I've added 3 input boxes that are associated with each name. The inputs are lengthType, size, maxArrayElements, which are of course user selectable.
What I'm having trouble with is adding these values back to the object so it looks like:
"selectedFields": {
"id": {
lengthType: Variable,
size: 1,
maxArrayElements: 1
},
"price": {
lengthType: Fixed,
size: 10,
maxArrayElements: 1
}
}
How can I add these 3 values back to the name object that was created so it looks like the above example?
I don't want to post a wall of code, so I'm posting the checkbox function that handles creating the selectedFields object with the appropriate selected names. I suspect that the input values should get added here somehow, but I'm not sure.
checkbox = ({ name, isChecked }) => {
//handle check box of each fieldName
const obj = this.state.fieldNames.find(field => field.name === name);
if (isChecked === true) {
//checked conditional
this.setState(
{
selectedFields: {
...this.state.selectedFields,
[name]: {
...obj
}
}
},
() => {
console.log(
"callback in isChecked if conditional",
this.state.selectedFields
);
}
);
} else {
const newSelectedFields = this.state.selectedFields;
delete newSelectedFields[name];
this.setState(
{
selectedFields: newSelectedFields
},
() => {
console.log(
`box unchecked, deleted from object --->`,
this.state.selectedFields
);
}
);
}
};
You will have to make the first dropdown selection to view the data.
CodeSandbox link here
Answer
You need to change a few things because nothing is stating where to assign the new state outside of the root state object.
Handlers in your Index.js:
Your handlers for the change events aren't looking for the name of the object to determine if it exists or not. If you want to add it to the specified child object you better be sure it's there.
We adjust the handlers to take an object name and setState with object.assign on that specific object if it exists:
note: since lengthType doesn't have a name property we simply provide it with a string. e.currentTarget will provide the option span, not the root Dropdown element, so even supplying a name property to that component wouldn't allow us to use e.currentTarget.name - you may want to consult the Semantic UI documentation if you would prefer something different. I gave it a quick scan but didn't want to deep dive it.
handleChange = (e, obj_name) => {
if (this.state.selectedFields[obj_name]) {
let selectedFields = Object.assign({}, this.state.selectedFields);
selectedFields[obj_name] = Object.assign(
this.state.selectedFields[obj_name],
{ [e.target.name]: e.target.value }
);
this.setState({ selectedFields });
}
};
onLengthTypeChange = (e, obj_name) => {
if (this.state.selectedFields[obj_name]) {
let selectedFields = Object.assign({}, this.state.selectedFields);
selectedFields[obj_name] = Object.assign(
this.state.selectedFields[obj_name],
{ lengthType: e.currentTarget.textContent }
);
this.setState({ selectedFields });
}
};
The above, of course, won't work if you don't adjust your onChange events on your components so that, in addition to your event object, they also send your object name.
Handlers in your Component file:
Note: It was odd because in your Index.js file you seemed to half do this with lengthType but you weren't passing over additional data. You can't simply pass parameters into a handler - to get it to work you need to pass an anonymous function to the onChange properties that will take the event and pass it on to the handler functions with your object name:
<Table.Cell>
<Dropdown
placeholder="Pick a length Type:"
clearable
selection
search
fluid
noResultsMessage="Please search again"
label="lengthType"
multiple={false}
options={lengthTypeOptions}
header="Choose a Length Type"
onChange={e => onLengthTypeChange(e, name)}
value={lengthType}
required
/>
</Table.Cell>
<Table.Cell>
<Input
onChange={e => handleChange(e, name)}
value={this.state.size}
type="number"
name="size"
min="1"
placeholder="1"
required
/>
</Table.Cell>
<Table.Cell>
<Input
onChange={e => handleChange(e, name)}
value={this.state.maxArrayElements}
type="number"
name="maxArrayElements"
placeholder="1"
min="1"
max="100"
required
/>
</Table.Cell>
Once these things are adjusted, the code will update the specified properties on the child objects after the corresponding checkbox is selected.
Final Note:
I did not adjust it to save the previous state if you uncheck and then check the box. It wasn't specified in your question and I don't want to make assumptions.
Code Sandbox:
The adjusted code sandbox: https://codesandbox.io/s/createqueryschema-table-rewrite-bwvo4?fontsize=14
Additional Recommendations:
In your initial state your selectedFields is declared as an Array and then it is promptly turned into an Object when any checkbox is selected. I would suggest not doing this. Changing data types on a property during the course of running an application is very much asking for trouble.
When a checkbox is loaded you provide a checkbox function from your Index.js file. This is simply called box in your component. I would suggest keeping the names of properties and state equivalent when passing down from parent to child. It is much, much, much easier for someone else to come in and maintain if they have to - not to mention easier to retain your own sanity.
The above checkbox function takes props from child and passes them up to the parent. This would be the place to pass your collected data into a cache on the parent, into local/session storage, or whatever you want to do with your data. You could instead write code to the effect of: if the checkbox is selected when an input handler is called do a save - but I would say that it would probably be best on render since the screen is constantly updating anyway and you have the checkbox function readily passing props currently. This is preference, so it's your call
Good luck! Hope this helped!
I'm trying to bind a UI element (a single-line textbox or 'input' element) with a Vuex store. This fiddle has the code.
When the SearchResult component is visible, it auto-updates -- see the GIF below, where Lisp or Prolog is typed. That's not what I'd like to happen. What I'd really like to do is decouple the UI state (i.e. the value of the textbox) from the model's state, so that if I type Lisp and press Search, the SearchResult component updates itself.
Ideally I'd like to bind the textbox with a variable that's not in the store, but also add some code to observe changes to the store, so that any changes to the store are reflected in the UI.
I read the forms handling documentation for Vuex but wasn't very clear about the best way to get this done. Please could anyone help? I'm new to SPAs so I'm sure there's a better way of getting this done.
I think the approach you have used is the general approach if you want to use a store variable in input. Given that you want to decouple the UI variable with the model's state(Why?), you can do following:
Have a local variable in that vue instace
use that local variable with v-model
put a watch on state variable, if state variable changes, change local variable.
set state variable on button press, or some other way like onblur event
Here are relevant JS changes:
const app = new Vue({
router,
el: '#app',
data: {
localQuery: ''
},
computed: {
query: {
get () { return store.state.query },
set (v) { store.commit('setquery', v) }
}
},
methods: {
s1: function () {
console.log('app.s1 this.query: ' + this.query);
this.query = this.localQuery
router.push({ name: 'qpath', params: { query: this.query }});
}
},
watch:{
query: function (newVal) {
this.localQuery = newVal
}
}
})
see updated fiddle here.
I am trying to create an filter list which filter the list on typed input. But don't know why the if statement is not working.
Here is the function I wrote: `
filterList (event) {
var updatedList = this.props.array;
var filterText = this.state.text;
updatedList = updatedList.filter(function(item){
console.log(filterText);
if(item.name === filterText){
console.log('check', item.name);
}
});
Can someone help me out on this here is a link to my codepen: LINK
Your filter needs to update the array in the component state to force a re-render. You don't update props to force a re-render, props are more for initial data, or consistent data, which can be updated from the top level.
Change the handle function to pass the text to the filterList function, and return the result
handleChange(event) {
var array = this.filterList(event.target.value);
this.setState({ text: event.target.value, array: array });
},
filterList (filterText) {
var updatedList = this.props.array;
return updatedList.filter(function(item){
return item.name === filterText;
});
}
Update getInitialState to use the props:
getInitialState() {
return { text:'', array: this.props.array};
}
Then use the state instead of props in your render:
var arrayComponents = this.state.array.map(function(photo) {
return <li className="photo photo-name">{photo.name} <img className="photo" src={photo.link}/></li>;
})
First, when I run your codepen example, I'm not seeing any errors indicating that this.state.text is undefined. It's not filtering anything, but it is displaying the value of this.state.text. So the issue is not quite what your question suggests it is...
So how to we get this thing filtering? Basically, the idea with ReactJS components is that everything related to the current state of your component should be in this.state, and any decisions about what to display based on that state should be in the render method. And keep in mind that any time you change the state using the setState method, it's going to trigger a re-rendering of your component.
With that in mind, I'd set things up like this:
your component receives a list of photos via the array prop (I'd probably call it something else, since it's not very descriptive and it's awfully close to a reserved word in Javascript)
the state for the component has one value: text (again, not very descriptive)
the component has a handleChange handler for the input element. I would connect this to the onChange handler for the input element - don't worry, it will be called every time the value in the input element changes. This should be the only event handler on the input element and all it should do is call this.setState({ text: event.target.value });
do the list filtering in the render method. The key here is that you don't need to keep a filtered list of your photos - all you're doing with it is rendering it, so do it only when you need to (or rather, when React thinks you need to and calls the render method). So your render method will look something like this:
render() {
var myFilteredList = this.props.array.filter(function(item){
console.log(filterText);
if(item.name === filterText){
console.log('check', item.name);
return true;
}
return false;
});
var arrayComponents = myFilteredList.map(function(photo) {
return <li className="photo photo-name">{photo.name} <img className="photo" src={photo.link}/></li>;
});
return (
<div>
<h1>Hello, {this.props.name}</h1>
<p>{this.state.text}</p>
<input type="text" onKeyUp={this.handleChange} />
<ul>
{arrayComponents}
</ul>
</div>
);
}
That's it. This has a couple of advantages:
it keeps it simple - don't maintain any state for the component outside of this.state and if you only need it for rendering, don't maintain it at all.
it keeps your code cleaner - the way I've approached it, your component has only two methods: handleChange and render
it (should be) more performant - React is pretty good at deciding when to render based on state changes, and it tends to be better when components have minimal state. (I say "should be" simply because I haven't tested it out at all).