[英]How can I create a search with multiple, optional, parameters using JavaScript?
What I currently have "works", however each parameter depends on the last. 我目前所拥有的“工作”,但每个参数取决于最后一个。 My goal was to allow the user to use any amount of the search fields to filter through posts, but can't seem to be able to wrap my head around how to actually execute it.
我的目标是允许用户使用任意数量的搜索字段来过滤帖子,但似乎无法理解如何实际执行它。
Code for the search fields: 搜索字段的代码:
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;
Here is the code for the parent component (Where the data is actually filtered): 以下是父组件的代码(实际过滤数据的位置):
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.log('Error: ', error.message);
} else {
console.log(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.log('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);
The data returned from the API is in the form of an array of objects as follows: 从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
}]
As can be seen above, I had attempted to just comment out the dropdown's "Disabled" attribute, but that causes it to stop working as a filter completely, and returns all results no matter the selection. 从上面可以看出,我曾尝试只注释下拉列表的“已禁用”属性,但这会导致它完全停止作为过滤器工作,并返回所有结果,无论选择如何。 This is caused by my mess of ternary operators checking for each filter.
这是由于我的三元运算符混乱检查每个过滤器造成的。 Is there a better way I could be doing this?
有没有更好的方法可以做到这一点?
Even though the answer from @Nina Lisitsinskaya is correct, I would not have a huge list of if
s and have all just done with a filter concatenation. 即使@Nina Lisitsinskaya的答案是正确的,我也不会有一个巨大的
if
列表,并且只是完成了过滤器连接。
That way adding another way to filter is easier and quite readable. 这样,添加另一种过滤方式更容易,更易读。 The solution though is similar.
解决方案虽然类似。
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)
...
}
Of course then later you want to use filtered
similarly to this in the JSX expression, otherwise there is nothing to show. 当然后来你想在JSX表达式中使用与此类似的
filtered
,否则没有什么可以显示。
...
<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>
There is no need to use terrible huge ternary operators in the JSX at all. 根本不需要在JSX中使用可怕的巨大的三元运算符。 First you can filter the collection sequentially with each filter:
首先,您可以使用每个过滤器按顺序过滤集合:
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);
}
...
Then you can just use filtered
in the JSX expression: 然后你可以在JSX表达式中使用
filtered
:
...
<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>
You should never do such calculations in your render
method - it should work with clean calculated state/props
. 你永远不应该在你的
render
方法中进行这样的计算 - 它应该使用干净的计算state/props
。 Basically filtering should happen on your backend
, but if you want to filter on frontend
you should move filtering logic to service method, something like this: 基本上过滤应该在你的
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
is used because with this approach you iterate posts
only once. For loop
是因为使用此方法只迭代一次posts
。 If you are not going to change your service method, at least add this post filtering inside of your resolved promise
in componentDidMount
, but I stronlgy advise not to do such things in render
method. 如果您不打算更改服务方法,至少在
componentDidMount
中的已解析的promise
中添加此post过滤,但我强烈建议不要在render
方法中执行此类操作。
Try the following (instructions in code comments): 请尝试以下(代码注释中的说明):
// 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>
)
}
Another thing to consider is that PostContainer
is being passed a single prop post
that is an object. 另一个要考虑的是,
PostContainer
正在传递一个道具post
就是一个对象。 You could probably simplify prop access quite a bit in that component if you spread that post object so it became the props: 如果您传播该帖子对象使其成为道具,您可以在该组件中简化道具访问:
{filteredPosts.map(post => <PostContainer key={post.id} {...post} />)}
Then in PostContainer
, props.post.id
would become props.id
. 然后在
PostContainer
, props.post.id
将成为props.id
。 The props api becomes simpler and the component will be easier to optimize (if that becomes necessary). 道具api变得更简单,组件将更容易优化(如果有必要)。
I think you can use Lodash _.filter
collection method to help you: 我想你可以使用Lodash
_.filter
收集方法来帮助你:
Lodash documentation: https://lodash.com/docs/4.17.15#filter Lodash文档: https ://lodash.com/docs/4.17.15#filter
Multiple inputs search 多输入搜索
/*
* `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>
);
}
}
Single input search 单输入搜索
Or you can have one single search input field, and that will filter the whole data 或者,您可以拥有一个搜索输入字段,这将过滤整个数据
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>
);
}
}
This can be achieved with one single filter function. 这可以通过一个单一的过滤功能来实现。
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} />);
}
Just one single loop through the list. 通过列表只需一个循环。 No hassle of lots of ifs and else, or ternary operators.
没有很多ifs和其他三元运算符的麻烦。 Just ordered way of filtering according to the need.
只是根据需要订购过滤方式。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.