Home > Back-end >  I'm trying to filter an array of Objects simultaneosly by number and by string value
I'm trying to filter an array of Objects simultaneosly by number and by string value

Time:11-06

i have a little problem in my App ! it's a simple App that shows some restaurant tables and let you filter these tables by number of people eating (capacity) and the preferred place (Inside, Patio, Bar)

I have a problem specifically with this filter functionality: i'm able to filter the tables by Place, but when i try to simultaneosly filter the Tables by Capacity it won't work (it'll forget the Place filter, and just filter them by Capacity). I'll explain the user flow i used:

  1. I click on the Place select and choose the option 'Patio'
  2. At this point 2 tables are showed; one is for 5 people, the other for 9 people
  3. i click on the Capacity filter, choosing a table for 9 persons.
  4. the App will forget the 'Patio' value and it'll also show a table with Location ' Bar' (and that's the part i'm trying to fix, i want the App to remember 'Patio' and only showing that one)
     const defaultState : any = {
            tables: [],
            tablesFiltered: [],
            bookings: [],
            error: null,
            loading: false,
        }
         case FILTER_TABLE:
                        return {
                            ...state,
                            tablesFiltered: action.payload,
                        }

this is my action

export const filterTables = (filteredTable: tableI[]) => {
    return (dispatch: (arg0: { type: string; payload?: unknown; }) => void) =>
    dispatch({type: FILTER_TABLE, payload: filteredTable})
}

finally, the component where the logic of the filter is:

TableFilter.tsx

import { Form } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux';
import { filterTables } from '../../store/actions';
import { tableI } from '../../Interfaces';

const TableFilter: React.FC = () => {
  const tables: tableI[] = useSelector((state: any) => state.tables.tables);

  const dispatch = useDispatch();

  const handleChange = (event: any) => {
    const locationFilter: string = event.target.value;
    const capacityFilter: any = event.target.value;

    // console.log(typeof capacityFilter);

    const filteredArr = tables.filter(
      (table) => table.location === locationFilter
    );

    // console.log(filteredArr);
    dispatch(filterTables(filteredArr));
  };

  const changeCapacity = (event: any) => {
    // const locationFilter: string = event.target.value;
    const capacityFilter: any = event.target.value;

    // console.log(typeof capacityFilter);

    const filteredArr = tables.filter(
      (table) => table.capacity >= Number.parseInt(capacityFilter)
    );

    // console.log(filteredArr);
    dispatch(filterTables(filteredArr));
  };

  const locations: string[] = tables.map((table) => table.location);
  const capacity: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];

  const uniqueLocations = [...new Set(locations)];
return (
    <Form>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Select a table</Form.Label>
        <Form.Control
          onChange={handleChange}
          as="select"
          aria-label="form-select-location"
        >
          <option>Select your table's location</option>
          {uniqueLocations &&
            uniqueLocations.map((location: string, index: number) => (
              <option aria-label="location" key={index} value={location}>
                {location}
              </option>
            ))}
        </Form.Control>
      </Form.Group>

      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Select the capacity of your table</Form.Label>
        <Form.Control
          onChange={changeCapacity}
          as="select"
          aria-label="form-select-capacity"
        >
          <option>Number of persons sitting </option>
          {capacity &&
            capacity.map((capacity: number, index: number) => (
              <option aria-label="capacity" key={index} value={capacity}>
                {capacity}
              </option>
            ))}
        </Form.Control>
      </Form.Group>
    </Form>
  );
};

export default TableFilter;

here's my GitHub repo:

https://github.com/miki-miko/booking-system-testing

CodePudding user response:

You could take an array of all filters and filter the data by all filters, you actually have.

For example take two filters, like

capacity = n => ({ capacity }) => capacity >= n;
place = p => ({ place }) => place === p;

Then put them ino an array. It does not matter, if you use only one or more, if necessary.

filters = [
    capacity(2),
    place('Inside')
]

As result, it returns objects where all filters return true, or at least a truthy return value.

This one with Array#every returns the objects where all conditions are true.

result = data.filter(o => filters.every(fn => fn(o))); // all

If only one constraint has to be true, take Array#some instead.

result = data.filter(o => filters.some(fn => fn(o))); // exists

