Reputation: 337
I have a component that renders selects to the screen so the user can order data dynamically.
The default states is :
state = {
sortBy: [ { author: 'asc' } ]
};
the first object above refers to the first row of selects. One select changes object key and the next one object value.
But when I change the first select of the second row the states changes to:
state = {
sortBy: [ { author: 'asc' }, {} ]
};
And crashes my sort function with TypeError: Cannot read property 'toLowerCase' of undefined.
The only way to make it work is by changing first the select that set object value, making the state change to:
state = {
sortBy: [ { author: 'asc' }, {undefined: 'asc'} ]
};
and then change the object key.
Component that renders select to screen:
const TYPES = [
{ slug: 'title', description: 'Title' },
{ slug: 'author', description: 'Author' },
{ slug: 'editionYear', description: 'Edition Year' }
];
class BookListSorter extends React.Component {
state = {
sortBy: [ { author: 'asc' } ]
};
getSortByKeyForIndex = (index) => Object.keys(this.state.sortBy[index] || {})[0];
getSortByValueForIndex = (index) => Object.values(this.state.sortBy[index] || {})[0];
changeSort = (key, index) => (e) => {
const { target } = e;
this.setState(({ sortBy }) => {
const type = key === 'type' ? target.value : this.getSortByKeyForIndex(index);
const direction = key === 'direction' ? target.value : this.getSortByValueForIndex(index);
return type || direction ? sortBy.splice(index, 1, { [type]: direction }) : sortBy.splice(index, 1);
});
};
filterTypes = (index) => ({ slug }) => {
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;
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>
<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="asc">None</option>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<br />
</div>
))}
<br />
<Books order={sortBy} />
</div>
);
}
}
Book component that receives BookListSort state as props and use it on order function:
class Books extends Component {
state = {
order: this.props.order
};
componentWillReceiveProps({ order }) {
this.setState({ ...this.state, order });
}
render() {
return (
<div>
<h2>Books</h2>
<Query query={BOOKS_QUERY}>
{({ data, loading, error }) => {
if (loading) return <h4>Loading...</h4>;
if (error) return <p>Error</p>;
// Filter
const cleanedSort = this.state.order.filter(
(obj) => Object.entries(obj).length !== 0 && obj.constructor === Object
);
const orderedBooks = orderBooksBy(data.books, cleanedSort);
return orderedBooks.map((book) => <Book key={book.id} {...book} />);
}}
</Query>
</div>
);
}
}
Order Function:
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 orderBooksBy(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;
});
}
export default orderBooksBy;
Upvotes: 0
Views: 339
Reputation: 15851
I would resolve it with a filter. When retrieving sortBy array you can do the following:
// State
state = {
sortBy: [ { author: 'asc' }, {} ]
};
// Filter
const cleanedSort = this.state.sortBy.filter(obj => Object.entries(obj).length !== 0 && obj.constructor === Object);
// Get it cleaned
console.log(cleanedSort);
Upvotes: 2