Динамічний пошук

Є список працівників і поле пошуку. При введенні відображаються усі, які містять вказаний фрагмент.


Рішення:

'use strict'

/**
 * @see {EntrySearch}
 * @typedef {Object} EntrySearchCSSClasses
 * @property {Object} [search]
 * @property {string} [search.base]
 * @property {string} [search.header]
 * @property {string} [search.list]
 * @property {string} [search.empty]
 * @property {string} [search.input]
 * @property {string} [entry]
 */

/**
 * @see {EntrySearch}
 * @typedef {Object} EntrySearchOptions
 * @property {string} [headerText]
 * @property {string} [emptyText]
 * @property {string} [inputText]
 */

class Entry {
  /** @type {string} */
  name
  /** @type {string} */
  cssClass
  /** @type {HTMLDivElement} */
  $el
  isHidden = false

  /**
   * @param {string} name
   * @param {string} [cssClass]
   */
  constructor(name, cssClass = '') {
    this.name = name
    this.cssClass = cssClass
  }

  /** @param {string} fragment */
  includes(fragment) {
    return this.name.toLowerCase().includes(fragment.trim().toLowerCase())
  }

  show() {
    this.isHidden = false
    this.$el.hidden = false
  }

  hide() {
    this.isHidden = true
    this.$el.hidden = true
  }

  render(cssSelector) {
    this.$el = document.createElement('LI')
    this.$el.className = this.cssClass
    this.$el.textContent = this.name

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

    return this.$el
  }
}

class EntrySearch {
  /** @type {string[]} */
  entryList
  /** @type {EntrySearchCSSClasses} */
  cssClasses
  /** @type {EntrySearchOptions} */
  options
  /** @type {Entry[]} */
  entryObjectList
  /** @type {HTMLInputElement} */
  inputEl
  /** @type {HTMLUListElement} */
  entryListEl
  /** @type {HTMLLIElement} */
  emptyEl
  /** @type {HTMLDivElement} */
  $el

  /**
   * @param {string[]} entryList
   * @param {EntrySearchCSSClasses} [cssClasses]
   * @param {EntrySearchOptions} [options]
   */
  constructor(entryList, cssClasses = {}, options = {}) {
    this.cssClasses = {
      search: {
        base: 'search',
        header: 'search__header',
        list: 'search__list',
        empty: 'search__empty',
        input: 'search__input',
      },
      entry: 'search__item',
      ...cssClasses,
    }

    this.options = {
      headerText: 'Search',
      emptyText: 'Nothing found.',
      inputText: 'Type something to search...',
      ...options,
    }

    this.entryList = entryList
    this.entryObjectList = entryList.map(
      (entry) => new Entry(entry, this.cssClasses.entry),
    )
  }

  #renderInput() {
    const inputEl = document.createElement('INPUT')

    inputEl.type = 'search'
    inputEl.placeholder = this.options.inputText
    inputEl.className = this.cssClasses.search.input

    return inputEl
  }

  #renderEntryList() {
    const listEl = document.createElement('UL')

    listEl.className = this.cssClasses.search.list

    for (const entryObj of this.entryObjectList) {
      listEl.append(entryObj.render())
    }

    return listEl
  }

  #renderHeader() {
    const headerEl = document.createElement('H3')

    headerEl.textContent = this.options.headerText
    headerEl.className = this.cssClasses.search.header

    return headerEl
  }

  #renderEmpty() {
    const emptyEl = document.createElement('li')

    emptyEl.textContent = this.options.emptyText
    emptyEl.className = this.cssClasses.search.empty

    return emptyEl
  }

  handleSearch() {
    const value = this.inputEl.value

    for (const entryObj of this.entryObjectList) {
      if (entryObj.includes(value)) entryObj.show()
      else entryObj.hide()
    }

    const nothingFound = this.entryObjectList.every(({isHidden}) => isHidden)

    if (nothingFound && !this.emptyEl.isConnected)
      this.entryListEl.append(this.emptyEl)
    else if (!nothingFound && this.emptyEl.isConnected) this.emptyEl.remove()
  }

  render(cssSelector) {
    const headerEl = this.#renderHeader()

    this.inputEl = this.#renderInput()
    this.inputEl.addEventListener('input', this.handleSearch.bind(this))

    this.entryListEl = this.#renderEntryList()

    this.emptyEl = this.#renderEmpty()

    this.$el = document.createElement('DIV')
    this.$el.className = this.cssClasses.search.base
    this.$el.append(headerEl, this.inputEl, this.entryListEl)

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

    return this.$el
  }
}

// =============================================================================

const employeeList = [
  'Білостоцький Ростислав',
  'Ковальчук Анастасія',
  'Коб Тетяна',
  'Власенко Ілля',
  'Баланюк Владислав',
  'Ковтун Ігор',
  'Огородник Андрій',
  'Корець Вероніка',
]
/** @type {EntrySearchCSSClasses} */
const searchCssClasses = {
  search: {
    base: 'u-max-is-sm u-pb-400 u-pi-300 u-flow-200 u-rounded-lg u-border',
    header: 'u-text-450 u-font-bolder u-text-center',
    list: 'u-flow-100',
    empty: 'u-text-zinc-400',
    input: 'c-input',
  },
}
/** @type {EntrySearchOptions} */
const searchOptions = {
  headerText: 'Працівники',
  emptyText: 'Нічого не знайдено :(',
  inputText: 'Пошук',
}
const employeeSearch = new EntrySearch(
  employeeList,
  searchCssClasses,
  searchOptions,
)

// =============================================================================

const frameworks = [
  'React',
  'Angular',
  'Vue.js',
  'Svelte',
  'Ember.js',
  'Backbone.js',
  'Preact',
  'SolidJS',
  'Qwik',
  'Astro',
  'Next.js',
  'Nuxt.js',
  'Remix',
  'Gatsby',
  'Meteor',
  'Alpine.js',
  'Lit',
  'Mithril',
  'Aurelia',
  'Knockout',
]
/** @type {EntrySearchCSSClasses} */
const frameworkCssClasses = {
  search: {
    base: 'u-max-is-sm u-pb-400 u-pi-300 u-flow-200 u-rounded-lg u-border-yellow-400 u-text-purple-600',
    header: 'u-text-450 u-font-bolder u-text-center',
    list: 'u-flow-100',
    empty: 'u-text-zinc-400',
    input: 'c-input',
  },
}
const frameworkSearch = new EntrySearch(frameworks, frameworkCssClasses, {
  headerText: 'JS Frameworks',
})

// =============================================================================

const containerEl = document.querySelector('.js-app')

containerEl?.append(employeeSearch.render(), frameworkSearch.render())