admin管理员组

文章数量:1391798

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 ponent (Where the data is actually filtered):

import React, { Component } from 'react';
import Sidebar from '../../ponents/Sidebar';
import API from '../../utils/API';
import PostContainer from '../../ponents/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();
  }

  ponentDidMount() {
    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);
        }
      });
  }

  ponentWillUnmount() {
    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:

[{

"id":4,
"title":"1995 Toyota Supra",
"desc":"asdf",
"itemImg":".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":".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 ment out the dropdown's "Disabled" attribute, but that causes it to stop working as a filter pletely, 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?

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 ponent (Where the data is actually filtered):

import React, { Component } from 'react';
import Sidebar from '../../ponents/Sidebar';
import API from '../../utils/API';
import PostContainer from '../../ponents/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();
  }

  ponentDidMount() {
    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);
        }
      });
  }

  ponentWillUnmount() {
    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:

[{

"id":4,
"title":"1995 Toyota Supra",
"desc":"asdf",
"itemImg":"https://i.imgur./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./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 ment out the dropdown's "Disabled" attribute, but that causes it to stop working as a filter pletely, 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?

Share Improve this question asked Jul 9, 2019 at 0:12 RevircsRevircs 1,3523 gold badges14 silver badges25 bronze badges 1
  • Can you post a codesandbox/stackblitz link reproducing the issue? – Munim Munna Commented Jul 22, 2019 at 15:11
Add a ment  | 

6 Answers 6

Reset to default 6 +25

Even though the answer from @Nina Lisitsinskaya is correct, I would not have a huge list of ifs and have all just done with a filter concatenation.

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.

  ...

  <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. 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:

  ...

  <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. 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:

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. If you are not going to change your service method, at least add this post filtering inside of your resolved promise in ponentDidMount, but I stronlgy advise not to do such things in render method.

Try the following (instructions in code ments):

// 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. You could probably simplify prop access quite a bit in that ponent 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 bee props.id. The props api bees simpler and the ponent will be easier to optimize (if that bees necessary).

I think you can use Lodash _.filter collection method to help you:

Lodash documentation: https://lodash./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. Just ordered way of filtering according to the need.

本文标签: