Поиск и фильтрация опций

Компоненты интерфейса, работающие с наборами данных — селекты, списки выбора, автокомплиты — требуют механизмов быстрого поиска и фильтрации. При большом количестве элементов пользователь должен иметь возможность находить нужный вариант без прокрутки длинных списков.

В библиотеке Naive UI поиск и фильтрация реализованы через несколько механизмов:

  • встроенный поиск в компонентах выбора
  • пользовательские функции фильтрации
  • удалённый поиск (remote search)
  • динамическое обновление опций
  • асинхронная загрузка данных

Эти механизмы используются в компонентах:

  • n-select
  • n-auto-complete
  • n-cascader
  • n-transfer
  • n-tree-select

Каждый из них предоставляет собственные инструменты управления поиском.


Базовая фильтрация в n-select

Компонент n-select поддерживает встроенный поиск по списку опций. Для его включения используется свойство filterable.

<n-select
  v-model:value="value"
  filterable
  :options="options"
/>

Структура данных:

const options = [
  { label: 'JavaScript', value: 'js' },
  { label: 'TypeScript', value: 'ts' },
  { label: 'Vue', value: 'vue' },
  { label: 'React', value: 'react' }
]

После активации filterable:

  • в поле появляется строка ввода
  • список автоматически фильтруется
  • поиск выполняется по label

Поиск нечувствителен к регистру и происходит по вхождению строки.


Пользовательская функция фильтрации

Встроенный алгоритм подходит для простых случаев. При необходимости сложной логики используется свойство filter.

<n-select
  v-model:value="value"
  filterable
  :options="options"
  :filter="customFilter"
/>

Функция фильтрации:

const customFilter = (pattern, option) => {
  return option.label.toLowerCase().includes(pattern.toLowerCase())
}

Параметры функции:

Параметр Описание
pattern введённая пользователем строка
option объект опции

Возвращаемое значение:

true  — элемент отображается
false — элемент скрывается

Фильтрация по нескольким полям

Иногда данные содержат несколько атрибутов: название, код, описание.

const options = [
  { label: 'JavaScript', value: 'js', category: 'language' },
  { label: 'Vue', value: 'vue', category: 'framework' },
  { label: 'React', value: 'react', category: 'framework' }
]

Расширенная фильтрация:

const filter = (pattern, option) => {
  const text = pattern.toLowerCase()

  return (
    option.label.toLowerCase().includes(text) ||
    option.value.toLowerCase().includes(text) ||
    option.category.toLowerCase().includes(text)
  )
}

Теперь поиск выполняется по нескольким полям.


Отключение локальной фильтрации

В некоторых ситуациях данные загружаются с сервера. В таком случае локальная фильтрация отключается:

<n-select
  filterable
  remote
  :options="options"
  :loading="loading"
  @search="handleSearch"
/>

Свойство:

remote

указывает компоненту, что поиск выполняется вне компонента.


Обработка события поиска

Событие search вызывается при изменении строки поиска.

const handleSearch = (query) => {
  console.log(query)
}

Параметр:

query — введённая строка

Типичный сценарий — отправка запроса к серверу.


Реализация удалённого поиска

Асинхронная загрузка данных может выглядеть следующим образом.

const options = ref([])
const loading = ref(false)

const handleSearch = async (query) => {
  loading.value = true

  const result = await fetch(`/api/search?q=${query}`)
  const data = await result.json()

  options.value = data.map(item => ({
    label: item.name,
    value: item.id
  }))

  loading.value = false
}

Преимущества удалённого поиска:

  • поддержка огромных наборов данных
  • уменьшение объёма загрузки
  • всегда актуальная информация

Дебаунсинг поисковых запросов

Без оптимизации каждый ввод символа вызывает HTTP-запрос. Для уменьшения нагрузки применяется debounce.

Пример с использованием lodash.

import { debounce } from 'lodash'

const handleSearch = debounce(async (query) => {
  loading.value = true

  const result = await fetch(`/api/search?q=${query}`)
  const data = await result.json()

  options.value = data

  loading.value = false
}, 300)

Теперь запрос отправляется только после паузы ввода.


Минимальная длина поискового запроса

Часто серверные запросы выполняются только после ввода определённого количества символов.

const handleSearch = async (query) => {
  if (query.length < 2) {
    options.value = []
    return
  }

  loading.value = true

  const result = await fetch(`/api/search?q=${query}`)
  const data = await result.json()

  options.value = data
  loading.value = false
}

Это уменьшает количество запросов и повышает производительность.


Отображение состояния загрузки

Свойство loading отображает индикатор загрузки.

<n-select
  filterable
  remote
  :loading="loading"
  :options="options"
  @search="handleSearch"
