Компоненты интерфейса, работающие с наборами данных — селекты, списки выбора, автокомплиты — требуют механизмов быстрого поиска и фильтрации. При большом количестве элементов пользователь должен иметь возможность находить нужный вариант без прокрутки длинных списков.
В библиотеке Naive UI поиск и фильтрация реализованы через несколько механизмов:
Эти механизмы используются в компонентах:
n-selectn-auto-completen-cascadern-transfern-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-completen-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-transfern-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-selectn-tree-select работает с древовидной структурой.
<n-tree-select
filterable
:options="options"
/>
Пример данных:
const options = [
{
label: 'Languages',
key: 'lang',
children: [
{ label: 'JavaScript', key: 'js' },
{ label: 'Python', key: 'python' }
]
}
]
Поиск выполняется по всем уровням дерева.
При работе с тысячами элементов важны оптимизации.
Основные подходы:
Использование:
virtual-scroll
Это снижает нагрузку на DOM.
Хранение данных на сервере.
Подготовка структуры поиска заранее.
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
Такой подход обеспечивает: