简体   繁体   English

如何使用JavaScript创建包含多个可选参数的搜索?

[英]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 . 然后在PostContainerprops.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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM