I have the following code and need to add two extra things to it but I'm stuck and not sure how to do it.
I need to add:
If there are no products in the category a NotFound component will show a message.
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
Ideally I'm looking for the simplest solution as I'm currently learning React. Thanks!
Main Component
import React from 'react';
import Item from './components/Item';
class App extends React.Component {
state = {
items: [
{
title: "The Spice Girls",
price: 10,
category: "Pop",
quantity: 1,
},
{
title: "Beethoven",
price: 5,
category: "Classical",
quantity: 1,
},
{
title: "Bob Marley",
price: 15,
category: "Reggae",
quantity: 1,
}
],
category: " ",
filtered: [],
}
handleChange = e => {
this.setState({category: e.target.value},()=>console.log(this.state.category));
}
handleClick = (event) => {
event.preventDefault()
var newList = this.state.items;
var filteredItems = newList.filter(item => item.category === this.state.category)
this.setState({filtered: filteredItems})
}
render () {
let show;
if(this.state.category !== " "){
show = this.state.filtered.map((item, i) => <Item key = {i} cd={item}/>)
}else{
show = this.state.items.map( (item,i) => <Item key = {i} cd={item}/>)
}
return (
<div>
<h1 className = "title">CD</h1>
<h2>Search music below:</h2>
<form>
Music style: <input onChange = {this.handleChange}></input>
<button onClick = {this.handleClick}>Search</button>
</form>
{show}
</div>
)
}
}
export default App;
Item Component
import React from 'react';
class Item extends React.Component {
render () {
return (
<div className = "items">
<div className = "item">
<h3>{this.props.cd.title}</h3>
<div className = "price">Price: {this.props.cd.price}€</div>
<div className = "quantity">Quantity: {this.props.cd.quantity}</div>
<div className = "category">Category: {this.props.cd.category}</div>
</div>
</div>
)
}
}
export default Item;
First of all some suggested changes before I answer your question
There are a few things which confused me much when analysing your code, so will share them with you, as if in the future you work on teams, it would be handy if other people can understand your code.
You have an on change event for the text box which is different to the event for the search button next to it. Users would expect it to be the same and so that's really confusing.
You have 2 lists of items essentially, a raw and unfiltered and you switch between which 2 to present on the screen. Sometimes you need to have a raw set and that's fine, but perhaps make sure that the only ones which are presented as such is just either the state.items or the state.filtered. I would probably expect the state.filtered
Make your search case insensitive, e.g. pop should match Pop
Answer to your question'ss
If there are no products in the category a NotFound component will show a message.
For this I would first modify your show logic to work on the same filtered list just change your event functions to manipulate the filtered list and untouch the items one.
add another condition perhaps for when there are no cases
if (this.state.filtered) {
show = this.state.filtered.map((item, i) => <Item key={i} cd={item} />);
} else {
show = <h1>NoneFound</h1>;
}
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
handleClick = event => {
event.preventDefault();
var { category } = this.state;
var newList = this.state.items;
var filteredItems;
if ([" ", "All"].some(t => t === category)) {
filteredItems = newList;
} else {
filteredItems = newList.filter(
item => item.category.toLowerCase() === this.state.category.toLowerCase()
);
}
this.setState({ filtered: filteredItems });
};
My souloution to this was to modify your onClick event to correctly manipulated the filtered list.
You can see my full soloution to this on my codesandbox here
Related
I am dynamically rendering a large list of cards using React hooks, and each component has its drop-down list. If I select one option, which updates the component state, every drop-down update to the same value. I have tried implementing select an option of React, and separately React-dropdown, and none of my approaches work. I recognize the problem is that when any dropdown is updated, they all refer to the same value in state and if I comment out the state update the dropdowns work.
I just don’t capture the state update. I am unsure what is the best way to capture these selection(s) while the user picks between the dropdowns and then showing the rendered selection.
A lot of the approaches I have seen don’t address having multiple dynamically created dropdowns on one page, so if there are any thoughts on how to resolve the issue, that would be helpful. Below is part of the code:
<select onChange(event=> selectedDropdownItem(selection)
value={event.target.value}
>
dropdownList.map(name =>
<option key={name.id} value={name.id} label={name.name} />
</select>
const [header, setHeader] = useState({ name: “select name”, id: null })
selectedDropdownItem = selection => {
setHeader({
name: selection.name,
id: selection.id
})
}
As i am not much clear about your question for the multiple selection in single component. I just created one which will do the dynamic dropdown based on that, and update the name based on selection using useState. Whenever you do the selection , it will come to parent component and do the update and return the value to the child component.
Hope i explained as you expected.
Here is the working code link.
If you have any queries, give a comment :)
import React, {useState} from 'react';
const DropDownCard = ({name, list, selectedValue, onChange}) => {
const onSelectChange = event => {
const { value } = event.target;
const { name } = list.find(val => val.id == value);
onChange({id: value, name});
};
return (
<div>
<h1>Card {name}</h1>
<select onChange={onSelectChange} value={selectedValue}>
{
list.map(name => <option
key={name.id}
value={name.id}
label={name.name}
/>)
}
</select>
</div>
)
}
const cardList = [
{name:'', selectedValue: null},
{name:'', selectedValue: null},
{name:'', selectedValue: null},
];
const Home = () => {
const [hea, setHea] = useState(cardList);
const list = [
{name: "select name", id: ''},
{name: "Name 1", id: 1},
{name: "Name 2", id: 2}
];
const onCardNameSelect = (info, index) => {
setHea(prevState => {
const {id, name} = info;
prevState[index].selectedValue = id;
prevState[index].name = name;
return [...prevState];
});
};
return(
<>
<div>Hello</div>
{hea.map((val,i) => <DropDownCard key={"dropDown"+Math.random()}
name={val.name}
selectedValue={val.selectedValue}
list = {list}
onChange = {event => onCardNameSelect(event, i)}
/>)}
</>
)
};
export default Home;
Have a CRA set up and all is working fine, there is one component in particular though that onClick events on an element are not firing. Based on Tim Smith's method here.
Code:
import React, { Component } from 'react'
import './SearchForm.css'
class SearchForm extends Component {
constructor(props) {
super(props)
this.state = {
filtered: []
}
this.handleChange = this.handleChange.bind(this)
this.handleSelect = this.handleSelect.bind(this)
}
componentDidMount() {
this.setState({
filtered: this.props.items
})
}
componentWillReceiveProps(nextProps) {
this.setState({
filtered: nextProps.items
})
}
handleChange(event) {
// Variable to hold the original version of the list
let currentList = []
// Variable to hold the filtered list before putting into state
let newList = []
// If the search bar isn't empty
if (event.target.value !== "") {
// Assign the original list to currentList
currentList = this.props.items
// Use .filter() to determine which items should be displayed
// based on the search terms
newList = currentList.filter(item => {
// change current item to lowercase
const lc = item.name.toLowerCase()
// change search term to lowercase
const filter = event.target.value.toLowerCase()
// check to see if the current list item includes the search term
// If it does, it will be added to newList. Using lowercase eliminates
// issues with capitalization in search terms and search content
return lc.includes(filter)
})
} else {
// If the search bar is empty, set newList to original task list
newList = this.props.items
}
// Set the filtered state based on what our rules added to newList
this.setState({
filtered: newList
})
}
handleSelect(string) {
console.log(string)
}
render() {
var _this = this
this.search_results = this.state.filtered.map(function(item, key) {
return (
// THESE onClicks DO NOT FIRE
<li key={item.id} onClick={(e) => _this.handleSelect(item.object)}>
<div className="name">{item.name}</div>
<small>{item.type}</small>
</li>
)
})
return (
<div className="search">
<input type="text" className="search-form" onChange={this.handleChange} placeholder="Search by client or advisor" />
<div className="results-container">
<ul className="results">
{this.search_results}
</ul>
// BUTTON HERE (in .results-container) DOES NOT FIRE
<button onClick={(e) => _this.handleSelect('hi')}>hi</button>
</div>
// BUTTON HERE (out of .results-container) FIRES
<button onClick={(e) => _this.handleSelect('hi')}>hi</button>
</div>
)
}
}
export default SearchForm
I have tried using just this vs. _this, binding/not binding the handleSelect function, passing and not passing event to it, passing and not passing any variables at all to it (and just having handleSelect console.log a 'firing'). I realize that the use of "this" within the map was a no go, thus the _this variable. Within that map, if I console.log(_this.handleSelect), it shows the function in the console.
Thank you in advance for taking a look!
Solved-- this ended up being an issue where the .results-container was hidden (via display: none;) and was only shown (via display: block) when the .results-container was focused on. Due to this no onClick events were firing on the items within .results-container.
Got around it by:
.search .results-container {
height: 0;
overflow: hidden;
}
input.search-form:focus + .results-container, .search .results-container:hover {
height: auto;
}
Thank you Acy for sending me in the right direction!
I am trying to solve a problem that happens in react app. In one of the views (components) i have a management tools that operate on big data. Basically when view loads i have componentDidMount that triggers ajax fetch that downloads array populated by around 50.000 records. Each array row is an object that has 8-10 key-value pairs.
import React, { Component } from "react";
import { List } from "react-virtualized";
import Select from "react-select";
class Market extends Component {
state = {
sports: [], // ~ 100 items
settlements: [], // ~ 50k items
selected: {
sport: null,
settlement: null
}
};
componentDidMount() {
this.getSports();
this.getSettlements();
}
getSports = async () => {
let response = await Ajax.get(API.sports);
if (response === undefined) {
return false;
}
this.setState({ sports: response.data });
};
getSettlements = async () => {
let response = await Ajax.get(API.settlements);
if (response === undefined) {
return false;
}
this.setState({ settlements: response.data });
};
save = (key, option) => {
let selected = { ...this.state.selected };
selected[key] = option;
this.setState({ selected });
};
virtualizedMenu = props => {
const rows = props.children;
const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => (
<div key={key} style={style}>
{rows[index]}
</div>
);
return (
<List
style={{ width: "100%" }}
width={300}
height={300}
rowHeight={30}
rowCount={rows.length || 1}
rowRenderer={rowRenderer}
/>
);
};
render() {
const MenuList = this.virtualizedMenu;
return (
<div>
<Select
value={this.state.selected.sport}
options={this.state.sports.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("sport", option)}
/>
<Select
components={{ MenuList }}
value={this.state.selected.settlement}
options={this.state.settlements.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("settlement", option)}
/>
</div>
);
}
}
The problem i am experiencing is that after that big data is downloaded and saved to view state, even if i want to update value using select that has ~100 records it takes few seconds to do so. For example imagine that smallData is array of 100 items just { id: n, name: 'xyz' } and selectedFromSmallData is just single item from data array, selected with html select.
making a selection before big data loads takes few ms, but after data is loaded and saved to state it suddenly takes 2-4 seconds.
What would possibly help to solve that problem (unfortunately i cannot paginate that data, its not anything i have access to).
.map() creates a new array on every render. To avoid that you have three options:
store state.sports and state.settlements already prepared for Select
every time you change state.sports or state.settlements also change state.sportsOptions or state.settlementsOptions
use componentDidUpdate to update state.*Options:
The third option might be easier to implement. But it will trigger an additional rerender:
componentDidUpdate(prevProps, prevState) {
if (prevState.sports !== this.state.sports) {
this.setState(oldState => ({sportsOptions: oldState.sports.map(...)}));
}
...
}
Your onChange handlers are recreated every render and may trigger unnecessary rerendering of Select. Create two separate methods to avoid that:
saveSports = option => this.save("sport", option)
...
render() {
...
<Select onChange={this.saveSports}/>
...
}
You have similar problem with components={{ MenuList }}. Move this to the state or to the constructor so {MenuList} object is created only once. You should end up with something like this:
<Select
components={this.MenuList}
value={this.state.selected.settlement}
options={this.state.settlementsOptions}
onChange={this.saveSettlements}
/>
If this doesn't help consider using the default select and use a PureComponent to render its options. Or try to use custom PureComponents to render parts of the Select.
Also check React-select is slow when you have more than 1000 items
The size of the array shouldn't be a problem, because only the reference is stored in the state object, and react doesn't do any deep equality on state.
Maybe your render or componentDidUpdate iterates over this big array and that causes the problem.
Try to profile your app if this doesn't help.
I have a table of schedules that is rendered by a dropdown. Each schedule can then be marked for export via a slider, this will store the schedule id in scheduleIdsToExport and show that schedule in the export table.
But if I change the Select Query dropdown, which renders more schedules specific to that query, the schedules marked for export from the previous query disappear from the table. I want the schedules marked for export to persist in the table no matter what query is selected from the dropdown.
So I'm thinking I need to have something in my slider function to update state with the all the schedule objects marked for export and have them persist in the exported table. I'm not exactly sure how to go about storing all the schedules to keep them in the exported table and have the scheduleIdsToExport array also keep the id's of each schedule
slider = ({ id, isExported }) => {
if (isExported === true) {
this.setState(
{
scheduleIdsToExport: [id, ...this.state.scheduleIdsToExport]
},
() => {
console.log(this.state.scheduleIdsToExport);
}
);
} else {
const newArray = this.state.scheduleIdsToExport.filter(
storedId => storedId !== id
);
this.setState(
{
scheduleIdsToExport: newArray
},
() => {
console.log(this.state.scheduleIdsToExport);
}
);
}
};
The sandbox here will provide further explanation on what is happening.
This is just chaotic!
The problem : Keep track from multiples list of items(schedules) that will eventually be added to another list schedulesToExport
The Solution :
Create a parent component that reflects the previously described state
class Container extends Component{
state ={
querys :[
['foo','lore ipsum','it\'s never lupus'],
['bar','ipsumlorem', 'take the canolli']
],
selectedQuery : 0,
schedulesToExport : []
}
}
Now we have a list of lists, that can be interpreted as a list of querys containing a list of schedules
Render a select element to reflect each query:
render(){
const { querys, selectedQuery } = this.state
const options = querys.map((_, index) => (<option value={index}> Query: {index + 1}</option>))
return(
<div>
<select onChange={this.selectQuery}>
{options}
</select>
{
querys[selectedQuery].map(schedule => (
<button onClick={() => this.selectSchedule(index)}> Schedule: {schedule} </button>
))
}
</div>
)
}
What's happening? We are just rendering the selected query by it's index and showing all it's respective schedules.
Implement the selectQuery and selectSchedule methods which will add the selected schedule in the list to export:
selectQuery = e => this.setState({selectedQuery : e.target.value})
selectSchedule = index => {
const { selectedQuery } = this.state
const selected = this.state.querys[selectedQuery][index]
this.setState({
schedulesToExport: this.state.schedulesToExport.concat(selected)
})
}
That's it, now you a have a list of querys being displayed conditionally rendered selectedQuery props is just a index, but could be a property's name. You only see schedules from the current selected query, so when you click on schedule we just return it's index, and the state.querys[selectedQuery][index] will be your selected schedule, that is securely store in the state on a separated list.
I have updated your sandbox here.
In essence, it does not work in your example because of the following:
schedules
.filter(schedule =>
scheduleIdsToExport.includes(Number(schedule.id))
)
.map(schedule => {
return (
<Table.Row>
...
</Table.Row>
);
})
The value of schedules is always set to the current query, hence you end up showing schedules to export for the current query only.
A solution that changes very little of your code is to ditch scheduleIdsToExport altogether, and use schedulesToExport instead. Initially, we'll set schedulesToExport to an empty
object; we'll add schedules to it (keyed by schedule id) every time a schedule is selected - we'll delete schedules in the same way every time a schedule is unselected.
class App extends React.Component {
// ...
slider = ({ id, isExported }) => {
const obj = this.state.schedules.find(s => Number(s.id) === Number(id));
if (isExported === true) {
this.setState({
schedulesToExport: {
...this.state.schedulesToExport,
[id]: {
...obj
}
}
});
} else {
const newSchedulesToExport = this.state.schedulesToExport;
delete newSchedulesToExport[id];
this.setState({
schedulesToExport: newSchedulesToExport
});
}
};
// ...
}
You would then render the schedules to export as follows:
Object.keys(schedulesToExport).map(key => {
const schedule = schedulesToExport[key];
return (
<Table.Row>
...
</Table.Row>
);
})
Again, see more details in sandbox here.
This is a super noob question, I'm learning redux and it's capabilities. However, I'm stuck while trying to remove an item from a list. This list has items added to it via a JSON object I imported and that is pushed into an array which I later loop through.
In one component I add the prices(mock data) through button clicks and I have a total (sum of the prices) working. This action also adds the name to the side list.
I've been searching but haven't found a good solution. I was hoping someone here can help me understand and/or point me in the right direction.
So far when I click the button it adds an empty button to the list instead than removing. Correct me if I'm wrong but this is the intended behavior for the slice function, to return an array?
Here is my initial state:
const initialState = [{
name: '',
total: '',
}]
My action type:
case ActionTypes.REMOVE_EXAMS:
return[
...state[0].name.slice(0, action.index),
...state[0].name.slice(action.index + 1)
]
case ActionTypes.ADD_PRICES:
return [
{
total: Number(state[0].total + action.price),
name: action.name
}
]
This is the component that renders a side list with all the items that have been added. My idea is to press the button and remove that item permanently from this side list
static propTypes = {
dataCarryName: PropTypes.string.isRequired,
removeExams: PropTypes.func.isRequired,
total: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.names = [];
}
render() {
{this.names.push(this.props.dataCarryName)}
{console.log(this.names)}
return (
<MuiThemeProvider>
<div className="side-exam-view">
<Paper zDepth={2}>
<h1> Carrito </h1>
{this.props.total}
{this.names.map((i, k) => {
return(
<ul key={k}
className="exam-list-names">
<li key={k}>
<button onClick={() => this.props.removeExams(( i.length, {i}))}> {i} - X</button>
</li>
<Divider />
</ul>
)
})}
</Paper>
</div>
</MuiThemeProvider>
);
}}
Update:
These are my action creators:
export const addPrices = (price, name) => {
return{
type: ActionTypes.ADD_PRICES,
price,
name
}}
export const removeExams = (index, name) => {
return {
type: ActionTypes.REMOVE_EXAMS,
index,
name
}}
Ok. I see at least a couple issues here and hopefully dealing with them will help you solve your problem.
First, I'm not sure that the way you're keeping your sum makes sense. I think what you really want to do is maintain a list of exams where each exam has a name and a price. You also want to be able to display the list of exam names and also the sum of the prices. So for starters I think your reducer should look more like this.
const initialState = [];
export default function reduce(state = initialState, action) {
switch (action.type) {
case ActionTypes.ADD_EXAM:
return state.concat({ name: action.name, price: action.price });
case ActionTypes.REMOVE_EXAM:
return state.filter(exam => exam.name === action.name)
default:
return state;
}
}
So ADD_EXAM adds a new entry to your array with a name and a price, and REMOVE_EXAM looks for the exam specified by action.name and filters it out.
You can easily display your sum like this:
const sum = exams.reduce((sum, exam) => sum + exam.price, 0);
And you can display your names like this:
const names = exams.map((exam, index) => <p key={index}>{exam.name}</p>);