Dynamically Generated React Controlled Inputs - javascript

I have a window where a group of 3 HTML combo box(select box) are generated on every button click.
lets say category,subcategory and data. options in sub category and data are dynamically rendered based on its previous selected value.
i am storing the selected values as an array of object like
const [exerciseData, setExerciseData] = useState([[]]);
Sample data:
exerciseData : [
//first group
[
category:"",
subcategory:"",
name:""
],
//second group
[
category:"",
subcategory:"",
name:""
]
]
So literally first group of input can be indicated by exerciseData[0] and next by exerciseData[1].category etc..
The problem is i want there groups like to be reordered by the user like moving up 1 group or moving down by 1.So i need the select boxes to be a controlled element. But how can i set that?
I tried like this :
<select name="category" value={exerciseData[i].category} ... > {options} </select>
when i give like this the select box value cannot be changed.it always stays at its default value.
any suggestion will be appreciated
Thanks in advance

To make an input controlled, you need to manually handle the input value via state. In your example, the input value seems to be fixed/static.
The following snippet is an example of how to create a controlled select input in React.
Update:
Made a full demo snippet. Please let me know if that is what you was looking for.
const {useState, useCallback, Fragment} = React;
const categories = [
{
id: 'CAT.1',
data: [
{
id: 'CAT.1 SUB.A',
data: [
{id: 'CAT.1 SUB.A OPT.1'},
{id: 'CAT.1 SUB.A OPT.2'},
{id: 'CAT.1 SUB.A OPT.3'}
]
},
{
id: 'CAT.1 SUB.B',
data: [
{id: 'CAT.1 SUB.B OPT.1'},
{id: 'CAT.1 SUB.B OPT.2'},
{id: 'CAT.1 SUB.B OPT.3'}
]
}
]
},
{
id: 'CAT.2',
data: [
{
id: 'CAT.2 SUB.A',
data: [
{id: 'CAT.2 SUB.A OPT.1'},
{id: 'CAT.2 SUB.A OPT.2'},
{id: 'CAT.2 SUB.A OPT.3'}
]
},
{
id: 'CAT.2 SUB.B',
data: [
{id: 'CAT.2 SUB.B OPT.1'},
{id: 'CAT.2 SUB.B OPT.2'},
{id: 'CAT.2 SUB.B OPT.3'}
]
}
]
},
];
function Select(props) {
const {
name,
options = [],
onIndex
} = props;
const [value, setValue] = useState(options[0] && options[0].id);
const onChangeHandler = useCallback((e) => {
onIndex && onIndex(options.findIndex((item) => item.id === e.target.value));
setValue(e.target.value);
}, [onIndex, options]);
return (
<label>
{name}:
<select value={value} onChange={onChangeHandler}>
{
options.map((item) => {
const {id} = item;
return <option value={id}>{id}</option>;
})
}
</select>
</label>
);
}
function Field() {
const [catIndex, setCatIndex] = useState(0);
const [subIndex, setSubIndex] = useState(0);
return (
<div>
<Select name='Category' options={categories} onIndex={setCatIndex}/>
<Select name='Sub Category' options={categories[catIndex].data} onIndex={setSubIndex}/>
<Select name='Option' options={categories[catIndex].data[subIndex].data}/>
</div>
);
}
function App() {
const [fields, setFields] = useState(() => [<Field />]);
const addField = useCallback(() => {
setFields((prevFields) => {
const index = prevFields.lenght;
return [...prevFields, <Field key={index} />];
});
}, []);
return (
<Fragment>
<form>
{fields}
</form>
<button type='button' onClick={addField}>Add Field</button>
</Fragment>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
form {
display: flex;
flex-direction: column;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id='root'></div>
Please let me know if you have any doubt.

Related

Disable dependent dropdown option in Reactjs

I am making a simple react application where there are dropdowns in which one dependent on another.
-> Here dropdown 1 has the value as type of game like Indoor and Outdoor.
-> Here dropdown 2 has the value as type of sport like Chess , Tennis and Football .
Requirement:
The following different use cases needs to be covered,
Scenarios:
-> User selects Indoor from dropdown 1, then in dropdown 2 only the value of Chess needs to be enabled and others needs to be
disabled.
-> User selects Outdoor from dropdown 1, then in dropdown 2 only the value of Tennis and Football needs to be enabled and option Chess
needs to be disabled.
Vice versa:
-> User selects Chess from dropdown 2, then in dropdown 1 only the value of Indoor needs to be enabled and others needs to be
disabled.
-> User selects Tennis or Football from dropdown 2, then in dropdown 1 only the value of Outdoor needs to be enabled and others needs to be disabled.
Here we provide option of allowClear so that user can reset their selection in any select box selection (the close icon) and do the above mentioned scenario in any way like selecting option from first dropdown or in second dropdown based on which the another dropdown make the option enable or disable.
Right now I have a data like this and open for modification to achieve the expected result.
const data = {
games: {
type: [
{ id: 1, value: "Indoor", sportId: [2] },
{ id: 2, value: "Outdoor", sportId: [1, 3] }
],
sport: [
{ id: 1, value: "Tennis", typeId: [2] },
{ id: 2, value: "Chess", typeId: [1] },
{ id: 3, value: "Football", typeId: [2] }
]
}
}
The property names may vary so I cannot rely on the hard coded/static name inside code like data.games.type or data.games.sport.
And hence I tried with dynamic approach like,
{Object.entries(data.games).map((item, index) => {
return (
<div className="wrapper" key={index}>
<h4> {item[0]} </h4>
<Select
defaultValue="selectType"
onChange={handleChange}
allowClear
>
<Option value="selectType"> Select {item[0]} </Option>
{item[1].map((option, j) => (
<Option key={j} value={option.value}>
{option.value}
</Option>
))}
</Select>
<br />
</div>
);
})}
Reactjs sandbox:
Note: The options needs to be disabled (only) and should not be removed from select box as user can clear any select box selection and
select value from any of the dropdown.
Pure Javascript Approach: (Ignore reset of dropdown in this JS example which handled in reactjs with help of clear icon (close icon))
Also here is the Pure JS (working) way of approach tried with hard coded select boxes with id for each element respectively and also with some repetition of code in each addEventListener,
const data = {
games: {
type: [
{ id: 1, value: "Indoor", sportId: [2] },
{ id: 2, value: "Outdoor", sportId: [1, 3] }
],
sport: [
{ id: 1, value: "Tennis", typeId: [2] },
{ id: 2, value: "Chess", typeId: [1] },
{ id: 3, value: "Football", typeId: [2] }
]
}
}
const typeSelect = document.getElementById('type')
const sportSelect = document.getElementById('sport')
const createSelect = (values, select) => {
values.forEach(t => {
let opt = document.createElement('option')
opt.value = t.id
opt.text = t.value
select.append(opt)
})
}
createSelect(data.games.type, typeSelect)
createSelect(data.games.sport, sportSelect)
typeSelect.addEventListener('change', (e) => {
const val = e.target.value
const type = data.games.type.find(t => t.id == val)
Array.from(sportSelect.querySelectorAll('option')).forEach(o => o.disabled = true)
type.sportId.forEach(sId =>
sportSelect.querySelector(`option[value="${sId}"]`).disabled = false)
})
sportSelect.addEventListener('change', (e) => {
const val = e.target.value
const sport = data.games.sport.find(s => s.id == val)
Array.from(typeSelect.querySelectorAll('option')).forEach(o => o.disabled = true)
sport.typeId.forEach(sId =>
typeSelect.querySelector(`option[value="${sport.typeId}"]`).disabled = false)
})
<select id="type"></select>
<select id="sport"></select>
Could you please kindly help me to achieve the result of disabling the respective options from respective select box based on the conditions mentioned in the above mentioned scenario's in pure reactjs way?
For the comment given by #Andy, there is a reset option available in the select I am using, with close icon, so using that user can clear the select box and select the other dropdown option. This option is provided under allowClear in the antd select . Kindly please see the select box that I have in the above codesandbox, it has clear icon in the last.
Here's what I have as a working solution with my understanding of your question. You want dynamic options that can easily validate against other dynamic options. It's about the best I could come up with that wasn't completely unmaintainable. It's about 98% dynamic but for the validation purposes some properties do need to be defined.
Example:
Setup the interfaces and types
interface IState { // <-- need to be known
type: number;
sport: number;
}
interface IOption {
id: number;
value: string;
valid: Record<keyof IState, number[]>;
}
type Valid = "sport" & "type"; // <-- this needs to be known
interface Data {
games: {
[key: string]: Array<Record<Valid, IOption[]>>;
};
}
Data
const data: Data = {
games: {
type: [
{ id: 1, value: "Indoor", valid: { sport: [2] } },
{ id: 2, value: "Outdoor", valid: { sport: [1, 3] } }
],
sport: [
{ id: 1, value: "Tennis", valid: { type: [2] } },
{ id: 2, value: "Chess", valid: { type: [1] } },
{ id: 3, value: "Football", valid: { type: [2] } }
],
}
};
Create component state to hold the selected option values. These should match the known selection types in the data. The idea here is that we are converting the select inputs to now be controlled inputs so we can validate options against selected state.
export default function App() {
const [state, setState] = React.useState<IState>({
type: -1,
sport: -1,
category: -1
});
const changeHandler = (key: keyof IState) => (value: number) => {
setState((state) => ({
...state,
[key]: value
}));
};
This is the meat of the addition. Validates options against currently selected state values according to the data configuration. Looks through each option's valid object and compares against current selected state. Returns if a current option is a valid selectable option or not.
const isValid = (key: keyof IState, option: IOption) => {
const { valid } = option;
return (Object.entries(valid) as [[keyof IState, number[]]]).every(
([validKey, validValues]) => {
const selectedValue = state[validKey];
if (!selectedValue || selectedValue === -1) return true;
return validValues.includes(state[validKey]);
}
);
};
return (
<>
<br />
{(Object.entries(data.games) as [[keyof IState, IOption[]]]).map(
([key, options]) => {
return (
<div className="wrapper" key={key}>
<h4>{key}</h4>
<Select
value={state[key] || -1}
onChange={changeHandler(key)}
allowClear
>
<Option disabled value={-1}>
Select {key}
</Option>
{options.map((option) => (
<Option
key={option.id}
value={option.id}
disabled={!isValid(key, option)} // if not valid, then disable
>
{option.value}
</Option>
))}
</Select>
<br />
</div>
);
}
)}
</>
);
}
https://codesandbox.io/s/react-typescript-forked-gt7gvy?file=/src/App.tsx
I added keeping state of chosen values in each select and conditional disabling of options in the select.
import "antd/dist/antd.min.css";
import { Select } from "antd";
import * as React from "react";
import "./styles.css";
const { Option } = Select;
const data = {
games: {
type: [
{ id: 1, value: "Indoor", sportId: [2] },
{ id: 2, value: "Outdoor", sportId: [1, 3] }
],
sport: [
{ id: 1, value: "Tennis", typeId: [2] },
{ id: 2, value: "Chess", typeId: [1] },
{ id: 3, value: "Football", typeId: [2] }
]
}
};
export default function App() {
const [category, setCategory] = React.useState(null);
const [sport, setSport] = React.useState(null);
const handleChange = (value: any, index: number) => {
console.log(value);
const valueToSet = value.startsWith("select") ? null : value;
if (index === 0) {
setCategory(valueToSet);
} else if (index === 1) {
setSport(valueToSet);
}
};
return (
<>
<br />
{Object.entries(data.games).map((item, index) => {
return (
<div className="wrapper" key={index}>
<h4> {item[0]} </h4>
<Select
defaultValue="selectType"
onChange={(value) => handleChange(value, index)}
allowClear
>
<Option value="selectType"> Select {item[0]} </Option>
{item[1].map((option, j) => (
<Option
key={j}
value={option.value}
disabled={
'typeId' in option && // index === 1 or just belong to sports
category &&
data.games.type.find((x) => x.value === category)?.id !==
option.typeId[0]
}
>
{option.value}
</Option>
))}
</Select>
<br />
</div>
);
})}
</>
);
}

Select value doesnt change the first time I trigger onChange event when using setSate React

I have a set of select menus and I am trying to change a value when I select an option using onChange={updateValue} event. When I first select an option, the value is not being updated in the select menu.
It only changes the second time I try to choose an option. Not sure what I am doing wrong.
Edit: I did some more research (OnChange event using React JS for drop down) and I believe I need the value of the select to be updated as well, using setState. I cant figure out how to do it without having a variable for each value and set the state again.
let selectMenus = [
{
id: 'id1',
name: 'name1',
label: 'label1',
value: '0',
options: [
{
text: 'All ages',
value: '0',
},
{
text: '35 - 37 yrs',
value: '1',
},
],
buttonLabel: 'Refresh',
},
{
id: 'id2',
name: 'name2',
label: 'label2',
value: '1',
options: [
{
text: 'All ages',
value: '0',
},
{
text: '45 - 50 yrs',
value: '1',
},
],
buttonLabel: 'Refresh',
},
];
const [url, setUrl] = useState('http://localhost:5000/selectDropdowns1');
const updateValue = () => {
setUrl('http://localhost:5000/selectDropdowns2');
};
<form>
{selectMenus.map((select) => (
<div key={select.id} className='select-container'>
<label htmlFor={select.id}>{select.label}</label>
<select id={select.id} name={select.name} value={select.value} onChange={updateValue}>
{select.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{select.buttonLabel}</button>
</div>
))}
</form>;
The problem is that when you provide onChange prop to select component it become a controlled component.
For more information: React Docs - Forms #controlled components
When you dealing with controlled components you must provide a value to it and when onChange triggerd it should update that value to work properly. Since you did not provide the full code, I imagine you have an array of select menus and options attached to it.
So in this case every select component should have own onChange method and own value to work properly. To achive this we should create another component for only Select Options. Like this;
function SelectComponent({ optionList, onSelected }) {
const [value, setValue] = useState();
const updateValue = ({ target }) => {
setValue(target.value);
if (onSelected) onSelected(target.value);
};
return (
<>
<label htmlFor={optionList.id}>{optionList.label}</label>
<select
id={optionList.id}
name={optionList.name}
value={value}
onChange={updateValue}
>
{optionList.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{optionList.buttonLabel}</button>
</>
);
}
This component accepts to props; optionList and onSelected
optionList is the list of options to render
onSelected is a method that we call when user select and option
On main component, we should change the select section with our select component with props optionList and onSelected
return (
<div>
{selectMenus.map((select) => (
<div key={select.id} className="select-container">
<SelectComponent optionList={select} onSelected={updateValue} />
</div>
))}
</div>
);
So overall code is like this:
import { useState } from "react";
import { v4 as uuid } from "uuid";
export default function App() {
const [url, setUrl] = useState();
const updateValue = (value) => {
setUrl(value);
};
const selectMenus = [
{
id: 1,
label: "Menu 1",
name: "menu1",
buttonLabel: "Menu 1",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
},
{
id: 2,
label: "Menu 2",
name: "menu2",
buttonLabel: "Menu 2",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
},
{
id: 3,
label: "Menu 3",
name: "menu3",
buttonLabel: "Menu 3",
options: [
{
text: "option 1",
value: "option1"
},
{
text: "option 2",
value: "option2"
},
{
text: "option 3",
value: "option3"
}
]
}
];
return (
<div className="App">
<h1>URL Value: {url}</h1>
{selectMenus.map((select) => (
<div key={select.id} className="select-container">
<SelectComponent optionList={select} onSelected={updateValue} />
</div>
))}
</div>
);
}
function SelectComponent({ optionList, onSelected }) {
const [value, setValue] = useState();
const updateValue = ({ target }) => {
setValue(target.value);
if (onSelected) onSelected(target.value);
};
return (
<>
<label htmlFor={optionList.id}>{optionList.label}</label>
<select
id={optionList.id}
name={optionList.name}
value={value}
onChange={updateValue}
>
{optionList.options.map((option) => (
<option value={option.value} key={uuid()}>
{option.text}
</option>
))}
</select>
<button>{optionList.buttonLabel}</button>
</>
);
}
Working example is overhere codesandbox

React mapping menu from JSON

I have a scrolling menu items, and the titles of each item is hardcoded into a const, along side with the id
const list = [
{ name: "category1", id: 0 },
{ name: "category2", id: 1 },
{ name: "category3", id: 2 },
{ name: "category4", id: 3 },
{ name: "category5", id: 4 },
{ name: "category6", id: 5 },
{ name: "category7", id: 6 },
{ name: "category8", id: 7 }
];
I have a json file that contains the category name for each child:
{
"results": [
{
"category": "category1",
"name": {
"title": "mr",
"first": "ernesto",
"last": "roman"
},
"email": "ernesto.roman#example.com",
"id": {
"name": "DNI",
"value": "73164596-W"
},
"picture": {
"large": "https://randomuser.me/api/portraits/men/73.jpg",
"medium": "https://randomuser.me/api/portraits/med/men/73.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/73.jpg"
}
},
{
"category": "category2",
"name": {
"title": "mr",
"first": "adalbert",
"last": "bausch"
},
"email": "adalbert.bausch#example.com",
"id": {
"name": "",
"value": null
} etc....
I want to show these categories "category": "category1", as the titles of my menu, I now that I need to start stateless and add them from the JSON, the fetching part from the JSON is done locally in componentDidMount, but I am not sure how can I map them into appearing as menu names to make the menu dynamic, I basically want the same output but from the json not hardcoded. here is a sandbox snippet, would appreciate the help.
https://codesandbox.io/s/2prw4j729p?fontsize=14&moduleview=1
Just convert the JSON output to an object like list with a map function from the results and then set is as MenuItems on the state, which is what you pass to the function on render(). Like that.
import React, { Component } from "react";
import ScrollMenu from "react-horizontal-scrolling-menu";
import "./menu.css";
// One item component
// selected prop will be passed
const MenuItem = ({ text, selected }) => {
return (
<div>
<div className="menu-item">{text}</div>
</div>
);
};
// All items component
// Important! add unique key
export const Menu = list =>
list.map(el => {
const { name, id } = el;
return <MenuItem text={name} key={id} />;
});
const Arrow = ({ text, className }) => {
return <div className={className}>{text}</div>;
};
export class Menucat extends Component {
state = {
selected: "0",
MenuItems: []
};
componentDidMount() {
fetch("menu.json")
.then(res => res.json())
.then(result => {
const items = result.results.map((el, idx) => {
return { name: el.category, id: idx };
});
this.setState({
isLoaded: true,
MenuItems: items
});
});
}
render() {
const { selected, MenuItems } = this.state;
// Create menu from items
const menu = Menu(MenuItems, selected);
return (
<div className="App">
<ScrollMenu
data={menu}
selected={selected}
onSelect={this.onSelect}
alignCenter={true}
tabindex="0"
/>
</div>
);
}
}
export default Menucat;
Cheers!
Looks like you don't have to hard code your category list at all. In your componentDidMount() fetch the json and group the results into separate categories like this:
const json = {
"results": [
{
category: "category1",
name: "Fred"
},
{
category: "category1",
name: "Teddy"
},
{
category: "category2",
name: "Gilbert"
},
{
category: "category3",
name: "Foxy"
},
]
}
const grouped = json.results.reduce((acc, cur) => {
if (!acc.hasOwnProperty(cur.category)) {
acc[cur.category] = []
}
acc[cur.category].push(cur)
return acc;
}, { })
// parent object now has 3 properties, namely category1, category2 and category3
console.log(JSON.stringify(grouped, null, 4))
// each of these properties is an array of bjects of same category
console.log(JSON.stringify(grouped.category1, null, 4))
console.log(JSON.stringify(grouped.category2, null, 4))
console.log(JSON.stringify(grouped.category3, null, 4))
Note that this json has 4 objects in result array, 2 of cat1, and 1 of cat 2 and cat3. You can run this code in a separate file to see how it works. Ofcourse you will be fetching the json object from server. I just set it for demonstration.
Then set teh state:
this.setState({ grouped })
Then in render() you only show the categories that have items like:
const menuBarButtons = Object.keys(this.state.grouped).map((category) => {
/* your jsx here */
return <MenuItem text={category} key={category} onClick={this.onClick} blah={blah}/>
/* or something , it's up to you */
})
I'm assuming you're showing the items based on the currently selected category this.state.selected. So after you have rendered your menu, you would do something like:
const selectedCatItems = this.state.grouped[this.state.selected].map((item) => {
return <YourItem name={item.name} key={item.id} blah={blah} />
})
Then render it:
return (
<div className="app">
<MenuBar blah={blah}>
{menuBarButtons}
</Menubar>
<div for your item showing area>
{selectedCatItems}
</div>
</div>
)
Also, don't forget to change your onClick() so that it sets this.state.selected state properly. I believe you can figure that out yourself.
Hope it helps.
PS: I didn't write a whole copy/paste solution to your problem simply because I'm reluctant to read and understand your UI details and the whole component to component data passing details..

Map a filtered array in React

I made a component containing two dropdown lists. The options in the second dropdown menu is supposed to be filtered depending on the selected option from the first dropdown menu.
Now, I want to map a filtered array that is stored in a const similary to the way i map options1:
render() {
const options1 = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'}
];
const options2 = [
{value: 'one-a', label: 'One A', link: 'one'},
{value: 'one-b', label: 'One B', link: 'one'},
{value: 'two-a', label: 'Two A', link: 'two'},
{value: 'two-b', label: 'Two B', link: 'two'}
];
const filteredOptions = options2.filter(o => o.link === this.state.selectedOption.value);
return (
<div style={style}>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => <option>{tag.value}</option>)}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => <option>{tag.value}</option>)}
</select>
</div>
</div>
)
}
The first mapping of options1 works just fine. However, my select tag gets rendered empty for the mapping of filteredOptions.
I have no idea why it won't work. Anyone happen to have an idea?
Full code: https://www.codepile.net/pile/evNqergA
Here is a working example for what you're trying to do.
import React, { Component } from "react";
const options1 = [
{ value: "one", label: "One" },
{ value: "two", label: "Two" }
];
const options2 = [
{ value: "one-a", label: "One A", link: "one" },
{ value: "one-b", label: "One B", link: "one" },
{ value: "two-a", label: "Two A", link: "two" },
{ value: "two-b", label: "Two B", link: "two" }
];
export default class SelectsComponent extends Component {
handleChange1 = e => {
console.log(e);
this.setState({
selectedOption: { value: e.target.value }
});
};
handleChange2 = e => {
this.setState({
selectedOption2: { value: e.target.value }
});
};
constructor(props) {
super(props);
this.state = {
selectedOption: { value: "one" },
selectedOption2: { value: "one-a" }
};
}
render() {
const filteredOptions = options2.filter(
o => o.link === this.state.selectedOption.value
);
return (
<div>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
</div>
);
}
}
In your scenario filteredOptions would be an empty Array.
The check for o.link === this.state.selectedOption.value is doing something wrong.
Check the value of this.state.selectedOption.value, this is not set correctly.
The best way to do this wouldn't be inside of the render method.
1) move your arrays into state or other instance members
2) make sure to only trigger the sorting once
this.setState(lastState => ({
...lastState,
options2: lastState.options2.filter(yourFilterFn)
}))
3) map the filtered array into JSX inside of your render method
Side-note: this uses immutable setState (which I gather is important given you create a new filtered array from the options2 in your example). If you want to follow an even more functional pattern, you can do the filtering inside of your render method (although I don't recommend it). If you decided to filter inside of your render method, consider using a memoization technique from React 16.7 (which is currently in Alpha).

How to get a certain data in the second autocomplete input that depend on what typed in the first input in React.js?

Okay, so I don't know how to properly express my simple problem because of how simple it is, I guess.
Basically, I have an autocomplete done by me in my React project.. I have two inputs "Country" and "City". When I type a country my autocomplete works great giving me suggestions but now I have to make the same for my second input so it would give me a list of cities that depends on which country is typed in the "Country" input...
"United Kingdom" => "London, Birmingham, Bighton etc."
How can I do that? Thank you!
P.S. I already have all the lists of countries and cities, I just don't know how to make the second input to depend on an information in the first one.
Code here
Autocomplete.jsx
https://github.com/lembas-cracker/Weather-app/blob/master/src/Autocomplete.jsx
Form.jsx
https://github.com/lembas-cracker/Weather-app/blob/master/src/Form.jsx
P.S. I already have all the lists of countries and cities, I just don't know how to make the second input to depend on an information in the first one.
If you know which country the city belongs to (perhaps via a key in the city object), you could run a simple filter function to remove any cities that don't belong to that country.
this.state = {
selectedCountry: 'London',
};
const cities = [
{ name: "Toronto", country: "Canada" },
{ name: "London", country: "United Kingdom" }
];
const filteredCities = cities.filter(city => {
return city.country !== this.state.selectedCountry;
});
On your city input field make sure to create an onBlur function to will run the filter on your cities list once the user leaves that input field.
Made a quick example. Did you mean smth like this? Since you haven't provided any part of your source code, I used plain HTML select for the demo.
https://jsfiddle.net/arfeo/n5u2wwjg/204186/
class App extends React.Component {
constructor() {
super();
this.state = {
countryId: 1,
};
}
onCountryChange(countryId) {
this.setState({ countryId: parseInt(countryId) });
}
render() {
return (
<div>
<Input
key="countriesInput"
type="countries"
countryId={this.state.countryId}
onChange={(countryId) => this.onCountryChange(countryId)}
/>
<Input
key="citiesInput"
type="cities"
countryId={this.state.countryId}
/>
</div>
);
}
}
class Input extends React.Component {
constructor() {
super();
this.selectRef = null;
}
renderOptions() {
const countries = [
{
id: 1,
name: 'England',
},
{
id: 2,
name: 'Germany',
},
{
id: 3,
name: 'France',
},
];
const cities = [
{
countryId: 1,
cities: [
{
id: 1,
name: 'London',
},
{
id: 2,
name: 'Liverpool',
},
{
id: 3,
name: 'Salisbury'
}
],
},
{
countryId: 2,
cities: [
{
id: 4,
name: 'Berlin',
},
{
id: 5,
name: 'Frankfurt',
},
],
},
{
countryId: 3,
cities: [
{
id: 6,
name: 'Paris',
},
],
},
];
switch (this.props.type) {
case 'countries': {
return countries.map((country) => (
<option
key={country.id.toString()}
value={country.id}
>
{country.name}
</option>
));
}
case 'cities': {
const citiesMap = cities.filter((city) => city.countryId === this.props.countryId);
if (citiesMap && citiesMap[0]) {
const citiesList = citiesMap[0].cities;
if (citiesList) {
return citiesList.map((city) => (
<option
key={city.id.toString()}
value={city.id}
>
{city.name}
</option>
));
}
}
return null;
}
default: return null;
}
}
render() {
return (
<select name={this.props.type} ref={(ref) => this.selectRef = ref} onChange={() => this.props.onChange(this.selectRef.value)}>
{this.renderOptions()}
</select>
);
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
UPDATE
Make your Form component stateful.
Add a state property for countries in Form (let it be countryId).
Pass this property as a prop into the second Autocomplete component.
When the first Autocomplete changes, change the countryId of the Form.
I've done something similar which may help you.
The Object.keys(instutiontypes) you could use to have an array of countries, instead. Then inside of those values, you can have an array of objects. You could have the cities here, e.g. {value: "Manchester", "label: Manchester", phoneExt: "0114"}
const instutiontypes = {
Kindergarten: [
{ value: "PreK", label: "PreK" },
{ value: "K1", label: "K1" },
{ value: "K2", label: "K2" },
{ value: "K3", label: "K3" },
],
"Primary School": [
{ value: "Grade 1", label: "Grade 1" },
{ value: "Grade 2", label: "Grade 2" },
{ value: "Grade 3", label: "Grade 3" },
{ value: "Grade 4", label: "Grade 4" },
{ value: "Grade 5", label: "Grade 5" },
{ value: "Grade 6", label: "Grade 6" },
],
}
To have the options in my input, I use Object.keys(instutiontypes) to get ['Kindergarten','Primary School']
Then, to get the array of ages to give to my secondary dropdown, I have written this code:
const types = ['Selection1', 'Selection2']
const agesList = [];
for (let i = 0; i < types.length; i++) {
Object.values(institutionTypes[types[i]]).map(({ label }) =>
agesList.push(label)
);
}
This way, the ages dropdown list is dependent on the values passed to institutionTypes.
I'm using mui's <Autocomplete /> components to make them be search dropdowns, with the prop options for the arrays.

Categories