this is my first post there :)
So How to manage array saved in reactjs state?
Context: a web site I react with, which provides multiple-choice questions to students and is managed by the teacher, an academic case
{
"id": 0,
"textvalue": "Les licornes existent-elles ?",
"singlecorrectanswer": true,
"explication": "hyhy-bèikb_b",
"answer": [
{
"id": 1,
"author": 1,
"textanswer": "Evidemment, oui",
"correct": true
},
{
"id": 2,
"author": 1,
"textanswer": "Bien sur que non",
"correct": false
}
]
}
Currently, I have this as an attribute of an Input for the question value: this.state.question.text value
But I can't modify the field by entering text, maybe because onChange is not defined.
In addition, I want the user to be able to modify each answer.
For the answer it is the same problem :
I will realize a map on my "answers", a solution is to create an onChange function that will deal with the index of the map and the array of the state and modify it. But this solution is a bit ugly. Do you have a better solution to automatically bind the "value" of the field to the state?
My apologies for my English, I'm french
Thanks
You should really have provided code we could review.
Yes, your issue is likely caused because you did not provide an onChange event.
This is in the docs https://reactjs.org/docs/forms.html#controlled-components
I tested this here:
https://codesandbox.io/s/nervous-ives-ccy4u
I found a solution, but it's not the best. As jasdhfiu said I had to provide a onChange event
Here is my code
handleQuestionChange = (field, value) => {
let question = Object.assign({}, this.state.question);
question[field] = value;
this.setState({question}, () => this.updateError());
};
handleQuestionThemeChange = (value) => {
let question = Object.assign({}, this.state.question);
question.theme = this.state.themes.find(o => o.label === value);
this.setState({question}, () => this.updateError());
};
handleAnswerChange = (field, value, index) => {
let question = Object.assign({}, this.state.question);
question.answer[index][field] = value;
this.setState({question}, () => this.updateError());
};
And my event which are place on buttons :
onChange: (e) => this.handleQuestionChange('textvalue', e.target.value)
onChange: (e) => this.handleQuestionChange('explication', e.target.value)
onClick={() => this.handleAnswerChange('correct', !answer.correct, index)
onChange: e => this.handleAnswerChange('textanswer', e.target.value, index)
onChange={e => this.handleQuestionThemeChange(e.target.value)}
If there is a simpler way let me know
Thanks
I would suggest you to use onBlur instead of onChange so that you don't need to update state on every character change.
To use onBlur you need to use defaultValue instead of value.
<input defaultValue={this.state.question.text value} onBlur={e => updateValue(e.target.value) /* your update function */} type='text' />
Related
I am trying to use the Ant design cascader with expanded sub menu on hover, and because there is a lot of data in the sub menu, I only load them lazily as also potrayed in the ant design cascader website.
My cascader consists of 3 different 'lists' of data. I first load something called flows which themselves have a property item which contains either item_id or items_group_id. Items and items-group are themselves different lists of data. The most important aspect of these we need to know is items and items-group have a label, and a value, which are the text name and the unique ID. Flows are lists with additional data and also always have a list of either a few items or item groups or both. Example of each type coming from the backend:
sampleflow = [
{
"id": 13206,
"rate": 23,
"quantity": 23,
"tax": 5,
"amount": 23,
"flow": 163, // NOTE this id is same as the id of the flow
"items_group": null,
"items": 198,
"item": [
163,
198
]
}
]
sampleItem = [
{
"kit_id": null,
"product_id": {
"id": 198,
"name": "Sample Item"
}
},
{
"kit_id": {
"id": 1,
"name": "Item group random1"
},
"product_id": null
}
]
As you can see, item and itemgroup list only has the data id and name, and I have to figure out myself what type of item it is by seeing which id is being sent; either product_id or kit_id(kit and items-group are the same thing).
Note how backend is sendint he flow id which is the same as the id of the flow itself, this is because when I created this record, the frontend sent the data in the format of { name, value, type }
<Form.List name="items">
{(fields, { add, remove }) => { // code to make multiple of these cascaders
return (
<div>
{fields.map((field, index) => (
<>
<Row align="middle">
<Col span={9}>
<div>
<Form.Item name="item">
<Col>
<Cascader
expandTrigger="hover"
defaultValue={cascaderItemValue[0][index] || []}
// defaultValue={fakeCascaderData}
loadData={loadData}
options={options}
onChange={(e, value) => onChangeCascader(e, index, value)}
placeholder="Select"
changeOnSelect
/>
</Col>
</Form.Item>
</div>
</Col></row></div></Form.List>
....
const loadData = async (selectedOptions) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
const flowItems = await loadAPI(`flows-exp/?id=${selectedOptions[0].value}`);
if (flowItems) {
targetOption.children = flowItems.data.items.map((item) => ({
value: item.kit_id ? item.kit_id.id : item.product_id.id,
label: item.kit_id ? `Item-Group ${item.kit_id.name}` : `Item ${item.product_id.name}`,
type: item.kit_id ? `Item-Group` : `Item `,
}));
}
setOptions([...options]);
};
As you can see I also have it in a function that makes multiple of these according to the user's choice. That added a bit of complexity of indexing every form item else the backend won't accept the data properly nor the frontend will be able to load them in the correct order. Anyways I was able to do this perfectly with a single level dept cascader. I am trying the same concept for this, but the second level depth is making it impossible for me to load the data properly.
const onChangeCascader = (array, index, val) => {
console.log('array, index', array, index, val);
if (array[0] && array[1] && val[1]) {
const name = array[0];
const value = array[1];
const type = val[1]?.type;
setItemsSelectedOptions((prev) => ({ ...prev, [String(index)]: { name, value, type } }));
}
};
Here I am sending the data to backend using this format of { name, value, type } where the name is the flow ID, value is product select id, and because there are two types of products, one item and other item-group, i also need to send which kind it is either item or item-group. ex response: { 192, 1, 'Item'}
Basically it is an amalgamation of the features shown. I was able to load the data from my backend, and show the options perfectly. Then i formatted the data a bit before sending it back to my backend. Here is the fucntion where i format the data coming from the backend so the cascader element can load the data properly:
useEffect(() => {
const fetchData = async (id) => {
const Rawdata = await retrieveAllotmentbyid(allotmentid);
console.log('Rawdata', Rawdata);
if (Rawdata.data.flows) {
responseProductArray = Rawdata.data.flows.map((product) => ({
product_type: product.flow || '',
product_value: product.items || product.items_group,
}));
}
const rawdataflowitems = []
const newselectedOptions = []
for (let i = 0; i < Rawdata.data.flows.length; i++) {
rawdataflowitems[i] = await Promise.all([loadAPI(`flows-exp/?id=${Rawdata.data.flows[i].flow}`)]);
}
console.log('rawdataflowitems', rawdataflowitems);
responseProductArray.map((prod) => {
tempProductArray.push([(prod.product_type), prod.product_value])
})
console.log('responseProductArray', responseProductArray, tempProductArray);
const { data } = Rawdata;
if (data.flows) {
data.items = data.flows;
}
for (let i = 0; i < data.items.length; i++) {
data.items[i].item = tempProductArray[i]
}
const cascaderItemArray = []
if (data.items) {
for (let i = 0; i < data.items.length; i++) {
if (data.items[i].items) {
cascaderItemArray[i] = ['product', data.items[i].items];
onChangeCascader(cascaderItemArray[i], i, 'items')
setCascaderItemValue([...cascaderItemValue, cascaderItemArray]);
} else {
cascaderItemArray[i] = ['kit', data.items[i].items_group];
onChangeCascader(cascaderItemArray[i], i, 'items_group')
setCascaderItemValue([...cascaderItemValue, cascaderItemArray]);
}
}
}
console.log('cleaned data after fetching', data);
console.log('cascaderItemValue', cascaderItemValue);
form.setFieldsValue({
...data,
});
}
if (allotmentid) {
fetchData(allotmentid);
}
}, [allotmentid])
In the fetchdata, there is an api call to the backend which loads the data of what items each of the flow sent from the backend contain. It is impossible for me to load every item in every flow as there are 100s of flows with around 5 items in each. So i only load the data of the flows I am getting from the backend. But the issue is, i am unable to load this data into the options state which i use as the options for the cascader. When a user manually uses the cascader, and hovers over the options, i am able to load the data from the backend and update the options which is a state. What I basically do when a user manually hovers over the cascader items, I take the value of the hovered flow, then make an api call to the backend and load the items data and format them properly. Then I add that to the children of the option which was hoveredd using the onChangeCascader i have mentioned above. I believe this is the issue I am having. After literally spending two shifts of 8 hours, i am very clueless on how to do this, or even if this apporach is correct.
This is how create the initial options for the cascader made up of only flows data:
useEffect(() => {
let flowOpts = [];
if (flows) {
flowOpts = flows.map((flow) => ({
value: flow.id,
label: flow.flow_name,
isLeaf: false,
}));
}
if (options.length == 0) {
setOptions(flowOpts);
}
}, [flows]);
After hovering, the children part is loaded and the items or item-groups are selectable and this is how the options look like:
{
"value": 163,
"label": "Rajah Garner",
"isLeaf": false,
"children": [
{
"value": 198,
"label": "Item 0",
"type": "Item "
},
{
"value": 1,
"label": "Item-Group 78465",
"type": "Item-Group"
}
]
}
Safe to say I have used atleast 100 console logs in this single file and am trying to track the data and its format in every way, but still I am helpless on how to approach this.
I understand this is an obscure and long problem, but i was unable to find anything similar on google or even SO. I even messaged the author of the cascader element from ant design to no avail. Please let me know what else details is needed to atleast discuss this.
I have data in the form of JSON as given below:
[
{
"course_name": "React",
"course_id": 2
},
{
"course_name": "Python",
"course_id": 1
}
]
Below is the code I have written in React. According to this whenever there is a change in the dropdown, the id is stored in the value.
<Select
labelId="demo-simple-select-outlined-label"
id="demo-simple-select-outlined"
onChange={handleChange}
label="CourseList"
>
{
dropdown.map(options =>
<MenuItem value={options["course_id"]} >{options["course_name"]}</MenuItem>
)
}
</Select>
const handleChange = (event) => {
handleChangecourse_id(event.target.value);
setVal(event.target.value);
}
Basically, I want both the values(whatever gets selected, it's course_id and course_name) and set it to two different states.
What I thought roughly if(val === dropdown.map(options=> options["course_id"]), then store the course_name somewhere, but I am unable to proceed.
Please help.
Thank you in advance.
Use below method after getting the selected id to get complete object.
let course = dropdown.filter(val => val.course_id==event.target.value);
I have a calendar app that renders its events with an array of objects:
const [events, setEvents] = useState([
{
title: "Werk",
start: moment().toDate(),
end: moment(),
allDay: false,
},
])
I am trying to allow the user to add new events with the inputs on the page.
So far I am just trying to change the title of the event with this code
<input
placeholder="Name"
onChange={(e) =>
setEvents([{ ...events, title: e.target.value }, console.log(events.title)])
}
required
></input>
When I console.log(e.target.value) it gives me what I am typing in, but the (events.title) is giving me undefined.
My end goal is to have the user add new events with the inputs in the following format. If I'm on the wrong track, or you know a better way I'd love to hear it.
const [events, setEvents] = useState([
{
title: "Werk",
start: moment().toDate(),
end: moment(),
allDay: false,
},
// this is what the user will add
{
title: "Example",
start: Number,
end: Number
}
])
Thank you in advance, my full code is Here And here is a codesandbox: https://codesandbox.io/embed/gracious-minsky-pp1w5?codemirror=1
I think that that console.log is missplaced, you shouldn't be calling that function inside an array assignment. And even if you call it below the setEvents function will not give you the title because the set function of the useState is asynchronous. Also I think you are not doing right the assignment of the events into your array.
In this line:
setEvents([{ ...events, title: e.target.value }, console.log(events.title)])
You are spreading the events inside a new array and then adding the title, leaving you with a structure similar to this:
[{
"0": {
"title": "Werk",
"start": "2020-12-05T05:12:57.931Z",
"end": "2020-12-05T05:12:57.931Z",
"allDay": false
},
"title": "myNewTitle"
}]
And nesting this titles for every change in the input.
For the structure you want to achieve you must push a new entry into an array that already contains your previous events, and then update the state. Like:
//I think the best approach is calling this in a submit or button click event
const buttonClick = () => {
const newEvents = [...events]; //This spreads every element inside the events array into a new one
newEvents.push({
title: eventTitleState, //You also need a useState for the title
start: moment().toDate(),
end: moment(),
});
setEvents(newEvents);
}
You can see this idea working here: https://codesandbox.io/s/green-cherry-xuon7
Your method of changing title is incorrect. Since it is array of objects, you will need to get to the specific object and then change the particular title. In your case replacing this code with yours helps in changing title of 0 index.
<input
placeholder="Name"
onChange={(e) =>{
let tempArr = [...events];
tempArr[0].title = e.target.value;
setEvents(tempArr)
}}
required
>
If you also want to add new objects, push it in tempArr on some event and update setEvents.
I have this sample code which is a search input that filters and returns different colors in a list. When you click on one of the items the innerHTML from that list item then populates the value in the input field.
My question is when I click on one of the list items, I would want to get that specific list item and store it in another list. Much like a search history. Is this difficult to add to the current state of the search field? How would you suggest that I'd approach this?
Do i need to have some kind of onSubmit function to achieve this?
See this example:
https://codesandbox.io/s/autocomplete-6lkgu
Thanks beforehand,
Erik
You can create a state variable searchHistory, which can be array, and onClick of the item, you can do :
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText,
searchHistory: [...this.state.searchHistory, e.currentTarget.innerText],
});
};
I'm assuming you want all of the search items to be unique? You can use a set for that. Here is the code below for your AutoComplete component.
constructor
constructor(props) {
super(props);
this.state = {
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: "",
searchHistory: new Set([])
};
}
onClick
onClick = e => {
const { searchHistory } = this.state;
searchHistory.add(e.currentTarget.innerText);
this.setState(
{
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText,
searchHistory: [
...searchHistory,
searchHistory
]
},
() => {
console.log("[search history]", this.state.searchHistory);
}
);
};
You can simply create an array in your state and push each option into it inside your onClick handler. I made a simple example here that just logs the history when it changes. You can then use/display it any way you'd like.
Note that you'd also need to add similar functionality inside of your onKeyDown handler, as it appears you're also triggering a change with the enter key. I updated the fork to include this.
so i'm developing a personality quiz app using one of the tutorials i found on the internet //mitchgavan.com/react-quiz/, I have a quizQuestions.js file for api where i fetch the answer and the question from, like so
{
question: "I am task oriented in order to achieve certain goals",
answers: [
{
type: "Brown",
content: "Hell Ya!"
},
{
type: " ",
content: "Nah"
}
]
},
it has type and content, so this is the initial state of the app, every time the user click on Hell YA button it will increment that type +1, for example Brown: 1 etc.. but the problem is, when user select Nah it will give me this :null , I have a AnswerOption.js component like so
function AnswerOption(props) {
return (
<AnswerOptionLi>
<Input
checked={props.answerType === props.answer}
id={props.answerType}
value={props.answerType}
disabled={props.answer}
onChange={props.onAnswerSelected}
/>
<Label className="radioCustomLabel" htmlFor={props.answerType}>
{props.answerContent}
</Label>
</AnswerOptionLi>
);
}
AnswerOption.PropTypes = {
answerType: PropTypes.string.isRequired,
answerContent: PropTypes.string.isRequired,
answer: PropTypes.string.isRequired,
onAnswerSelected: PropTypes.func.isRequired
};
and my setUserAnswer function like so
setUserAnswer(answer) {
const updatedAnswersCount = update(this.state.answersCount, {
[answer]: {$apply: (currentValue) => currentValue + 1}
});
this.setState({
answersCount: updatedAnswersCount,
answer: answer
});
}
my question is, how can i let react ignore that white space, so when user click Nah it will not do anything with it, and if there is different approach to the problem i will be gladly accept it, thanks in advance.
Simple solution to your problem is to check if answer is empty :
if(answer.trim()) {
const updatedAnswersCount = update(this.state.answersCount, {
[answer]: {$apply: (currentValue) => currentValue + 1}
});
this.setState({
answersCount: updatedAnswersCount,
answer: answer
});
}