Современные пользовательские интерфейсы требуют высокой интерактивности и удобства взаимодействия. В этой статье поговорим о том, как реализовать мощный, адаптивный компонент мульти-выбора на основе Vue 3 Composition API. ChipsMultiSelect — это компонент, который объединяет возможности выпадающего списка, визуализации выбора в виде «чипсов» и встроенной фильтрации.
Выбранные элементы отображаются в виде “чипсов”

Фильтрация элементов списка. Компонент совмещает функции выпадающего списка, набора “чипсов” и поле ввода для фильтрации.

При большом количестве “чипсов” происходит автоматический перенос на новую строку и увеличение высоты поля ввода

Основные возможности ChipsMultiSelect
-
Интерактивное управление состоянием:
Выбранные элементы отображаются в виде «чипсов».
Удаление осуществляется с помощью иконки «крестик» на чипсе.
-
Поддержка поиска и фильтрации:
Поле ввода позволяет пользователям находить элементы в реальном времени.
Динамическая фильтрация обновляет список на основе пользовательского ввода.
-
Адаптивный дизайн:
Высота поля ввода автоматически регулируется при большом количестве чипсов.
При достижении максимальной ширины, происходит перенос добавленных «чипсов» на новую строку
-
Легкая интеграция:
Поддержка работы с различными форматами данных, включая строки, объекты и массивы.
Особенности реализации
Проблемы, с которыми столкнулся при реализации данного компонента – стилизация input таким образом чтоб в нем отобразить список чипсов. Также необходимо спозиционировать каретку ввода сразу после чипсов. К тому же при расположении чипсов более чем в один ряд поле ввода должно смещаться к последней чипсе.
В общем случае с input решение выглядит довольно громоздким и сложным. Но тут нам на помощь приходят редактируемые div-ы (div cо свойством contenteditable = true ). Этот подход снимает все проблемы стилизации и заметно упрощает реализацию. Достаточно всего лишь застилизовать div под input.
Доступ к данным, введенным пользователем для фильтрации получим с использованием innerText
Еще одна особенность – чтоб при нажатии пользователем клавиши Enter при фильтрации не происходило перехода на новую строку event.preventDefault()
Компонент ChipsMultiSelect состоит из нескольких ключевых частей:
-
Компонент чипса (ChipsItem): Единичная чипса. Может быть объектом а не только строкой а также предоставляет возможность удалить его через крестик. Возможно переиспользовать в проекте, поэтому решено выделить в отдельный компонент.
-
Список чипсов (ChipsList): Отвечает за отображение коллекции выбранных чипсов и обрабатывает пользовательские действия.
-
Основной компонент (ChipsMultiSelect): содержит ChipsList, выпадающий список, функционал фильтрации.
Исходный код компонентов:
Компонент чипса (ChipsItem):
<script setup> import Icon from '@/ui-library-b2b/icons/B2BBaseIcon.vue' const props = defineProps({ item: { type: Object, }, bindName: { type: String, default: 'name', }, }) const emit = defineEmits(['delete']) function deleteItem() { emit('delete', props.item) } </script> <template> <div class="selected-item"> {{ item[bindName] }} <div class="selected-item__close" @click.stop="deleteItem()"> <Icon icon="Close" /> </div> </div> </template> <style scoped lang="scss"> .selected-item { display: flex; gap: 4px; align-items: center; color: var(--text-colors); font-weight: 300; font-style: normal; line-height: 20px; white-space: nowrap; font-size: 14px; letter-spacing: 0.005em; text-align: left; flex-direction: row; padding: 4px 6px 4px 8px; background: rgba(16, 24, 40, 0.1); border-radius: 2px; &__close { color: black; cursor: pointer; } } </style>
Список чипсов (ChipsList):
import { ref } from 'vue' import SelectedItem from '@/ui-library-b2b/search/ChipsItem.vue' const props = defineProps({ bindName: { type: String, default: 'name', }, inn: { type: Boolean, default: false, }, }) const emit = defineEmits(['on-keyup', 'blur']) const chips = defineModel() const multiselectRef = ref(null) function deleteItem(item) { chips.value = chips.value.filter((el) => el !== item) } function onKeyUp(e) { emit('on-keyup', multiselectRef.value.textContent) if (e.key === 'Enter') { multiselectRef.value.textContent = '' } } function onBlur() { emit('blur', multiselectRef.value.textContent) multiselectRef.value.textContent = '' } function handleInput() { const maxLength = 12 if (props.inn) { if (multiselectRef.value.textContent.length > maxLength) { multiselectRef.value.textContent = multiselectRef.value.textContent.slice(0, maxLength) } multiselectRef.value.textContent = multiselectRef.value.textContent.replace(/\D/g, '') } } </script> <template> <div class="chips"> <div v-for="(item, index) in chips" :key="index"> <SelectedItem :item="item" :bind-name @delete="deleteItem" /> </div> <div ref="multiselectRef" contenteditable="true" spellcheck="false" class="custom-div" @keydown.enter.prevent="" @keyup="onKeyUp" @blur="onBlur" @input="handleInput" /> </div> </template> <style lang='scss' scoped> .chips { display: flex; flex-direction: row; flex-wrap: wrap; gap: 3px; margin-top: 4px; width: 100%; } .custom-div { flex-grow: 1; white-space: nowrap; display: flex; align-items: center; overflow: hidden; } .custom-div:focus { outline: none; } </style>
Основной компонент (ChipsMultiSelect)
<script setup lang="ts"> // import import { ref } from 'vue' import Chips from '@/ui-library-b2b/search/ChipsList.vue' import Icon from '@/ui-library-b2b/icons/B2BBaseIcon.vue' // props const props = defineProps({ caption: { type: String, default: 'Список холдингов', }, placeholder: { type: String, default: '', }, }) // const const searchText = defineModel() const chips = ref([]) const title = ref('My title') const titleElement = ref(null) // methods function validate(event: Event) { event.preventDefault() // (event.target as HTMLInputElement).blur() chips.value.push(titleElement.value.innerText.trim()) titleElement.value.innerText = '' } function keyUp() { searchText.value = titleElement.value.innerText console.log(titleElement.value.innerText) } defineExpose({ titleElement }) </script> <template> <div class="multi-search"> <div class="multi-search__input"> <Icon class="multi-search__icon-search" icon="Search" /> <Chips v-model="chips" style="padding-left: 40px;" /> <div ref="titleElement" contenteditable="true" spellcheck="false" :placeholder="chips.length > 0 ? '' : placeholder" @keyup="keyUp" @keydown.enter="validate" /> </div> </div> </template> <style scoped lang="scss"> .multi-search{ [contenteditable=true]:empty:before{ content: attr(placeholder); padding-top: 3px; pointer-events: none; display: block; font-style: normal; font-weight: 400; font-size: 14px; line-height: 20px; letter-spacing: 0.005em; color: rgba(16, 24, 40, 0.5); } div[contenteditable=true] { padding: 5px; width: 100%; outline:none; } position: relative; &__icon-search{ position: fixed; margin: 5px 10px; width: 24px; height: 24px; } &__input{ display: flex; flex-direction: row; flex-wrap: nowrap; width: 800px; height: 36px; box-sizing: border-box; border: 1px solid rgba(16, 24, 40, 0.1); box-shadow: inset 0px 1px 0px rgba(16, 24, 40, 0.05), inset 0px 2px 0px rgba(16, 24, 40, 0.05); border-radius: 4px; } .btn { width: 16px; height: 16px; position: absolute; top: 8px; bottom: 10px; right: 10px; display: none; border: 0; padding-top: 0 -5px; border-radius: 50%; background-color: #fff; transition: background 200ms; outline: none; &:hover { width: 16px; height: 16px; display: block; background: url("../../assets/img/navigation/close.svg") no-repeat; } } input:valid ~ div { display: block; } .ok { background: url("../../assets/img/navigation/ok.svg") no-repeat; } .err { background: url("../../assets/img/navigation/close_gray.svg") no-repeat; } } </style>
Возможности кастомизации и расширения
Этот компонент можно легко кастомизировать для различных целей:
•Использование других типов данных: В компоненте можно работать с любыми типами данных, что позволяет использовать его для разных сценариев, таких как списки тегов, категорий и т.д.
•Поддержка разных форматов ввода: Ввод можно адаптировать под различные требования, например, поддержка телефонных номеров, ИНН, дат и других форматов.
•Динамическое обновление: Компонент можно адаптировать для работы с динамическими данными, например, при получении данных с сервера.
Применение и реальные кейсы использования:
-
CRM-системы: фильтрация и выбор значений из больших справочников.
-
E-commerce: подбор товаров по характеристикам.
-
Управление тегами: работа с категориями в CMS.
Заключение
В данной статье мы реализовали компонент, который:
• Снижает сложность интеграции фильтров и поиска в веб-приложениях.
• Упрощает управление пользовательским вводом и валидацией данных.
• Обеспечивает высокую степень кастомизации через свойства и события.
ChipsMultiSelect демонстрирует, как Vue 3 может быть использован для создания интерактивных UI-компонентов. Он сочетает в себе гибкость, мощный функционал и удобство использования, что делает его незаменимым инструментом для веб-разработчиков. Компонент легко интегрируется в различные проекты и улучшает пользовательский опыт.
Исходный код компонентов https://github.com/lyashov/ChipsMultiSelect.git
ссылка на оригинал статьи https://habr.com/ru/articles/863210/
Добавить комментарий