Action on button does not perform the action to change the sorting object which sort all itens in a list (another component). I expect the button to perform this changes passing the sortBy variable on this.props.dispatch(orderBy(sortBy)) or another dynamic way without a button.
import React from 'react';
import { connect } from 'react-redux';
import orderBy from '../actions/sorting';
const TYPES = [
{ slug: "title", description: "Title" },
{ slug: "author", description: "Author" },
{ slug: "editionYear", description: "Edition Year" }
];
class BookListSorter extends React.Component {
state = {
sortBy: [{ title: "asc" }]
};
// Helper methods
getSortByKeyForIndex = index =>
Object.keys(this.state.sortBy[index] || {})[0];
getSortByValueForIndex = index =>
Object.values(this.state.sortBy[index] || {})[0];
changeSort = (key, index) => e => {
// This is what is called when an select option changes
const { target } = e; // Save target from event to use in the callback
this.setState(({ sortBy }) => {
// Use a function for atomicness - this prevents state from being out of sync
// Get the type from the event object if the onchange handler is for the type,
// otherwise get from sortBy object
const type =
key === "type" ? target.value : this.getSortByKeyForIndex(index);
// Get the direction from the event object if the onchange handler is for the direction,
// otherwise get from sortBy object
const direction =
key === "direction" ? target.value : this.getSortByValueForIndex(index);
// If both options are set, replace the indexed spot in the sortby object
// Return updated state.
return type || direction
? sortBy.splice(index, 1, { [type]: direction })
: sortBy.splice(index, 1);
});
};
filterTypes = index => ({ slug }) => {
// Filter out already used keys from previous rows
const sortByKeys = this.state.sortBy
.slice(0, index)
.reduce((keys, sortObj) => keys.concat(Object.keys(sortObj)[0]), []);
return !sortByKeys.includes(slug);
};
render() {
const { sortBy } = this.state;
const lastIndex = sortBy.length - 1;
// Only add a new row if the one above it is completely filled out
const shouldAddNewRow =
this.getSortByKeyForIndex(lastIndex) &&
this.getSortByValueForIndex(lastIndex);
const rowCount = shouldAddNewRow ? sortBy.length + 1 : sortBy.length;
return (
<div>
<h1>Choose sort order</h1>
{Array.from(Array(Math.min(rowCount, TYPES.length))).map(
(dummy, index) => (
<div>
<span>Row {index}: </span>
<select
defaultValue={this.getSortByKeyForIndex(index)}
onChange={this.changeSort("type", index)}
>
<option value="">None</option>
{TYPES.filter(this.filterTypes(index)).map(
({ slug, description }) => (
<option value={slug}>{description}</option>
)
)}
</select>
<select
defaultValue={this.getSortByValueForIndex(index)}
onChange={this.changeSort("direction", index)}
>
<option value="">None</option>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<br />
</div>
)
)}
<br />
<button onClick={() => this.props.dispatch(orderBy(sortBy))}>sort</button>
</div>
);
}
};
const mapStateToProps = (state) => {
return {
sorting: state.sorting
};
};
//ACTIONS
//ADD BOOK
const addBook = ({ title = '', author = '', editionYear = 0} = {}) => ({
type: 'ADD_BOOK',
book: {
title,
author,
editionYear
}
});
//SORT BY
const orderBy = (order) => ({
type: 'SORT_BY',
orderBy: order
});
//book reducer
const bookReducerDefaultState = [];
const bookReducer = (state = bookReducerDefaultState, action) => {
switch(action.type) {
case 'ADD_BOOK':
return [
...state,
action.book
];
default:
return state;
};
};
//sorting reducer
const sortingReducerDefaultState = {
orderBy: [{title: 'asc'},{author: 'asc'}]
};
const sortingReducer = (state = sortingReducerDefaultState, action) => {
switch(action.type) {
case 'SORT_BY':
return {
...state,
orderBy: action.orderBy
};
default:
return state;
};
}
function compareBy(a, b, orderBy) {
const key = Object.keys(orderBy)[0],
o = orderBy[key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return 0;
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
return 0
}
function getSortedBooks(books, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return books.sort((a, b) => {
let result
for (let i = 0; i < orderBy.length; i++) {
result = compareBy(a, b, orderBy[i])
if (result !== 0) {
return result
}
}
return result
})
}
//store creation
const store = createStore(
combineReducers({
books: bookReducer,
sorting: sortingReducer
})
);
store.subscribe(() => {
const state = store.getState();
const sortedBooks = getSortedBooks(state.books, state.sorting.orderBy)
console.log(sortedBooks);
});
export default connect(mapStateToProps)(BookListSorter);
Can anyone help with this issue. Since the button i set up is not working?
Note: This was an answer to the original question
The best way to get the value of a select element in React is to add an onChangehandler.
In your example, it might look something like this:
<select onChange={(event) => this.setState({ firstType: event.target.value })}>
<option value="title">Title</option>
<option value="author">Author</option>
<option value="editionYear">Edition Year</option>
</select>
<select onChange={(event) => this.setState({ firstDirection: event.target.value })}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
By changing the above select inputs, the state would look like this:
{
firstType: 'author',
firstDirection: 'desc'
}
(The state wont automatically be set until changes are made, so you would have to initialize separately.)
You would then need to transform that object into the shape you need.
This is just an example, I'll leave it up to you to transform the state into the shape that you need and to connect up redux since it looks like that's what you intend to do with the import of connect.
Note: if the option tags don't have a value attribute set, the value in event.target.value would be the content inside the tags.
Related
I'm creating an app with the Pokemon API. I have the back ready, and on the front I work with redux and react.
What I do in the reducer I pass to a Filter.js file, which I import into Pokemons.js. In that last file, the only thing I do is map the array with all the pokemons.
The problem is that the filters and sorts are not working well. When I click on one of the Type options, it shows me the pokemons with that type, but then I can't choose another one. In the Created options, if I choose Data Base it shows me the ones created in the DB, but if I click on Api the page is reloaded and it shows me all the pokemons. And if I click on All, it does nothing.
I also couldn't get the sorts to work and if I touch the Clear Filter button everything breaks.
I think the problem is how the information passes from one side to the other, because I check the logic and I don't find any errors, but I'm no longer sure of anything.
This is the reducer:
import {
GET_POKEMONS,
GET_POKEMON_DETAIL,
CREATE_POKEMON,
DELETE_POKEMON,
GET_TYPES,
SORT_BY_ALPHABET,
SORT_BY_ATTACK,
FILTER_BY_CREATED,
FILTER_BY_TYPE,
SEARCH_POKEMON,
CLEAN_FILTER,
} from "./actions";
const initialState = {
pokemons: [],
pokemonDetail: {},
types: [],
filterByType: null,
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POKEMONS:
return {
...state,
pokemons: action.payload,
};
case GET_POKEMON_DETAIL:
return {
...state,
pokemonDetail: action.payload,
};
case GET_TYPES:
return {
...state,
types: action.payload,
};
case CREATE_POKEMON:
const name = action.payload.data.name;
const speed = action.payload.data.speed;
const hp = action.payload.data.hp;
const height = action.payload.data.height;
const weight = action.payload.data.weight;
const attack = action.payload.data.attack;
const defense = action.payload.data.defense;
const createdInDB = action.payload.data.createdInDB;
const types = action.payload.data.types;
const img = action.payload.data.img;
return {
...state,
//pokemons: state.pokemons.concat({action.payload.data.name, action.payload.data.speed })
pokemons: state.pokemons.concat({
name,
speed,
hp,
height,
weight,
attack,
defense,
createdInDB,
types,
img,
}),
};
case DELETE_POKEMON:
return {
...state,
pokemons: state.pokemons.filter(
(pokemon) => pokemon.id !== action.payload
),
};
//SORTS Y FILTERS
case SORT_BY_ALPHABET:
const sortAlpha =
action.payload === "a-z"
? state.pokemons.sort((a, b) => {
return a.name.toLowerCase() > b.name.toLowerCase();
})
: state.pokemons.sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase();
});
return {
...state,
pokemons: sortAlpha,
};
case SORT_BY_ATTACK:
const sortAsc = state.pokemons.sort((a, b) => a.attack > b.attack);
const sortDes = state.pokemons.sort((a, b) => a.attack < b.attack);
const sortAttack = action.payload;
if (sortAttack === "- to +")
return {
...state,
pokemons: sortAsc,
};
else if (sortAttack === "+ to -")
return {
...state,
pokemons: sortDes,
};
break;
case FILTER_BY_TYPE:
let type = action.payload;
let pokemonFiltered = state.pokemons.filter((poke) => poke.types.includes(type))
console.log("filterByType", action.payload);
if(pokemonFiltered.length > 0){
return {
...state,
pokemons: pokemonFiltered,
};
} else {
return {
...state,
pokemons: state.pokemons
}
}
case FILTER_BY_CREATED:
let created = state.pokemons.filter((p) => typeof p.id === "string");
let api = state.pokemons.filter((p) => typeof p.id === "number");
if (action.payload === "Data Base") {
console.log("action.payload is", action.payload);
return {
...state,
pokemons: created,
};
} else if (action.payload === "API") {
console.log("action.payload is", action.payload);
return {
...state,
pokemons: api,
};
} else if (action.payload === "All") {
console.log("action.payload is", action.payload);
return {
...state,
pokemons: state.pokemons,
};
}
break;
case SEARCH_POKEMON:
return {
...state,
pokemons: action.payload,
};
case CLEAN_FILTER:
return{
...state,
pokemons: state.pokemons,
}
default:
return { ...state };
}
};
export default rootReducer;
This is actions.js:
import axios from 'axios';
export const GET_POKEMONS = "GET_POKEMONS";
export const GET_POKEMON_DETAIL = "GET_POKEMON_DETAIL";
export const CREATE_POKEMON = "CREATE_POKEMON";
export const DELETE_POKEMON = "CREATE_POKEMON";
export const GET_TYPES = "GET_TYPES";
//Filtos y ordenamiento
export const FILTER_BY_TYPE = "FILTER_BY_TYPE";
export const FILTER_BY_CREATED = "FILTER_BY_CREATED";
export const SORT_BY_ALPHABET = "SORT_BY_ALPHABET";
export const SORT_BY_ATTACK = "SORT_BY_ATTACK";
export const CLEAN_FILTER = "CLEAR_FILTER"
export const SEARCH_POKEMON = "SEARCH_POKEMON";
export const getPokemons = () => {
return function(dispatch) {
return fetch('http://localhost:3001/pokemons')
.then(res => res.json())
.then(pokemons => dispatch(
{type: GET_POKEMONS, payload: pokemons}
))
}
};
export const getPokemonDetail = (id) => {
return function(dispatch) {
return fetch(`http://localhost:3001/pokemons/${id}`)
.then(res => res.json())
.then(data => dispatch(
{type: GET_POKEMON_DETAIL, payload: data[0]},
console.log('data[0] in actions', data[0])
))
}
};
export const createPokemon = (pokemon) => {
// const options = {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(pokemon),
// };
return async function(dispatch){
const newPokemon = await axios.post(`http://localhost:3001/pokemons/`, pokemon)
dispatch({type: CREATE_POKEMON, payload: newPokemon})
}
//return { type: CREATE_POKEMON, payload: pokemon}
};
export const deletePokemon = (id) => {
return { type: DELETE_POKEMON, payload: id}
};
export const getTypes = () => {
return function(dispatch){
return fetch('http://localhost:3001/types')
.then(res => res.json())
.then(pokemons => {
let types = [];
pokemons.map((pokemon) => types.push(pokemon.types))
dispatch ({type: GET_TYPES, payload: types})
}
)
}
};
export const sortByAlphabet = (order) => {
return ({type: SORT_BY_ALPHABET, payload: order});
}
export const sortByAttack = (order) => {
return {type: SORT_BY_ATTACK, payload: order}
}
export const filterByType = (type) => {
return {type: FILTER_BY_TYPE, payload: type}
}
export const filterByCreated = (value) => {
return {type: FILTER_BY_CREATED, payload: value}
}
export const searchPokemon = (query) => (dispatch, getstate) => {
const { pokemons } = getstate()
const result = pokemons.searchPokemon.find((poke) => poke.name.toLowerCase().includes(query.toLowerCase()));
dispatch({ type: SEARCH_POKEMON, payload: result})
}
export const cleanFilter = (payload) => {
return {type: CLEAN_FILTER, payload}
}
This is Filters.js:
import React, { useState } from "react";
import s from "./Filters.module.css";
import { useDispatch, useSelector } from "react-redux";
import * as actions from "../../redux/actions";
import { useHistory } from "react-router-dom";
export default function Filters() {
const [selectValue, setSelectValue] = React.useState("");
const [selectValueB, setSelectValueB] = React.useState("");
const [orden, setOrden] = useState("");
const [ordenB, setOrdenB] = useState("");
const history = useHistory();
const dispatch = useDispatch();
const pokemons = useSelector((state) => state.pokemons);
React.useEffect(() => {
if (!pokemons[0]) {
dispatch(actions.getPokemons());
dispatch(actions.getTypes());
}
}, [dispatch, pokemons]);
function handleClick(e){
//e.preventDefault();
const value = e.target.value;
console.log('resetear filtros')
dispatch(actions.cleanFilter(value))
//history.push('/pokemons')
};
function handleFilterType(e) {
//e.preventDefault();
const value = e.target.value;
setSelectValue(value); //para mostrarle a usuario lo que eligio
dispatch(actions.filterByType(value)); //disapara la action del reducer
history.push("/pokemons");
}
function handleFilterCreated(e) {
e.preventDefault();
const value = e.target.value;
setSelectValueB(value);
console.log('filtrar por creado')
dispatch(actions.filterByCreated(value));
}
function handleSortByAlpha(e) {
//e.preventDefault();
dispatch(actions.sortByAlphabet(e.target.value));
setOrden(`Ordered from ${e.target.value}`)
console.log('ordenado por alfabeto')
}
function handleSortByAttack(e) {
e.preventDefault();
dispatch(actions.sortByAttack(e.target.value));
setOrdenB(`Ordered from ${e.target.value}`)
console.log('ordenado por attack')
}
return (
<div className={s.filterSection}>
<div className={s.filters}>
<h2 className={s.filterTitle}>Filters</h2>
<div className={s.filterBy}>
<h3 className={s.filterSubitle}>Filter by type</h3>
<select
className={s.select}
value="default"
onChange={(e) => handleFilterType(e)}
>
<option value="default" disabled hidden>
Pokemon type
</option>
<option value="bug">bug</option>
<option value="dark">dark</option>
<option value="dragon">dragon</option>
<option value="electric">electric</option>
<option value="fairy">fairy</option>
<option value="fighting">fighting</option>
<option value="flying">flying</option>
<option value="fire">fire</option>
<option value="ghost">ghost</option>
<option value="grass">grass</option>
<option value="ground">ground</option>
<option value="ice">ice</option>
<option value="normal">normal</option>
<option value="poison">poison</option>
<option value="psychic">psychic</option>
<option value="rock">rock</option>
<option value="shadow">shadow</option>
<option value="steel">steel</option>
<option value="unknow">unknow</option>
<option value="water">water</option>
</select>
{selectValue && <h3 className={s.showFilter}>{selectValue}</h3>}
</div>
<div className={s.filterBy}>
<h3 className={s.filterSubitle}>Created in</h3>
<select
className={s.select}
value="default"
onChange={e => handleFilterCreated(e)}
>
<option value="default" disabled hidden>
Created in
</option>
<option value="All">All</option>
<option value="API">API</option>
<option value="Data Base">Data Base</option>
</select>
{selectValueB && <h3 className={s.showFilter}>{selectValueB}</h3>}
</div>
</div>
<div className={s.filters}>
<div className={s.filterBy}>
<h3 className={s.filterSubitle}>Sort by Alphabet</h3>
<select
value="default"
onChange={(e) => handleSortByAlpha(e)}
>
<option value="default" disabled hidden>
Sort by Alphabet
</option>
<option value="a-z" onClick={(e) => handleSortByAlpha(e)}>From A to Z</option>
<option value="z-a" onClick={(e) => handleSortByAlpha(e)}>From Z to A</option>
</select>
{orden && <h3 className={s.showFilter}>{orden}</h3>}
</div>
</div>
<div className={s.filters}>
<div className={s.filterBy}>
<h3 className={s.filterSubitle}>Sort by Attack</h3>
<select
value="default"
onChange={(e) => handleSortByAttack(e)}
>
<option value="default" disabled hidden>
Sort by Attack
</option>
<option value="- to +">From - to +</option>
<option value="+ to -">From + to -</option>
</select>
{ordenB && <h3 className={s.showFilter}>{ordenB}</h3>}
</div>
</div>
<button className={s.filterBtn} onClick={() => handleClick()}>Reset filters</button>
</div>
);
}
This is Pokemons.js
import React from 'react'
import s from './Pokemons.module.css'
import { useDispatch, useSelector } from "react-redux";
import * as actions from '../../redux/actions'
//importo para poder mapear todas las cards
import PokeCard from '../PokeCard/PokeCard';
import Filters from '../Filters/Filters';
export default function Pokemons() {
const dispatch = useDispatch();
const pokemons = useSelector((state)=> state.pokemons);
//const types = useSelector((state)=> state.types);
const filterByType = useSelector((state)=> state.filterByType);
React.useEffect(() => {
dispatch(actions.getPokemons());
dispatch(actions.getTypes());
},[dispatch])
console.log('filteredBytype', filterByType)
return (
<div className={s.pokemonsSection}>
<Filters />
<div className={s.allPokemons}>
{//si hay un filterbytype mostra lo q incluya ese filtro. Si no existe mapea todo pokemons
pokemons.map(poke =>{
return <PokeCard
key={poke.id}
id={poke.id}
name={poke.name}
image={poke.img}
types={poke.types}/>
})
}
</div>
</div>
)
}
I tryed diferent ways to code the reducer and the filters, but I get the same results.
I wish you can help me. Thanks!
The issue I see is that you are effectively mutating your "source of truth", i.e. the state.pokemons array. Each time state.pokemons is filtered the array size is less than or equal to its prior size. In other words, filtering is a reducing action, that array will never "remember" its prior elements once they are filtered, i.e. removed, from the array.
Filtered data is really derived state in that it is derived from some "source of truth" and a filtering condition. You don't want to mutate the original data, but you want to take it and the filter condition and derive a result to display.
I suggest storing the filtering conditions in the store and apply the filter/sorting when selecting the state. Adding/removing/editing should be done on the source data.
const initialState = {
pokemons: [],
pokemonDetail: {},
types: [],
filterByType: null,
sortBy: {
alpha: "a-z",
attack: "- to +",
},
filters: {},
search: "",
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POKEMONS:
return {
...state,
pokemons: action.payload,
};
case GET_POKEMON_DETAIL:
return {
...state,
pokemonDetail: action.payload,
};
case GET_TYPES:
return {
...state,
types: action.payload,
};
case CREATE_POKEMON:
const newPokemon = {...action.payload.data};
return {
...state,
pokemons: state.pokemons.concat(newPokemon),
};
case DELETE_POKEMON:
return {
...state,
pokemons: state.pokemons.filter(
(pokemon) => pokemon.id !== action.payload
),
};
//SORTS Y FILTERS
case SORT_BY_ALPHABET:
return {
...state,
sortBy: {
...state.sortBy,
alpha: action.payload
},
};
case SORT_BY_ATTACK:
return {
...state,
sortBy: {
...state.sortBy,
attack: action.payload
},
};
case FILTER_BY_TYPE:
return {
...state,
filterBy: {
...state.filterBy,
type: action.payload,
},
};
case FILTER_BY_CREATED:
return {
...state,
filterBy: {
...state.filterBy,
created: action.payload,
},
};
case SEARCH_POKEMON:
return {
...state,
search: action.payload,
};
case CLEAN_FILTER:
return {
...state,
sortBy: initialState.sortBy,
filterBy: initialState.filterBy,
search: initialState.search,
}
default:
return { ...state };
}
};
Update the searchPokemon action creator to just store the search query term lowercased.
export const searchPokemon = (query) => ({
type: SEARCH_POKEMON,
payload: query.toLowerCase()
});
Create a pokemon state selector function that does the work of filtering/sorting the state.pokemons array. Keep in mind that the .sort function callback should return a number (-1|0|1) instead of a boolean to indicate the sorted order between two elements. The .filter function callback returns a boolean if the current element should be included in the result array.
const pokemonSelector = state => {
return state.pokemons
.filter(pokemon => {
// NOTE: All strings include ""
return pokemon.name.toLowerCase().includes(state.search);
})
.filter(pokemon => {
return state.filterBy.type
? pokemon.types.includes(state.filterBy.type)
: true
})
.filter(pokemon => {
switch(state.filterBy.created) {
case "Data Base":
return typeof pokemon.id === "string";
case "API":
return typeof pokemon.id === "number";
default:
return true;
}
})
.sort((a, b) => {
return state.sortBy.alpha === "a-z"
? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
: b.name.toLowerCase().localeCompare(a.name.toLowerCase());
})
.sort((a, b) => {
switch(state.sortBy.attack) {
case "- to +":
return a.attack - b.attack;
case "+ to -":
return b.attack - a.attack;
default:
return 0;
}
});
};
Update the Pokemons component to import this new pokemonSelector selector function and pass this to the useSelector hook to compute the derived selected state.
...
import { pokemonSelector } from '../path/to/selectors';
export default function Pokemons() {
const dispatch = useDispatch();
const pokemons = useSelector(pokemonSelector); // gets filtered/sorted pokemons
React.useEffect(() => {
dispatch(actions.getPokemons());
dispatch(actions.getTypes());
},[dispatch])
return (
<div className={s.pokemonsSection}>
<Filters />
<div className={s.allPokemons}>
{pokemons.map(poke => (
<PokeCard
key={poke.id}
id={poke.id}
name={poke.name}
image={poke.img}
types={poke.types}
/>
))}
</div>
</div>
);
}
I'm working on a form which contains a section where users can choose the quantity and category of a concert ticket in a dropdown. This component is dynamically created and it can have more items(quantity + category). The dropdowns of the categories are related to each other. So if you choose one category the value of the other dropdowns should be set to the same selected value. But I also need the name of each category dropdown for validation.
This is how I created the category object:
const createCategory = () => {
let cat = {}
let nestedCat = {}
for (let it in item) {
for (let bt in item[it].buyertypes) {
cat2[item[it].buyertypes[bt].id] = ''
}
cat[item[it].id] = nestedCat
nestedCat = {}
}
return cat
}
const [category, setCategory] = useState(createCategory());
This is the initial object:
{
9871: { 1: "", 2: "", 3: "" }
9872: { 5: "", 6: "", 8: "" }
}
How I show and handle the ticket component
const handleQuantity = e => {
setQuantity({
...quantity,
[e.target.name]: e.target.value
});
}
const handleCategory = e => {
setCategory({
...category,
[e.target.name]: e.target.value
});
}
const ShowData = () => {
let ticketIem = []
for (let it in item) {
for (let bt in item[it].buyertypes) {
let buyerType = item[it].buyertypes[bt]
ticketIem.push({
'price': buyerType.prices, 'catId': item[it].id,
'qntId': item[it].buyertypes[bt].id
})
}
}
return (
<div>
{ticketIem.map((i, index) => (
<div key={index} >
<div>
<label>Number</label>
<select
value={quantity[i.qntId]}
onChange={handleQuantity}
name={i.qntId}
>
<option value={0}>0</option>
<option value={1}>1</option>
</select>
</div>
<div>
<label>Category</label>
<select
value={category[i.catId]}
onChange={handleCategory}
name={i.catId}
>
<option value="">Choose</option>
{categories.map((cat, index) => (
<option key={index} value={cat.id} name={i.qntId}>
{cat.description} {cat.value}
</option>
))}
</select>
</div>
</div>
))}
</div>
)
}
After selecting an option in the dropdown the category object will be set like:
{
9871: "11", 9872: "7"
}
but I expect:
{
9871: { 1: "11", 2: "11", 3: "11" }
9872: { 5: "7", 6: "7", 8: "7" }
}
If you want to set all the nested properties of a specific category or quantity then you'll need to also iterate the keys of those nested properties. Use Object.keys to get an array of the nested object's keys, and reduce them into a new object with the new value set for each key.
I suggest also using a functional state update since each update depends on the existing/current state as it is shallowly copied into the next state.
const handleQuantity = (e) => {
setQuantity((quantity) => ({
...quantity,
[e.target.name]: Object.keys(quantity[e.target.name]).reduce(
(obj, key) => ({
...obj,
[key]: e.target.value
}),
{}
)
}));
};
const handleCategory = (e) => {
setCategory((category) => ({
...category,
[e.target.name]: Object.keys(category[e.target.name]).reduce(
(obj, key) => ({
...obj,
[key]: e.target.value
}),
{}
)
}));
};
I'm having an issue where upon loading the page I can either sort the table by the "name" column (ascending or descending) - - OR - - use a searchbar to filter through the names of the employees. My issue is that once I've sorted alphabetically, the search/filter no longer works.
I'm very new to React (I'm sure that's very obvious by my code) so please let me know if there's something obvious I'm doing wrong. Thanks in advance!
import React, { Component } from "react";
import API from "../utils/API"
import EmployeeRow from "./EmployeeRow"
class TableMain extends Component {
state = {
result: [],
search: "",
sortOrder: "descending"
}
componentDidMount() {
API.search()
.then(results => {
console.log(results)
this.setState({
result: results.data.results.map((res, i) => ({
image: res.picture.large,
firstName: res.name.first,
lastName: res.name.last,
phone: res.phone,
email: res.email,
dob: res.dob.date,
key: i
})
)
})
})
};
filterResults = (results) => {
const value = this.state.search
const finalResult = results.filter((employee) => {
const lastName = employee.lastName.toLowerCase();
const firstName = employee.firstName.toLowerCase()
const fullName = firstName + " " + lastName
if (fullName.includes(value)) {
return employee
}
});
return finalResult
};
sortResults = (event) => {
const results = this.state.result
// const id = event.target.id
// if (id === 'name'){
// } else if (id === 'phone'){
// } else if (id === 'email'){
// }
if (this.state.sortOrder === "descending") {
results.sort((a, b) => {
if (a.firstName > b.firstName) {
return -1
}
return a.firstName > b.firstName ? 1 : 0
},
this.setState({ sortOrder: "ascending" }))
} else if (this.state.sortOrder === "ascending") {
results.sort((a, b) => {
if (a.firstName < b.firstName) {
return -1
}
return a.firstName > b.firstName ? 1 : 0
},
this.setState({ sortOrder: "descending" }))
}
console.log("RESULTS: ", results)
this.setState({
sortedResults: results,
isSorted: true
})
}
onChange = e => {
const value = e.target.value;
if (!value) {
this.setState({ isSearchEmpty: true });
} else {
this.setState({ search: e.target.value, isSearchEmpty: false });
}
}
render() {
// console.log("State", this.state)
let employeeResults = this.state.result
if (this.state.isSearchEmpty) {
employeeResults = this.state.result
} else {
employeeResults = this.filterResults(this.state.result)
}
if (this.state.isSorted) {
employeeResults = this.state.sortedResults
}
return (
<div>
<input label="Search" onChange={this.onChange} />
<div className="row">
<table style={{ width: "100%" }}>
<tbody>
<tr>
<th>Image</th>
<th style={{ cursor: "pointer" }} onClick={this.sortResults} id="name">Name</th>
<th id="phone">Phone</th>
<th id="email">Email</th>
<th id="dob">DOB</th>
</tr>
{[...employeeResults].map((item) =>
<EmployeeRow
image={item.image}
firstName={item.firstName}
lastName={item.lastName}
email={item.email}
phone={item.phone}
dob={item.dob}
key={item.key}
/>
)}
</tbody>
</table>
</div>
</div>
)}
}
export default TableMain;
The issue is:
if (this.state.isSorted) {
employeeResults = this.state.sortedResults;
}
When you sort, you set state.isSorted to true, however you never set it back to false once you have finished. When you then try to filter, you do the filter:
if (this.state.isSearchEmpty) {
employeeResults = this.state.result;
} else {
employeeResults = this.filterResults(this.state.result);
}
if (this.state.isSorted) { // this is never reset after sorting.
employeeResults = this.state.sortedResults;
}
But as this.state.isSorted is still true, you use the values in this.state.sortedResults again.
please let me know if there's something obvious I'm doing wrong
You are making this tricky for yourself, as you are filtering/sorting the same collection of data. That's why you need to perform the action in the render, as you are trying to maintain the original list for later usage.
If you seperate the list into two collections: original unmodified and a display list, you can always refer to the original list to perform filtering/sorting.
componentDidMount() {
API.search().then(results => {
const tableData = results.data.results.map((res, i) => ({
image: res.picture.large,
firstName: res.name.first,
lastName: res.name.last,
phone: res.phone,
email: res.email,
dob: res.dob.date,
key: i
}));
this.setState({ originalResults: tableData, displayResults: tableData });
});
}
Then filtering can be done, as soon as the onChange occurs:
onChange = e => {
const query = e.target.value;
this.setState(prevState => ({
displayResults:
query.length > 0
? this.filterResults(query, prevState.originalResults)
: prevState.originalResults
}));
};
and similarly for the sorting, which can be performed on the display-results rather than the whole which means you can now sort, filtered results.
I created an example here https://codesandbox.io/s/sad-cannon-d61z6
I stubbed out all the missing functionality.
I am trying to watch a field which have watch:true field.In other words I want add useEffect dynamically.I have one json (which is coming from server).I want to watch field value (which have property watch:true).using it's value I want to update other field value .
here is my code
https://codesandbox.io/s/crimson-violet-uxbzd
see this object have watch: true, so I need to watch or check if it value is changed or not
{
label: "First",
name: "first",
type: "select",
remove: true,
watch: true,
options: ["", "one", "two"]
},
if it's value is changed then call this function
const getSecondDropdownValue = function(value) {
return new Promise((resolve, reject) => {
if (value === "one") {
setTimeout(function() {
resolve(["three", "four"]);
}, 1000);
}
if (value === "two") {
setTimeout(function() {
resolve(["five", "six"]);
}, 1000);
}
});
};
any update?.
Check if the item has watch property, if it does pass getSecondDropdownValue to onChange event of the select option. something like
<select onChange={hasWatch ? getSecondDropdownValue : () => {}}>...</select>
Create a component that will render select options.
// options = array of list options
// onChange = onchange event handler
// name = name of the select
const Option = ({ options, onChange, name }) =>
(options.length && (
<select onChange={(e) => onChange(e.target.value, name)}>
{Array.isArray(options) &&
options.map((option, index) => (
<option value={option} key={option + index}>{option}</option>
))}
</select>
)) ||
false;
Add useState for storing the data from the api.
// initial data from the api
const [data, updateData] = useState(apiData);
// update select options and the list
const updateSelectData = (list, updated) => {
const index = list.findIndex(item => item.name === updated.name);
return [
...list.slice(0, index),
Object.assign({}, list[index], updated),
...list.slice(index + 1)
];
};
getSecondDropdownValue function
const getSecondDropdownValue = function(value, name) {
const updated = data.find(
item => item.dependentField && item.dependentField[0] === name
);
// return new Promise((resolve, reject) => {
if (value === "one") {
// setTimeout(function() {
// resolve(["three", "four"]);
// }, 1000);
updated.options = ["three", "four"];
}
if (value === "two") {
// setTimeout(function() {
// resolve(["five", "six"]);
// }, 1000);
updated.options = ["five", "six"];
}
// });
updateData(updateSelectData(data, updated));
};
Example
// Get a hook function
const {useState} = React;
const apiData = [
{
label: "First",
name: "first",
type: "select",
watch: true,
options: ["", "one", "two"]
},
{
label: "Second",
name: "second",
options: [],
dependentField: ["first"],
type: "select"
}
];
// option component
const Option = ({ options, onChange, name }) =>
(options.length && (
<select onChange={(e) => onChange(e.target.value, name)}>
{Array.isArray(options) &&
options.map((option, index) => (
<option value={option} key={option + index}>{option}</option>
))}
</select>
)) ||
false;
function App() {
const [data, updateData] = useState(apiData);
const updateSelectData = (list, updated) => {
const index = list.findIndex(item => item.name === updated.name);
return [
...list.slice(0, index),
Object.assign({}, list[index], updated),
...list.slice(index + 1)
];
};
const getSecondDropdownValue = function(value, name) {
const updated = data.find(
item => item.dependentField && item.dependentField[0] === name
);
// return new Promise((resolve, reject) => {
if (value === "one") {
// setTimeout(function() {
// resolve(["three", "four"]);
// }, 1000);
updated.options = ["three", "four"];
}
if (value === "two") {
// setTimeout(function() {
// resolve(["five", "six"]);
// }, 1000);
updated.options = ["five", "six"];
}
// });
updateData(updateSelectData(data, updated));
};
return (
<div className="App">
{data.map((options, index) => (
<Option
name={options.name}
key={index}
onChange={options.watch ? getSecondDropdownValue : () => {}}
options={options.options}
/>
))}
</div>
);
}
// Render it
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I have initialized some const, lets say A, using getDerivedStateFromProps. Now I want to update the value on some action using setState but it's not working.
constructor(props) {
super(props)
this.state = {
A: []
}
static getDerivedStateFromProps(nextProps, prevState) {
const A = nextProps.A
return {
A
}
}
handleDragStart(e,data) {
e.dataTransfer.setData('item', data)
}
handleDragOver(e) {
e.preventDefault()
}
handleDrop(e, cat) {
const id = e.dataTransfer.getData('item')
const item = find(propEq('id', Number(id)), this.state.A)
const data = {
...item.data,
category: cat,
}
const val = {
...item,
data
}
this.setState({
A: item,
})
}
}
**Listing the items and Drag and Drop to Categorize**
{this.state.A.map((item, index) => (
<ListRow
key={`lm${index}`}
draggable
name={item.name ? item.name : ''}
description={item.data ? item.data.description : ''}
type={item.data ? item.data.target_types : ''}
source={item.data ? item.data.source : ''}
stars={item.data ? item.data.stars : []}
onDragStart={e => this.handleDragStart(e, item.id)}
onDragOver={e => this.handleDragOver(e)}
onDrop={e => this.handleDrop(e, 'process')}
onModal={() => this.handleToggleModal(item)}
/>
))}
I expect the value of A to be an item from HandleDrop but it's returning the same value that is loaded from getDerivedStateFromProps.
Here's how I solved this problem.
I used componentDidUpdate instead of getDerivedStatesFromProps.
componentDidUpdate(prevProps) {
if (!equals(this.props.A, prevPropsA)) {
const A = this.props.A
this.setState({
A
})
}
}
And the handleDrop function as
handleDrop(e, cat) {
const id = e.dataTransfer.getData('item')
const item = find(propEq('id', Number(id)), this.state.A)
const data = {
....data,
category: cat,
}
const val = {
...quota,
data
}
let {A} = this.state
const index = findIndex(propEq('id', Number(id)), A)
if (!equals(index, -1)) {
A = update(index, val, A)
}
this.setState({
A
})
}
Thank you very much for all of your help. Any suggestions or feedback for optimizing this sol will be highly appreciated. Thanks