【问题标题】:How can I create a search with multiple, optional, parameters using JavaScript?如何使用 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链接来重现这个问题吗?

标签: javascript reactjs search filter


【解决方案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>
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2017-10-01
              • 2018-01-27
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多