Фільтри пошуку

Дано список автомобілів (марка, рік випуску, ціна). Сформувати елементи для фільтрування з використанням випадаючого списку (контент цих випадаючих списків сформувати у залежності від переданого списку).


Рішення:

'use strict'

class Select {
  static events = {
    selection: 'select:selection',
  }

  attribute
  valuesList
  cssClass
  value
  $el

  constructor(attribute, valuesList, cssClass = '') {
    this.attribute = attribute
    this.valuesList = valuesList
    this.cssClass = cssClass
  }

  dispatchSelection() {
    const selectionEvent = new CustomEvent(Select.events.selection, {
      detail: {
        [this.attribute]: this.value,
      },
      bubbles: true,
    })

    this.$el.dispatchEvent(selectionEvent)
  }

  handleSelection() {
    const index = this.$el.selectedIndex
    this.value = this.valuesList[index]
    this.dispatchSelection()
  }

  render(cssSelector) {
    this.$el = document.createElement('SELECT')
    this.$el.className = this.cssClass
    this.$el.addEventListener('change', this.handleSelection.bind(this))

    for (const value of this.valuesList) {
      const optionEl = document.createElement('OPTION')

      optionEl.textContent = value
      this.$el.append(optionEl)
    }

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}

class Filter {
  static events = {
    change: 'filter:apply',
  }

  filters
  selectedFilters = {}
  itemsList
  filteredItemsList
  cssObj
  $el

  constructor(filters, itemsList, cssObj = {}) {
    this.filters = filters
    this.itemsList = itemsList
    this.filteredItemsList = itemsList
    this.cssObj = {base: '', label: '', select: '', ...cssObj}
  }

  getFilteredItems(attribute, value) {
    return this.filteredItemsList.filter((item) => item[attribute] === value)
  }

  applyFilters() {
    this.filteredItemsList = this.itemsList // reset

    for (const [attribute, value] of Object.entries(this.selectedFilters)) {
      if (value === '') continue

      this.filteredItemsList = this.getFilteredItems(attribute, value)
    }
  }

  dispatchFilteredIds() {
    const customEvent = new CustomEvent(Filter.events.change, {
      detail: this.filteredItemsList.map(({id}) => id),
      bubbles: true,
    })

    this.$el.dispatchEvent(customEvent)
  }

  renderSelect(attribute) {
    const attributeValues = [''] // add an empty option

    for (const item of this.itemsList) {
      const value = item[attribute]

      if (attributeValues.includes(value)) continue

      attributeValues.push(value)
    }

    attributeValues.sort()

    return new Select(attribute, attributeValues, this.cssObj.select).render()
  }

  /** @param {CustomEvent} e */
  handleFilterApply({detail}) {
    this.selectedFilters = {...this.selectedFilters, ...detail}

    this.applyFilters()
    this.dispatchFilteredIds()
  }

  renderFilters() {
    const filtersEl = document.createElement('FORM')

    for (const {attribute, label} of this.filters) {
      const selectEl = this.renderSelect(attribute)

      const labelEl = document.createElement('LABEL')
      labelEl.className = this.cssObj.label
      labelEl.append(label, selectEl)

      filtersEl.append(labelEl)
    }

    filtersEl.addEventListener(
      Select.events.selection,
      this.handleFilterApply.bind(this),
    )

    return filtersEl
  }

  render(cssSelector) {
    this.$el = this.renderFilters()
    this.$el.className = this.cssObj.base

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}

class Car {
  static renderTHead() {
    const rowEl = document.createElement('TR')

    for (const label of arguments) {
      const cell = document.createElement('TH')
      cell.textContent = label
      rowEl.append(cell)
    }

    const tHeadEl = document.createElement('THEAD')
    tHeadEl.append(rowEl)

    return tHeadEl
  }

  id
  brand
  year
  price
  color
  cssObj
  $el

  constructor({id, brand, year, color, price}, cssObj = {}) {
    this.id = id
    this.brand = brand
    this.year = year
    this.color = color
    this.price = price
    this.cssObj = {
      base: '',
      ...cssObj,
    }
  }

  renderCell(label) {
    const cellEl = document.createElement('TD')
    cellEl.textContent = label

    return cellEl
  }

  render(cssSelector) {
    this.$el = document.createElement('TR')
    this.$el.className = this.cssObj.base

    this.$el.append(
      this.renderCell(this.brand),
      this.renderCell(this.year),
      this.renderCell(this.color),
      this.renderCell(this.price),
    )

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}

class CarFilter {
  /** @type {{attribute: string, value: string}[]} */
  filters
  carsList
  /** An object that holds Car id to their respectful class instances
   *
   * @example { 65: new Car(), 54: new Car(), ... }
   * @type {Object<string, Car>}
   * */
  label
  carToIdObjects
  cssObj
  $el

  constructor(filters, carsList, label, cssObj = {}) {
    this.filters = filters
    this.carsList = carsList
    this.label = label
    this.cssObj = {
      base: '',
      header: '',
      body: '',
      table: '',
      itemObj: {},
      filterObj: {},
      ...cssObj,
    }
    this.carToIdObjects = this.createCarToIdObjects()
  }

  createCarToIdObjects() {
    const obj = {}

    for (const car of this.carsList) {
      obj[car.id] = new Car(car, this.cssObj.itemObj)
    }

    return obj
  }

  renderTable() {
    const tHeadEl = Car.renderTHead('Марка', 'Рік', 'Колір', 'Ціна')
    const tBodyEl = this.renderTBody()

    const tableEl = document.createElement('TABLE')
    tableEl.append(tHeadEl, tBodyEl)

    const tableContainer = document.createElement('DIV')
    tableContainer.append(tableEl)
    tableContainer.className = this.cssObj.table

    return tableContainer
  }

  renderTBody() {
    const tBodyEl = document.createElement('TBODY')
    tBodyEl.className = this.cssObj.list

    for (const id in this.carToIdObjects) {
      tBodyEl.append(this.carToIdObjects[id].render())
    }

    return tBodyEl
  }

  hideCars() {
    for (const id in this.carToIdObjects) {
      this.carToIdObjects[id].$el.hidden = true
    }
  }

  /**
   * @param {CustomEvent} e
   */
  handleFilter({detail: filteredIds}) {
    this.hideCars()

    for (const id of filteredIds) {
      this.carToIdObjects[id].$el.hidden = false
    }
  }

  render(cssSelector) {
    const headerEl = document.createElement('H3')
    headerEl.className = this.cssObj.header
    headerEl.textContent = this.label

    this.filterEl = new Filter(
      this.filters,
      this.carsList,
      this.cssObj.filterObj,
    ).render()
    this.filterEl.addEventListener(
      Filter.events.change,
      this.handleFilter.bind(this),
    )

    this.tableEl = this.renderTable()

    const bodyEl = document.createElement('DIV')
    bodyEl.className = this.cssObj.body
    bodyEl.append(this.filterEl, this.tableEl)

    this.$el = document.createElement('DIV')
    this.$el.className = this.cssObj.base
    this.$el.append(headerEl, bodyEl)

    if (cssSelector) document.querySelector(cssSelector).append(this.$el)

    return this.$el
  }
}

// =============================================================================
const cars = [
  {
    id: 0,
    brand: 'Toyota',
    year: 2018,
    color: 'червоний',
    price: 21000,
  },
  {
    id: 1,
    brand: 'Toyota',
    year: 2020,
    color: 'синій',
    price: 25000,
  },
  {
    id: 2,
    brand: 'Toyota',
    year: 2023,
    color: 'білий',
    price: 25000,
  },
  {
    id: 3,
    brand: 'BMW',
    year: 2018,
    color: 'блакитний',
    price: 41000,
  },
  {
    id: 4,
    brand: 'BMW',
    year: 2022,
    color: 'помаранчевий',
    price: 25000,
  },
  {
    id: 5,
    brand: 'BMW',
    year: 2023,
    color: 'білий',
    price: 45000,
  },
  {
    id: 6,
    brand: 'Ford',
    year: 2018,
    color: 'синій',
    price: 18000,
  },
  {
    id: 7,
    brand: 'Ford',
    year: 2023,
    color: 'блакитний',
    price: 18000,
  },
  {
    id: 8,
    brand: 'Mercedes-Benz',
    year: 2023,
    color: 'помаранчевий',
    price: 60000,
  },
  {
    id: 9,
    brand: 'Hyundai',
    year: 2021,
    color: 'білий',
    price: 22000,
  },
  {
    id: 10,
    brand: 'Hyundai',
    year: 2023,
    color: 'червоний',
    price: 36000,
  },
]
const carFilters = [
  {attribute: 'brand', label: 'марка'},
  {attribute: 'year', label: 'рік'},
  {attribute: 'color', label: 'колір'},
]
const cssObj = {
  base: 'showroom | u-p-400 u-flow-500 u-border-zinc-700 u-rounded-lg',
  header: 'u-text-center',
  body: 'showroom__body | u-gap-400',
  table: 'showroom__table',
  filterObj: {
    base: 'showroom__filter | u-grid u-gap-200',
    label: 'u-grid u-gap-100 u-capitalize u-font-bolder',
    select: '',
  },
}
const carFiltration = new CarFilter(carFilters, cars, 'Фільтр авто', cssObj)

carFiltration.render('.js-app')