CodePudding user response:

You should filter the data in your selector since it is derived data and you should not store such data in your redux state. Here is a simple example of how you could do this:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;

const initialState = {
  tables: ['A', 'B', 'C', 'D', 'E'].flatMap((place) =>
    [...new Array(5)].map((_, index) => ({
      place,
      capacity: index   1,
    }))
  ), //not sure if filter needs to be here, is is shared
  // by multiple components in your application?
  filter: {
    capacity: 'all',
    place: 'all',
  },
};
//action types
const SET_FILTER = 'SET_FILTER';
//action creators
const setFilter = (key, value) => ({
  type: SET_FILTER,
  payload: { key, value },
});
const reducer = (state, { type, payload }) => {
  if (type === SET_FILTER) {
    const { key, value } = payload;
    return {
      ...state,
      filter: {
        ...state.filter,
        [key]: value,
      },
    };
  }
  return state;
};
//selectors
const selectTables = (state) => state.tables;
const selectFilter = (state) => state.filter;
const selectPlaces = createSelector(
  [selectTables],
  (tables) => [...new Set(tables.map(({ place }) => place))]
);
const selectCapacities = createSelector(
  [selectTables],
  (tables) => [
    ...new Set(tables.map(({ capacity }) => capacity)),
  ]
);
//return true if value is all, this is specific to the filer value
//  if the value is "all" then return true
//When passing a function to this it returns a function that takes a value
//  when calling that function with a value it returns a function that
//  takes an item
const notIfAll = (fn) => (value) => (item) =>
  value === 'all' ? true : fn(value, item);
//specific filter
const capacityBiggerThan = notIfAll(
  //could do more abstraction here with getProp, and re usable compare
  //  functions but leave this out for simplicity
  (value, item) => item.capacity >= Number(value)
);
const isPlace = notIfAll(
  (value, item) => value === item.place
);
//Apply multiple filter functions, it receives an array of filter functions
//  and returns a function that receives an item as parameter, when the item
//  is passed to this function it will call all filter functions passing this item
const mergeFilters = (filterFunctions) => (item) =>
  filterFunctions.every((filterFunction) =>
    filterFunction(item)
  );
//select filtered data
const selectFilterData = createSelector(
  [selectTables, selectFilter],
  (tables, { place, capacity }) => {
    return tables.filter(
      mergeFilters([
        isPlace(place),
        capacityBiggerThan(capacity),
      ])
    );
  }
);
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      () => (next) => (action) => next(action)
    )
  )
);
const FilterSelect = React.memo(function FilterSelect({
  filterKey,
  value,
  values,
}) {
  const dispatch = useDispatch();
  return (
    <select
      value={value}
      onChange={(e) =>
        dispatch(setFilter(filterKey, e.target.value))
      }
    >
      <option value="all">all</option>
      {values.map((place) => (
        <option key={place} value={place}>
          {place}
        </option>
      ))}
    </select>
  );
});
const App = () => {
  const { capacity, place } = useSelector(selectFilter);
  const places = useSelector(selectPlaces);
  const capacities = useSelector(selectCapacities);
  const filterData = useSelector(selectFilterData);
  return (
    <div>
      <FilterSelect
        filterKey="place"
        value={place}
        values={places}
      />
      <FilterSelect
        filterKey="capacity"
        value={capacity}
        values={capacities}
      />
      <div>
        <h1>result</h1>
        <pre>
          {JSON.stringify(filterData, undefined, 2)}
        </pre>
      </div>
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>


<div id="root"></div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

The code may be confusing since there are functions that get a function passed to it and return a function that when called with a value will return yet another function. You could rewrite the filter in the following way but it would not be re usable and more difficult to extend:

const selectFilterData = createSelector(
  [selectTables, selectFilter],
  (tables, { place, capacity }) => {
    //just put all logic in one function, easier to read but repeating logic
    //  and more difficult to maintain if many values are used or rules change
    return tables.filter((item) => {
      const placeFilter =
        place === 'all' ? true : item.place === place;
      const capacityFilter =
        capacity === 'all'
          ? true
          : item.capacity >= Number(capacity);
      return placeFilter && capacityFilter;
    });
  }
);
  • Related