Как создать поиск с несколькими необязательными параметрами с помощью JavaScript?

То, что у меня сейчас есть, «работает», однако каждый параметр зависит от последнего. Моя цель состояла в том, чтобы позволить пользователю использовать любое количество полей поиска для фильтрации сообщений, но, похоже, я не могу понять, как это на самом деле выполнить.

Код для полей поиска:

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

}]

Как видно выше, я попытался просто закомментировать атрибут «Отключено» раскрывающегося списка, но это привело к тому, что он полностью перестал работать как фильтр и возвращает все результаты независимо от выбора. Это вызвано моим беспорядком тернарных операторов, проверяющих каждый фильтр. Есть ли лучший способ сделать это?

Можете ли вы опубликовать ссылку на codeandbox/stackblitz, воспроизводящую проблему?

Munim Munna 22.07.2019 17:11
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
9
1
2 089
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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);
  }

  ...

Затем вы можете просто использовать 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 или тернарными операторами. Просто заказал способ фильтрации в соответствии с необходимостью.

Другие вопросы по теме