I'm working on a Car website and I added a simple json file with data of each car brand and their models.
[
{"id": "1", "brand": "Seat", "models": ["Alhambra", "Altea", "Altea XL", "Arosa", "Cordoba", "Cordoba Vario", "Exeo", "Ibiza", "Ibiza ST", "Exeo ST", "Leon", "Leon ST", "Inca", "Mii", "Toledo"]},
{"id": "2", "brand": "Renault", "models": ["Captur", "Clio", "Clio Grandtour", "Espace", "Express", "Fluence", "Grand Espace", "Grand Modus", "Grand Scenic", "Kadjar", "Kangoo", "Kangoo Express", "ZoƩ"]},
{"id": "3", "brand": "Audi", "models": ["A1", "A2", "A3", "A4", "A4 Allroad", "A4 Avant", "A5", "A6", "A7", "A8", "A8 Long", "Q3", "Q5", "Q7", "R8", "RS4 Cabriolet", "RS4/RS4 Avant", "RS5", "RS6 Avant", "RS7"]}
]
I added a dropdown menu where when I select a specific brand it will show me only the models of that brand. For example, if I Pick Audi I will only get "A1, A2, A3, A4, etc.". However, right now when I pick my car brand I get all those brand models in one line. How can I make it so each model is on a separate line instead of all clustered in one?
Here are pictures of how it looks now:
Image 1
Image 2
My code
import React, { Component } from 'react';
import ReactDOM from "react-dom";
export default class simpleData extends Component {
constructor(props) {
super(props);
this.state = {
cars: [],
models: ""
};
this.handleSubmitCar = this.handleSubmitCar.bind(this);
}
handleSubmitCar(event) {
alert("Your selected value is: " + this.state.models);
event.preventDefault();
}
handleChangeCar = event => {
this.setState({ models: event.target.value });
};
getUnique(arr, comp) {
const unique = arr
//store the comparison values in array
.map(e => e[comp])
// store the keys of the unique objects
.map((e, i, final) => final.indexOf(e) === i && i)
// eliminate the dead keys & store unique objects
.filter(e => arr[e])
.map(e => arr[e]);
return unique;
}
componentDidMount() {
const cars = require("./cars.json");
this.setState({ cars: cars });
}
render() {
const uniqueCar = this.getUnique(this.state.cars, "brand");
const cars = this.state.cars;
const models = this.state.models;
const filterDropdown = cars.filter(function(result) {
return result.brand === models;
});
return (
<div>
<form onSubmit={this.handleSubmitCar}>
<br />
<br />
<label>
Car brand
<select
value={this.state.models}
onChange={this.handleChangeCar}
>
{uniqueCar.map(models => (
<option key={models.id} value={models.brand}>
{models.brand}
</option>
))}
</select>
</label>
<input type="submit" value="Submit" /><br/>
Car Model
<select>
{filterDropdown.map(models => (
<option key={models.id}>
{models.models}
</option>
))}
</select>
</form>
</div>
);
}
}
In your map function
{filterDropdown.map(models => (
<option key={models.id}>
{models.models}
</option>
))}
You need a way to map the models that the user selected, the selected car's models, which is models.models
Instead what's happening is you're filtering the selected car in the original json, and returning any value that matches the selected car, that returns an array, which stores only 1 object, the selected car itself, and you're mapping over that array.
that means in your map function, you can access the id, brand, model properties, but because you can only loop once, you can't loop over the models themselves.
What you need to do, is have a seperate state, for the selected models. and map over that instead. the selected models value would change whenever a car gets reselected
handleChangeCar = event => {
// Returns an array with objects that pass the filter condition,
// meaning it will return an array with only one object
const returnSelectedBrand = this.state.cars.filter(function(result) {
return result.brand === event.target.value;
});
this.setState({
selectedBrand: event.target.value,
selectedBrandModels: returnSelectedBrand[0].models
});
};
and map over that instead.
<select>
{
// if selectedBrandModel is truthy, map it
selectedBrandModels && selectedBrandModels.map((model, i) => {
return (
// you might need to add an id for each model, we're using
// the loop index for now
<option key={i}>
{model}
</option>
)
})
}
</select>
Here's the full working code
import React, { Component } from 'react';
export default class simpleData extends Component {
constructor(props) {
super(props);
this.state = {
cars: [],
// Renamed to 'selectedBrand' instead of model
selectedBrand: "",
// added 'selectedBrandModels to keep track of the selected brand's models
selectedBrandModels: []
};
this.handleSubmitCar = this.handleSubmitCar.bind(this);
}
handleSubmitCar(event) {
alert("Your selected value is: " + this.state.models);
event.preventDefault();
}
handleChangeCar = event => {
// Returns an array with objects that pass the filter condition, meaning it
// will return an array with only one object
const returnSelectedBrand = this.state.cars.filter(function(result) {
return result.brand === event.target.value;
});
this.setState({
selectedBrand: event.target.value,
selectedBrandModels: returnSelectedBrand[0].models
});
};
getUnique(arr, comp) {
const unique = arr
//store the comparison values in array
.map(e => e[comp])
// store the keys of the unique objects
.map((e, i, final) => final.indexOf(e) === i && i)
// eliminate the dead keys & store unique objects
.filter(e => arr[e])
.map(e => arr[e]);
return unique;
}
componentDidMount() {
const cars = require("./cars.json");
this.setState({ cars: cars });
}
render() {
const uniqueCar = this.getUnique(this.state.cars, "brand");
// used object deconstructoring for clearer syntax
const { selectedBrandModels } = this.state;
return (
<div>
<form onSubmit={this.handleSubmitCar}>
<br />
<br />
<label>
Car brand
<select
value={this.state.models}
onChange={this.handleChangeCar}
>
{uniqueCar.map(models => (
<option key={models.id} value={models.brand}>
{models.brand}
</option>
))}
</select>
</label>
<input type="submit" value="Submit" /><br/>
Car Model
<select>
{
// if selectedBrandModel is truthy, map it
selectedBrandModels && selectedBrandModels.map((model, i) => {
return (
// you might need to add an id for each model, we're using
// the loop index for now
<option key={i}>
{model}
</option>
)
})
}
</select>
</form>
</div>
);
}
}
EDIT: I realised you could also just map the selected car object directly instead of making a separate state for its models, something like:
selectedCar &&
selectedCar.models.map((model, i) => {
// or selectedCar[0].models.map if its an array
return (
// you might need to add an id for each model, we're using
// the loop index for now
<option key={i}>
{model}
</option>
)
})
}
Related
Trying to create a filter that'll return items depending on some property(inStock in this case)
I'm not getting any items when trying to set the filter to filter only items who have true in their inStock property , at all.
when console.log availability - i'm getting the proper boolean i set on the filter (true/false)
and when i console.log the the inStock items they all return true (which is fine, it's what i set).
I have tried setting the value in the filter component as a string- still didn't work.
would much appreciate your help with this one
toy.service.js
function query(filterBy) {
return storageService.query(STORAGE_KEY).then((toys) => _filteredToys(toys, filterBy));
}
function _filteredToys(toys, filterBy) {
const { txt, availability } = filterBy;
const filteredtoys = toys.filter((toy) => {
console.log(availability);
console.log(toy.inStock);
return toy.inStock === availability;
});
return Promise.resolve(filteredtoys);
}
the toy-filter component
import React from 'react';
export class ToyFilter extends React.Component {
state = {
filterBy: {
availability: '',
txt: '',
},
};
handleChange = ({ target }) => {
const { name, value } = target;
this.setState(
(prevState) => ({ filterBy: { ...prevState.filterBy, [name]: value } }),
() => {
this.props.onSetFilter(this.state.filterBy);
}
);
};
render() {
const { txt } = this.state.filterBy;
return (
<section>
<select name='availability' onChange={this.handleChange}>
<option value='' key='filter-all'>
All
</option>
<option value={true} key='filter-in-stock'>
in stock
</option>
<option value={false} key='filter-sold-out'>
sold out
</option>
</select>
<input type='text' name='txt' value={txt} onChange={this.handleChange} placeholder='Search toy...' />
</section>
);
}
}
The way in which you are setting availability in the state, it will always be a string. So true in the console output is not a boolean but a string. Please check the data type of availability using typeof availability.
My guess is comparison return toy.inStock === availability is failing because inStock is bool while availability is string.
I have created a list of objects and each list item has input within:
{flowers_data?.map((flower) => {
return (
<>
<div className={classes.Nested_Flower_Container} key={flower.id}>
<div className={classes.Nested_Flower_Name}>
{flower.name}
</div>
<div className={classes.Nested_Flower_Input} style={{ marginRight: '0.2em' }}>
<TextField
id="Amount"
label="Amount"
variant="outlined"
size="small"
type="number"
onChange={(e) => {
setAmount(e.target.value);
handleAddList(e.target.value, flower);
}}
className={classes_2.root}
/>
</div>
</div>
</>)
})}
How can I add an object that has a value in input to an array? I tried to do this using a function that I created, but each time I change one element's target.value and move on to the next item to change its input value, there is only one element in the array with the latest target.value. And after modifying the inputs, when I try to output the values outside that function with e.g. a button, the add_flowers_tab array is empty.
handleAddList function:
let temp_flower: Flower;
let add_flowers_tab: Flower[] = [];
const handleAddList = (targetValue: string, flower: Flower) => {
temp_flower = {
"id": flower.id,
"name": flower.name,
"price": flower.price,
"amount": Number(targetValue),
"creation_date": flower.creation_date
}
if (targetValue === '') {
/* Delete flower when input is empty */
add_flowers_tab.forEach(tabFlower => {
if (tabFlower.id === temp_flower.id) {
const indexOfDelete = add_flowers_tab.indexOf(tabFlower);
add_flowers_tab.splice(indexOfDelete, 1);
}
})
}
if (targetValue !== '') {
/* Add flower to tab when input has value */
if (add_flowers_tab.length > 0) {
/* When input changes, delete flower with old input value and add the new one */
add_flowers_tab.forEach(tabFlower => {
if (tabFlower.id === temp_flower.id) {
const indexOfDelete = add_flowers_tab.indexOf(tabFlower);
add_flowers_tab.splice(indexOfDelete, 1);
add_flowers_tab.push(temp_flower);
}
})
}
else {
/* Add new flower as a first element in the array */
add_flowers_tab.push(temp_flower);
}
/*
Displays an array with only the most recently added temp_flower, even though
several inputs in list have values
*/
console.log(add_flowers_tab);
}
}
Here is the minimum solution for generating a list on Inputs that each update a single element in a list on state.
export const ManyInputs = ({inputs}) => {
const [list, setList] = useState(inputs);
const setIndividual = (value, id) =>
// Update a single field in the object in the list that matches the given id
setList(list.map(l => l.id === id ? ({...l, value}) : l));
return list.map(i =>
// Send the id back to help match the object
<TextField onChange={(e) => setIndividual(e.target.value, i.id)} />
);
};
You can have one React state, which has an array of objects that you enter through your "mapped" component.
const [tmpAmount, setTmpAmount] = React.useState<{flower:Flower,amount:number}[]>([])
// fill the temporary state with generated flowers only once
React.useEffect(()=>{
setTmpAmount(flowers_data.map((flwr) =>{flower:flwr, amount:0}))
},[])
and replace the onChange()
onChange={(e) => {
// find existing flower to edit the amount of it
let indexToChange = flowers_data.findIndex((element) => element.id === flower.id)
// update and replace the array variable with updated object
setAmount2((prev) =>
[
...prev.slice(0, indexToChange),
{
...prev[indexToChange],
amount: e.target.value,
},
...prev.slice(indexToChange + 1),
]);
}
You can also check if the array changes and print it:
React.useEffect(()=>console.log(tmpAmount),[tmpAmount])
I want this select to have an object value, this works but I can't show the title of the selected object, does anyone know why?
my code is:
const [categoria, setCategoria] = useState("");
const litaCategorias = [
{
"id": 2,
"titulo": "test",
"descripcion": "descripcion",
}];
const handleChangeCategoria = (value) => {
setCategoria(value)
console.log(value)
}
<Select
fullWidth
variant="outlined"
value={categoria.titulo}
onChange={e => handleChangeCategoria(e.target.value)}
labelWidth={0}
placeholder={"Seleccione un destino"}
>
{litaCategorias.map(categoria => {
return (
<option
value={categoria}
key={categoria.titulo}
>
{categoria.titulo}
</option>
);
})}
</Select>
I have double checked the Select component in material-ui. It provides props api value with description:
If the value is an object it must have reference equality with the option in order to be selected. If the value is not an object, the string representation must match with the string representation of the option in order to be selected.
You you should memorize you array litaCategorias. to make reference equality.
Should use useMemo to memorized array.
const litaCategorias = useMemo(() => [ { "id": 2, "titulo": "test", "descripcion": "descripcion", }], []);
I think it's because you're setting the param in your callback in the .map function to categoria which is defined in the state and is initialized to an empty string. This line here - {litaCategorias.map(categoria => {. The option value in the select dropdown is not getting the correct object because of this.
Try something like this instead:
import React, { useState } from "react";
export default function App() {
const [categoria, setCategoria] = useState("");
const litaCategorias = [
{ id: 1, titulo: "test", descripcion: "descripcion" },
{ id: 2, titulo: "test2", descripcion: "descripcion2" }
];
const handleChangeCategoria = (value) => {
setCategoria(value);
console.log(value);
};
return (
<select
value={categoria.titulo}
onChange={(e) => handleChangeCategoria(e.target.value)}
>
{litaCategorias.map((cat) => {
return (
<option value={cat.titulo} key={cat.id}>
{cat.titulo}
</option>
);
})}
</select>
);
}
https://codesandbox.io/s/wild-mountain-0r4p6?file=/src/App.js
Hi I am new to react and am trying to use onSelect to return on array of items that are associated with that name. I am using the dot filter method to filter an array so that only items with the same key as the name that is selected appear. However my array returns empty.
class HorizantScroller extends React.Component {
state = {
selected: 'Brands',
statelist: [
{name: "Brands",
items: ["1", "2", "3"]
},
{name: "Films",
items: ["f1", "f2", "f3"]
},
{name: "Holiday Destination",
items: ["f1", "f2", "f3"]
}
]
};
onSelect = key => {
this.setState({ selected: key });
const myList = this.state.statelist;
const myItemDetails = myList.filter(items=>items.key === key);
console.log(myItemDetails)
}
render() {
const { selected } = this.state;
// Create menu from items
const menu = Menu(this.state.statelist, selected);
const {statelist} = this.state;
return (
<div className="HorizantScroller">
<ScrollMenu
data={menu}
arrowLeft={ArrowLeft}
arrowRight={ArrowRight}
selected={selected}
onSelect={this.onSelect}
/>
<Items Items={Items[selected]}/>
</div>
);
}
}
export default HorizantScroller;
According to your data-structure, you need to use use item.name to check for the selectedKey
myList.filter(items=>items.name === key);
Note: you must make sure that you are not updating stateList state after filtering the array, otherwise your your state will loose the data
Instead you must use another state variable to store filtered list or apply the filter while render instead of storing the filtered value in state
I have this Dropdown menu instance:
<Dropdown
selection
options={this.state.options}
search
value={value}
onChange={this.handleChange}
onSearchChange={this.handleSearchChange}
/>
and when my backend returns response, which is then set as state and it is structured like this:
"options": [
{
"text": "New York,All Airports (NYC) , USA",
"value": "NYC"
},
{
"text": "New York,Newark Liberty Intl (EWR), USA",
"value": "EWR"
},
{
"text": "New York,John F Kennedy (JFK), USA",
"value": "JFK"
},
{
"text": "New York,La Guardia (LGA), USA",
"value": "LGA"
}
]
...I get this warning:
Warning: flattenChildren(...): Encountered two children with the same
key, 1:$BLZ. Child keys must be unique; when two children share a
key, only the first child will be used.
in select (created by Dropdown)
in div (created by Dropdown)
in Dropdown (created by SearchForm)
How do I add keys to these elements to prevent this warning?
So looking at the code for the Semantic UI source for the dropdown component, the render options function converts your passed in options into a array of DropdownItem components:
renderOptions = () => {
const { multiple, search, noResultsMessage } = this.props
const { selectedIndex, value } = this.state
const options = this.getMenuOptions()
if (noResultsMessage !== null && search && _.isEmpty(options)) {
return <div className='message'>{noResultsMessage}</div>
}
const isActive = multiple
? optValue => _.includes(value, optValue)
: optValue => optValue === value
return _.map(options, (opt, i) => (
<DropdownItem
key={`${opt.value}-${i}`}
active={isActive(opt.value)}
onClick={this.handleItemClick}
selected={selectedIndex === i}
{...opt}
// Needed for handling click events on disabled items
style={{ ...opt.style, pointerEvents: 'all' }}
/>
))
}
the key for this array is set by taking the value prop and appending the index to it:
key={`${opt.value}-${i}`}
which should always be unique since the index is used but there is another part of the code for hidden inputs
renderHiddenInput = () => {
debug('renderHiddenInput()')
const { value } = this.state
const { multiple, name, options, selection } = this.props
debug(`name: ${name}`)
debug(`selection: ${selection}`)
debug(`value: ${value}`)
if (!selection) return null
// a dropdown without an active item will have an empty string value
return (
<select type='hidden' aria-hidden='true' name={name} value={value} multiple={multiple}>
<option value='' />
{_.map(options, option => (
<option key={option.value} value={option.value}>{option.text}</option>
))}
</select>
)
}
in this one the key is set to only the value, not the value plus index.
<option key={option.value} value={option.value}>{option.text}</option>
this might be your problem, if you have duplicate values then the key will not be unique. Double check the options list to make sure you don't have duplicate values.