То, что у меня сейчас есть, «работает», однако каждый параметр зависит от последнего. Моя цель состояла в том, чтобы позволить пользователю использовать любое количество полей поиска для фильтрации сообщений, но, похоже, я не могу понять, как это на самом деле выполнить.
Код для полей поиска:
import React from "react";
import { Input, DropDown } from "../Form";
import "./index.css";
function Sidebar(props) {
return (
<div className = "sidebar-container">
<p>Search Posts: {props.carMake}</p>
<div className = "field-wrap">
<Input
value = {props.carMake}
onChange = {props.handleInputChange}
name = "carMake"
type = "text"
placeholder = "Manufacturer"
/>
</div>
<div className = "field-wrap">
<Input
value = {props.carModel}
onChange = {props.handleInputChange}
disabled = {!props.carMake}
name = "carModel"
type = "text"
placeholder = "Model"
/>
</div>
<div className = "field-wrap">
<Input
disabled = {!props.carModel || !props.carMake}
value = {props.carYear}
onChange = {props.handleInputChange}
name = "carYear"
type = "text"
placeholder = "Year"
/>
</div>
<div className = "field-wrap">
<DropDown
//disabled = {!props.carModel || !props.carMake || !props.carYear}
value = {props.category}
onChange = {props.handleInputChange}
name = "category"
type = "text"
id = "category"
>
<option>Select a category...</option>
<option>Brakes</option>
<option>Drivetrain</option>
<option>Engine</option>
<option>Exhaust</option>
<option>Exterior</option>
<option>Intake</option>
<option>Interior</option>
<option>Lights</option>
<option>Suspension</option>
<option>Wheels & Tires</option>
</DropDown>
</div>
</div>
);
}
export default Sidebar;
Вот код для родительского компонента (где данные фактически фильтруются):
import React, { Component } from 'react';
import Sidebar from '../../components/Sidebar';
import API from '../../utils/API';
import PostContainer from '../../components/PostContainer';
import { withRouter } from 'react-router';
import axios from 'axios';
import './index.css';
class Posts extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
carMake: '',
carModel: '',
carYear: '',
category: 'Select A Category...'
};
this.signal = axios.CancelToken.source();
}
componentDidMount() {
API.getAllPosts({
cancelToken: this.signal.token
})
.then(resp => {
this.setState({
posts: resp.data
});
})
.catch(function(error) {
if (axios.isCancel(error)) {
console.info('Error: ', error.message);
} else {
console.info(error);
}
});
}
componentWillUnmount() {
this.signal.cancel('Api is being canceled');
}
handleInputChange = event => {
const { name, value } = event.target;
this.setState({
[name]: value
});
};
handleFormSubmit = event => {
event.preventDefault();
console.info('Form Submitted');
};
render() {
const { carMake, carModel, carYear, category, posts } = this.state;
const filterMake = posts.filter(
post => post.carMake.toLowerCase().indexOf(carMake.toLowerCase()) !== -1
);
const filterModel = posts.filter(
post => post.carModel.toLowerCase().indexOf(carModel.toLowerCase()) !== -1
);
const filterYear = posts.filter(
post => post.carYear.toString().indexOf(carYear.toString()) !== -1
);
const filterCategory = posts.filter(
post => post.category.toLowerCase().indexOf(category.toLowerCase()) !== -1
);
return (
<div className='container-fluid'>
<div className='row'>
<div className='col-xl-2 col-lg-3 col-md-4 col-sm-12'>
<Sidebar
carMake = {carMake}
carModel = {carModel}
carYear = {carYear}
category = {category}
handleInputChange = {this.handleInputChange}
handleFormSubmit = {event => {
event.preventDefault();
this.handleFormSubmit(event);
}}
/>
</div>
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{carMake && carModel && carYear && category
? filterCategory.map(post => (
<PostContainer post = {post} key = {post.id} />
))
: carMake && carModel && carYear
? filterYear.map(post => (
<PostContainer post = {post} key = {post.id} />
))
: carMake && carModel
? filterModel.map(post => (
<PostContainer post = {post} key = {post.id} />
))
: carMake
? filterMake.map(post => (
<PostContainer post = {post} key = {post.id} />
))
: posts.map(post => <PostContainer post = {post} key = {post.id} />)}
</div>
</div>
</div>
);
}
}
export default withRouter(Posts);
Данные, возвращаемые API, представлены в виде массива объектов следующим образом:
[{
"id":4,
"title":"1995 Toyota Supra",
"desc":"asdf",
"itemImg":"https://i.imgur.com/zsd7N8M.jpg",
"price":32546,
"carYear":1995,
"carMake":"Toyota",
"carModel":"Supra",
"location":"Phoenix, AZ",
"category":"Exhaust",
"createdAt":"2019-07-09T00:00:46.000Z",
"updatedAt":"2019-07-09T00:00:46.000Z",
"UserId":1
},{
"id":3,
"title":"Trash",
"desc":"sdfasdf",
"itemImg":"https://i.imgur.com/rcyWOQG.jpg",
"price":2345,
"carYear":2009,
"carMake":"Yes",
"carModel":"Ayylmao",
"location":"asdf",
"category":"Drivetrain",
"createdAt":"2019-07-08T23:33:04.000Z",
"updatedAt":"2019-07-08T23:33:04.000Z",
"UserId":1
}]
Как видно выше, я попытался просто закомментировать атрибут «Отключено» раскрывающегося списка, но это привело к тому, что он полностью перестал работать как фильтр и возвращает все результаты независимо от выбора. Это вызвано моим беспорядком тернарных операторов, проверяющих каждый фильтр. Есть ли лучший способ сделать это?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