/>

Во время загрузки:

  • список блокируется
  • появляется индикатор

Компонент n-auto-complete

n-auto-complete предназначен именно для поиска.

<n-auto-complete
  v-model:value="value"
  :options="options"
  @search="handleSearch"
/>

Структура данных:

const options = [
  { label: 'JavaScript', value: 'JavaScript' },
  { label: 'TypeScript', value: 'TypeScript' }
]

Отличие от n-select:

Компонент Особенность
n-select выбор из фиксированного списка
n-auto-complete поиск по тексту

Динамическое обновление опций

Опции могут изменяться динамически.

const options = ref([])

watch(searchQuery, async (query) => {
  const data = await fetchData(query)
  options.value = data
})

Компонент автоматически перерисует список.


Фильтрация в n-cascader

Компонент n-cascader используется для иерархических данных.

Пример структуры:

const options = [
  {
    label: 'Frontend',
    value: 'frontend',
    children: [
      { label: 'Vue', value: 'vue' },
      { label: 'React', value: 'react' }
    ]
  }
]

Включение поиска:

<n-cascader
  filterable
  :options="options"
/>

Особенность:

поиск выполняется по всей цепочке узлов.


Кастомная фильтрация в n-cascader

<n-cascader
  filterable
  :options="options"
  :filter="filter"
/>

Функция:

const filter = (pattern, path) => {
  return path.some(node =>
    node.label.toLowerCase().includes(pattern.toLowerCase())
  )
}

path — массив узлов пути.


Фильтрация в n-transfer

n-transfer позволяет перемещать элементы между списками.

Поиск включается свойством:

<n-transfer
  filterable
  :options="options"
  v-model:value="value"
/>

Фильтрация применяется к:

  • исходному списку
  • списку выбранных элементов

Пользовательская фильтрация n-transfer

<n-transfer
  filterable
  :options="options"
  :filter="filter"
/>

Функция:

const filter = (pattern, option) => {
  return option.label.includes(pattern)
}

Поиск в n-tree-select

n-tree-select работает с древовидной структурой.

<n-tree-select
  filterable
  :options="options"
/>

Пример данных:

const options = [
  {
    label: 'Languages',
    key: 'lang',
    children: [
      { label: 'JavaScript', key: 'js' },
      { label: 'Python', key: 'python' }
    ]
  }
]

Поиск выполняется по всем уровням дерева.


Оптимизация фильтрации больших списков

При работе с тысячами элементов важны оптимизации.

Основные подходы:

1. виртуализация

Использование:

virtual-scroll

Это снижает нагрузку на DOM.

2. удалённый поиск

Хранение данных на сервере.

3. индексация данных

Подготовка структуры поиска заранее.

4. кэширование результатов

const cache = new Map()

const search = async (query) => {
  if (cache.has(query)) {
    return cache.get(query)
  }

  const result = await api(query)

  cache.set(query, result)
  return result
}

Отображение пустого результата

Если поиск не дал результатов, отображается сообщение.

<n-select
  filterable
  :options="options"
  no-data-text="Ничего не найдено"
/>

Также можно задать собственный слот:

<template #empty>
  Ничего не найдено
</template>

Сортировка результатов поиска

После фильтрации можно изменить порядок элементов.

const filter = (pattern, option) => {
  return option.label.includes(pattern)
}

const sortedOptions = computed(() => {
  return options.value.sort((a, b) =>
    a.label.localeCompare(b.label)
  )
})

Использование:

<n-select
  filterable
  :options="sortedOptions"
/>

Подсветка совпадений

Для улучшения UX совпадения можно подсвечивать.

Создаётся кастомный рендер:

<n-select
  filterable
  :options="options"
  :render-label="renderLabel"
/>

Функция:

const renderLabel = (option) => {
  const pattern = searchQuery.value

  const text = option.label.replace(
    new RegExp(pattern, 'gi'),
    match => `<mark>${match}</mark>`
  )

  return h('span', { innerHTML: text })
}

Подсветка облегчает восприятие результатов.


Кэширование результатов поиска

При повторных запросах можно использовать кэш.

const cache = {}

const handleSearch = async (query) => {
  if (cache[query]) {
    options.value = cache[query]
    return
  }

  const data = await api(query)

  cache[query] = data
  options.value = data
}

Это уменьшает сетевую нагрузку.


Архитектура поисковых компонентов

Типичная архитектура поиска:

UI компонент
      ↓
обработчик события search
      ↓
debounce
      ↓
API запрос
      ↓
нормализация данных
      ↓
обновление options

Такой подход обеспечивает:

  • масштабируемость
  • высокую производительность
  • гибкость логики фильтрации.