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

Posted

技术标签:

【中文标题】如何使用 JavaScript 创建具有多个可选参数的搜索?【英文标题】:How can I create a search with multiple, optional, parameters using JavaScript? 【发布时间】:2019-11-18 11:58:08 【问题描述】:

我目前有什么“作品”,但是每个参数都取决于最后一个。我的目标是允许用户使用任意数量的搜索字段来过滤帖子,但我似乎无法理解如何实际执行它。

搜索字段的代码:

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.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);

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

]

从上面可以看出,我试图只注释掉下拉列表的“禁用”属性,但这会导致它完全停止作为过滤器工作,并返回所有结果,无论选择如何。这是由于我检查每个过滤器的三元运算符混乱造成的。有没有更好的方法可以做到这一点?

【问题讨论】:

你能发布一个codeandbox/stackblitz链接来重现这个问题吗? 【参考方案1】:

即使@Nina Lisitsinskaya 的回答是正确的,我也不会有大量的ifs 列表,而我只是完成了过滤连接。

这样添加另一种过滤方式更容易且可读性强。解决方案虽然类似。

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)

    ...


当然以后你要在 JSX 表达式中使用类似 this 的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>

【讨论】:

【参考方案2】:

这可以通过一个过滤器功能来实现。

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 />);

列表中只有一个循环。没有很多 if 和 else 或三元运算符的麻烦。只是根据需要排序过滤方式。

【讨论】:

【参考方案3】:

我认为你可以使用 Lodash_.filter 收集方法来帮助你:

Lodash 文档:https://lodash.com/docs/4.17.15#filter

多输入搜索

/*
 * `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;

渲染方法

    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()));
    );
  );

渲染方法

    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>
    );
  

【讨论】:

【参考方案4】:

尝试以下操作(代码 cmets 中的说明):

// 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 正在传递一个作为对象的单个 prop post。如果您传播该帖子对象使其成为道具,您可能可以在该组件中简化道具访问相当多:

filteredPosts.map(post => <PostContainer key=post.id ...post />)

然后在PostContainer 中,props.post.id 将变为props.id。 props api 变得更简单,组件将更容易优化(如果有必要的话)。

【讨论】:

【参考方案5】:

您永远不应该在您的 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。如果你不打算改变你的服务方法,至少在你解析的promisecomponentDidMount中添加这个帖子过滤,但我强烈建议不要在render方法中做这样的事情。

【讨论】:

【参考方案6】:

根本没有必要在 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);
  

  ...

那么你可以在 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>

【讨论】:

以上是关于如何使用 JavaScript 创建具有多个可选参数的搜索?的主要内容,如果未能解决你的问题,请参考以下文章

SQL - 创建具有多个可选参数的存储过程

如何使用 2 个位置参数在 argparse 上打印帮助界面?

如何创建具有多个位置参数的 argparse 互斥组?

具有多个参数的函数中的 Javascript 默认参数

Pig Join - 如何连接具有多个字段的两个表,其中键中的一个字段是可选的?

具有多个可选参数的控制台输出