В JSX вообще нет необходимости использовать ужасные огромные тернарные операторы. Сначала вы можете фильтровать коллекцию последовательно каждым фильтром:
render() {
const { carMake, carModel, carYear, category, posts } = this.state;
let filtered = [...posts];
if (carMake) {
filtered = filtered.filter(post => post.carMake.toLowerCase().indexOf(carMake.toLowerCase()) !== -1);
}
if (carModel) {
filtered = filtered.filter(post => post.carModel.toLowerCase().indexOf(carModel.toLowerCase()) !== -1);
}
if (carYear) {
filtered = filtered.filter(post => post.carYear.toString().indexOf(carYear.toString()) !== -1);
}
if (category) {
filtered = filtered.filter(post => post.category.toLowerCase().indexOf(category.toLowerCase()) !== -1);
}
...
Затем вы можете просто использовать filtered в выражении JSX:
...
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{filtered.map(post => <PostContainer post = {post} key = {post.id} />)}
</div>
Вы никогда не должны делать такие вычисления в своем методе render - он должен работать с чистым вычислением state/props. В основном фильтрация должна происходить на вашем backend, но если вы хотите фильтровать на frontend, вы должны перенести логику фильтрации в сервисный метод, примерно так:
function getPosts({ cancelToken, filter }) {
// first fetch your posts
// const posts = ...
const { carMake, carModel, carYear, category } = filter;
let filtered = [];
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
let add = true;
if (carMake && add) {
add = post.carMake.toLowerCase().indexOf(carMake.toLowerCase()) !== -1;
}
if (carModel && add) {
add = post.carModel.toLowerCase().indexOf(carModel.toLowerCase()) !== -1;
}
if (carYear && add) {
add = post.carYear.toLowerCase().indexOf(carYear.toLowerCase()) !== -1;
}
if (category && add) {
add = post.category.toLowerCase().indexOf(category.toLowerCase()) !== -1;
}
if (add) {
filtered.push(post);
}
}
return filtered;
}
For loop используется, потому что при таком подходе вы повторяете posts только один раз. Если вы не собираетесь менять свой метод обслуживания, то хотя бы добавьте эту фильтрацию постов внутри вашего разрешенного promise в componentDidMount, но я настоятельно не советую делать такие вещи в render методе.
Попробуйте следующее (инструкции в комментариях к коду):
// 1. don't default category to a placeholder.
// If the value is empty it will default to your empty option,
// which shows the placeholder text in the dropdown.
this.state = {
posts: [],
carMake: '',
carModel: '',
carYear: '',
category: ''
}
// 2. write a method to filter your posts and do the filtering in a single pass.
getFilteredPosts = () => {
const { posts, ...filters } = this.state
// get a set of filters that actually have values
const activeFilters = Object.entries(filters).filter(([key, value]) => !!value)
// return all posts if no filters
if (!activeFilters.length) return posts
return posts.filter(post => {
// check all the active filters
// we're using a traditional for loop so we can exit as soon as the first check fails
for (let i; i > activeFilters.length; i++) {
const [key, value] = activeFilters[i]
// bail on the first failure
if (post[key].toLowerCase().indexOf(value.toLowerCase()) < 0) {
return false
}
}
// all filters passed
return true
})
}
// 3. Simplify render
render() {
// destructure filters so you can just spread them into SideBar
const { posts, ...filters } = this.state
const filteredPosts = this.getFilteredPosts()
return (
<div className='container-fluid'>
<div className='row'>
<div className='col-xl-2 col-lg-3 col-md-4 col-sm-12'>
<Sidebar
{...filters}
handleInputChange = {this.handleInputChange}
handleFormSubmit = {this.handleFormSubmit}
/>
</div>
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{filteredPosts.map(post => <PostContainer post = {post} key = {post.id} />)}
</div>
</div>
</div>
)
}
Еще одна вещь, которую следует учитывать, это то, что PostContainer передается один реквизит post, который является объектом. Вероятно, вы могли бы немного упростить доступ к свойствам в этом компоненте, если бы вы расширили этот объект записи, чтобы он стал реквизитом:
{filteredPosts.map(post => <PostContainer key = {post.id} {...post} />)}
Тогда в PostContainerprops.post.id станет props.id. API реквизита становится проще, а компонент легче оптимизировать (если это будет необходимо).
Я думаю, вы можете использовать метод сбора Lodash _.filter, чтобы помочь вам:
Документация Lodash: https://lodash.com/docs/4.17.15#фильтр
Поиск по нескольким входам
/*
* `searchOption` is something like: { carMake: 'Yes', carYear: 2009 }
*/
function filterData(data = [], searchOption = {}) {
let filteredData = Array.from(data); // clone data
// Loop through every search key-value and filter them
Object.entries(searchOption).forEach(([key, value]) => {
// Ignore `undefined` value
if (value) {
filteredData = _.filter(filteredData, [key, value]);
}
});
// Return filtered data
return filteredData;
}
render method
return (
<div className='container-fluid'>
<div className='row'>
<div className='col-xl-2 col-lg-3 col-md-4 col-sm-12'>
<Sidebar
carMake = {carMake}
carModel = {carModel}
carYear = {carYear}
category = {category}
handleInputChange = {this.handleInputChange}
handleFormSubmit = {event => {
event.preventDefault();
this.handleFormSubmit(event);
}}
/>
</div>
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{
filterData(post, { carMake, carModel, carYear, category }).map(post => (
<PostContainer post = {post} key = {post.id} />
))
}
</div>
</div>
</div>
);
}
}
Поиск по одному входу
Или у вас может быть одно поле ввода поиска, и оно будет фильтровать все данные.
function filterData(data = [], searchString = '') {
return _.filter(data, obj => {
// Go through each set and see if any of the value contains the search string
return Object.values(obj).some(value => {
// Stringify the value (so that we can search numbers, boolean, etc.)
return `${value}`.toLowerCase().includes(searchString.toLowerCase()));
});
});
}
render method
return (
<div className='container-fluid'>
<div className='row'>
<div className='col-xl-2 col-lg-3 col-md-4 col-sm-12'>
<input
onChange = {this.handleInputChange}
value = {this.state.searchString}
/>
</div>
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{filterData(posts, searchString).map(post => <PostContainer post = {post} key = {post.id} />)}
</div>
</div>
</div>
);
}
}
Несмотря на то, что ответ от @Nina Lisitsinskaya правильный, у меня не было бы огромного списка if, и я просто сделал бы все с конкатенацией фильтров.
Таким образом, добавление другого способа фильтрации становится проще и читабельнее. Хотя решение аналогичное.
render() {
const { carMake = '', carModel = '', carYear = '', category = '', posts } = this.state;
let filtered = [...posts];
filtered = filtered
.filter(post => post.carMake.toLowerCase().indexOf(carMake.toLowerCase()) !== -1)
.filter(post => post.carModel.toLowerCase().indexOf(carModel.toLowerCase()) !== -1)
.filter(post => post.carYear.toString().indexOf(carYear.toString()) !== -1)
.filter(post => post.category.toLowerCase().indexOf(category.toLowerCase()) !== -1)
...
}
Конечно, позже вы захотите использовать filtered аналогично этому в выражении JSX, иначе нечего показывать.
...
<div className='col-xl-8 col-lg-7 col-md-6 col-sm-12 offset-md-1'>
{filtered.map(post => <PostContainer post = {post} key = {post.id} />)}
</div>
Это может быть достигнуто с помощью одной единственной функции фильтра.
render() {
const { carMake, carModel, carYear, category, posts } = this.state;
const filteredPost = posts.filter(post =>
post.category.toLowerCase().includes(category.toLowerCase()) &&
post.carYear === carYear &&
post.carModel.toLowerCase().includes(carModel.toLowerCase()) &&
post.carMake.toLowerCase().includes(carMake.toLowerCase())
);
return
...
filteredPost.map(post => <PostContainer post = {post} key = {post.id} />);
}
Всего один цикл по списку. Никаких хлопот с множеством ifs и else или тернарными операторами. Просто заказал способ фильтрации в соответствии с необходимостью.
Можете ли вы опубликовать ссылку на codeandbox/stackblitz, воспроизводящую проблему?