Use These Components to Create a Modern UI Using Vue3


Modern user interfaces demand high interactivity and usability. This article explores how to create a powerful, adaptive multi-select component using the Vue 3 Composition API. The ChipsMultiSelect component combines the features of a dropdown list, visual selection in the form of “chips,” and built-in filtering functionality.

Selected items are displayed as “chips”.


Real-time Filtering: The component integrates a dropdown list, a set of chips, and an input field for filtering.


Dynamic Resizing: When there are too many chips, they wrap to a new row, adjusting the height of the input field accordingly.

Key Features of ChipsMultiSelect

  1. Interactive State Management:
  • Selected items are displayed as chips.
  • Chips can be removed using a “close” icon.
  1. Search and Filtering Support:
  • The input field allows users to search for items in real-time.
  • The list updates dynamically based on the user input.
  1. Responsive Design:
  • The input field’s height adjusts dynamically for multiple rows of chips.
  • Chips wrap to new lines when the maximum width is exceeded.
  1. Easy Integration:
  • Supports various data formats, including strings, objects, and arrays.

Implementation Details

Challenges:

Styling the input field to display chips while ensuring proper caret positioning after the chips was a significant challenge. Additionally, the input field needs to shift dynamically to align with the last row of chips when they span multiple lines.

Solution:

Using editable divs (contenteditable=true) instead of traditional input fields simplifies styling and implementation. This approach resolves positioning and styling issues efficiently.


Key Techniques:

  • Use innerText to retrieve user input for filtering.
  • Prevent new lines on Enter keypress using event.preventDefault().

Component Structure

  1. Chip Component (ChipsItem):
  • Represents an individual chip.
  • Supports both strings and objects.
  • Includes a “close” button for removal.
  • Designed for reuse across projects.
  1. Chip List (ChipsList):
  • Displays a collection of selected chips.
  • Handles user interactions.
  1. Main Component (ChipsMultiSelect):
  • Encapsulates ChipsList, dropdown list, and filtering functionality.

ChipsItem:

<script setup>

import Icon from '@/ui-library-b2b/icons/B2BBaseIcon.vue'

const props = defineProps({

&nbsp;&nbsp;item: {

&nbsp;&nbsp;&nbsp;&nbsp;type: Object,

&nbsp;&nbsp;},

&nbsp;&nbsp;bindName: {

&nbsp;&nbsp;&nbsp;&nbsp;type: String,

&nbsp;&nbsp;&nbsp;&nbsp;default: 'name',

&nbsp;&nbsp;},

})

const emit = defineEmits(['delete'])

function deleteItem() {

&nbsp;&nbsp;emit('delete', props.item)

}

</script>

<template>

&nbsp;&nbsp;<div class="selected-item">

&nbsp;&nbsp;&nbsp;&nbsp;{{ item[bindName] }}

&nbsp;&nbsp;&nbsp;&nbsp;<div class="selected-item__close" @click.stop="deleteItem()">

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<Icon icon="Close" />

&nbsp;&nbsp;&nbsp;&nbsp;</div>

&nbsp;&nbsp;</div>

</template>

<style scoped lang="scss">

.selected-item {

&nbsp;&nbsp;display: flex;

&nbsp;&nbsp;gap: 4px;

&nbsp;&nbsp;align-items: center;

&nbsp;&nbsp;color: var(--text-colors);

&nbsp;&nbsp;font-weight: 300;

&nbsp;&nbsp;font-style: normal;

&nbsp;&nbsp;line-height: 20px;

&nbsp;&nbsp;white-space: nowrap;

&nbsp;&nbsp;font-size: 14px;

&nbsp;&nbsp;letter-spacing: 0.005em;

&nbsp;&nbsp;text-align: left;

&nbsp;&nbsp;flex-direction: row;

&nbsp;&nbsp;padding: 4px 6px 4px 8px;

&nbsp;&nbsp;background: rgba(16, 24, 40, 0.1);

&nbsp;&nbsp;border-radius: 2px;

&nbsp;&nbsp;&__close {

&nbsp;&nbsp;&nbsp;&nbsp;color: black;

&nbsp;&nbsp;&nbsp;&nbsp;cursor: pointer;

&nbsp;&nbsp;}

}

</style>



ChipsList:

<script setup>

import { ref } from 'vue'

import SelectedItem from '@/ui-library-b2b/search/ChipsItem.vue'

const props = defineProps({

&nbsp;&nbsp;bindName: {

&nbsp;&nbsp;&nbsp;&nbsp;type: String,

&nbsp;&nbsp;&nbsp;&nbsp;default: 'name',

&nbsp;&nbsp;},

&nbsp;&nbsp;inn: {

&nbsp;&nbsp;&nbsp;&nbsp;type: Boolean,

&nbsp;&nbsp;&nbsp;&nbsp;default: false,

&nbsp;&nbsp;},

})

const emit = defineEmits(['on-keyup', 'blur'])

const chips = defineModel()

const multiselectRef = ref(null)

function deleteItem(item) {

&nbsp;&nbsp;chips.value = chips.value.filter((el) => el !== item)

}

function onKeyUp(e) {

&nbsp;&nbsp;emit('on-keyup', multiselectRef.value.textContent)

&nbsp;&nbsp;if (e.key === 'Enter') {

&nbsp;&nbsp;&nbsp;&nbsp;multiselectRef.value.textContent = ''

&nbsp;&nbsp;}

}

function onBlur() {

&nbsp;&nbsp;emit('blur', multiselectRef.value.textContent)

&nbsp;&nbsp;multiselectRef.value.textContent = ''

}

function handleInput() {

&nbsp;&nbsp;const maxLength = 12

&nbsp;&nbsp;if (props.inn) {

&nbsp;&nbsp;&nbsp;&nbsp;if (multiselectRef.value.textContent.length > maxLength) {

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;multiselectRef.value.textContent = multiselectRef.value.textContent.slice(0, maxLength)

&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;multiselectRef.value.textContent = multiselectRef.value.textContent.replace(/D/g, '')

&nbsp;&nbsp;}

}

</script>

<template>

&nbsp;&nbsp;<div class="chips">

&nbsp;&nbsp;&nbsp;&nbsp;<div v-for="(item, index) in chips" :key="index">

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<SelectedItem :item="item" :bind-name @delete="deleteItem" />

&nbsp;&nbsp;&nbsp;&nbsp;</div>

&nbsp;&nbsp;&nbsp;&nbsp;<div

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ref="multiselectRef"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;contenteditable="true"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;spellcheck="false"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="custom-div"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@keydown.enter.prevent=""

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@keyup="onKeyUp"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@blur="onBlur"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@input="handleInput"

&nbsp;&nbsp;&nbsp;&nbsp;/>

&nbsp;&nbsp;</div>

</template>

<style lang='scss' scoped>

.chips {

&nbsp;&nbsp;display: flex;

&nbsp;&nbsp;flex-direction: row;

&nbsp;&nbsp;flex-wrap: wrap;

&nbsp;&nbsp;gap: 3px;

&nbsp;&nbsp;margin-top: 4px;

&nbsp;&nbsp;width: 100%;

}

.custom-div {

&nbsp;&nbsp;flex-grow: 1;

&nbsp;&nbsp;white-space: nowrap;

&nbsp;&nbsp;display: flex;

&nbsp;&nbsp;align-items: center;

&nbsp;&nbsp;overflow: hidden;

}

.custom-div:focus {

&nbsp;&nbsp;outline: none;

}

</style>

n

Main component (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({

&nbsp;&nbsp;caption: {

&nbsp;&nbsp;&nbsp;&nbsp;type: String,

&nbsp;&nbsp;&nbsp;&nbsp;default: 'Список холдингов',

&nbsp;&nbsp;},

&nbsp;&nbsp;placeholder: {

&nbsp;&nbsp;&nbsp;&nbsp;type: String,

&nbsp;&nbsp;&nbsp;&nbsp;default: '',

&nbsp;&nbsp;},

})

// const

const searchText = defineModel()

const chips = ref([])

const title = ref('My title')

const titleElement = ref(null)

// methods

function validate(event: Event) {

&nbsp;&nbsp;event.preventDefault()

&nbsp;&nbsp;// (event.target as HTMLInputElement).blur()

&nbsp;&nbsp;chips.value.push(titleElement.value.innerText.trim())

&nbsp;&nbsp;titleElement.value.innerText = ''

}

function keyUp() {

&nbsp;&nbsp;searchText.value = titleElement.value.innerText

&nbsp;&nbsp;console.log(titleElement.value.innerText)

}

defineExpose({ titleElement })

</script>

<template>

&nbsp;&nbsp;<div class="multi-search">

&nbsp;&nbsp;&nbsp;&nbsp;<div class="multi-search__input">

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<Icon class="multi-search__icon-search" icon="Search" />

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<Chips v-model="chips" style="padding-left: 40px;" />

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<div

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ref="titleElement" contenteditable="true" spellcheck="false" :placeholder="chips.length > 0 ? '' : placeholder" @keyup="keyUp"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@keydown.enter="validate"

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/>

&nbsp;&nbsp;&nbsp;&nbsp;</div>

&nbsp;&nbsp;</div>

</template>

<style scoped lang="scss">

.multi-search{

&nbsp;&nbsp;[contenteditable=true]:empty:before{

&nbsp;&nbsp;&nbsp;&nbsp;content: attr(placeholder);

&nbsp;&nbsp;&nbsp;&nbsp;padding-top: 3px;

&nbsp;&nbsp;&nbsp;&nbsp;pointer-events: none;

&nbsp;&nbsp;&nbsp;&nbsp;display: block;

&nbsp;&nbsp;&nbsp;&nbsp;font-style: normal;

&nbsp;&nbsp;&nbsp;&nbsp;font-weight: 400;

&nbsp;&nbsp;&nbsp;&nbsp;font-size: 14px;

&nbsp;&nbsp;&nbsp;&nbsp;line-height: 20px;

&nbsp;&nbsp;&nbsp;&nbsp;letter-spacing: 0.005em;

&nbsp;&nbsp;&nbsp;&nbsp;color: rgba(16, 24, 40, 0.5);

&nbsp;&nbsp;}

&nbsp;&nbsp;div[contenteditable=true] {

&nbsp;&nbsp;&nbsp;&nbsp;padding: 5px;

&nbsp;&nbsp;&nbsp;&nbsp;width: 100%;

&nbsp;&nbsp;&nbsp;&nbsp;outline:none;

&nbsp;&nbsp;}

&nbsp;&nbsp;position: relative;

&nbsp;&nbsp;&__icon-search{

&nbsp;&nbsp;&nbsp;&nbsp;position: fixed;

&nbsp;&nbsp;&nbsp;&nbsp;margin: 5px 10px;

&nbsp;&nbsp;&nbsp;&nbsp;width: 24px;

&nbsp;&nbsp;&nbsp;&nbsp;height: 24px;

&nbsp;&nbsp;}

&nbsp;&nbsp;&__input{

&nbsp;&nbsp;&nbsp;&nbsp;display: flex;

&nbsp;&nbsp;&nbsp;&nbsp;flex-direction: row;

&nbsp;&nbsp;&nbsp;&nbsp;flex-wrap: nowrap;

&nbsp;&nbsp;&nbsp;&nbsp;width: 800px;

&nbsp;&nbsp;&nbsp;&nbsp;height: 36px;

&nbsp;&nbsp;&nbsp;&nbsp;box-sizing: border-box;

&nbsp;&nbsp;&nbsp;&nbsp;border: 1px solid rgba(16, 24, 40, 0.1);

&nbsp;&nbsp;&nbsp;&nbsp;box-shadow: inset 0px 1px 0px rgba(16, 24, 40, 0.05), inset 0px 2px 0px rgba(16, 24, 40, 0.05);

&nbsp;&nbsp;&nbsp;&nbsp;border-radius: 4px;

&nbsp;&nbsp;}

&nbsp;&nbsp;.btn {

&nbsp;&nbsp;&nbsp;&nbsp;width: 16px;

&nbsp;&nbsp;&nbsp;&nbsp;height: 16px;

&nbsp;&nbsp;&nbsp;&nbsp;position: absolute;

&nbsp;&nbsp;&nbsp;&nbsp;top: 8px;

&nbsp;&nbsp;&nbsp;&nbsp;bottom: 10px;

&nbsp;&nbsp;&nbsp;&nbsp;right: 10px;

&nbsp;&nbsp;&nbsp;&nbsp;display: none;

&nbsp;&nbsp;&nbsp;&nbsp;border: 0;

&nbsp;&nbsp;&nbsp;&nbsp;padding-top: 0 -5px;

&nbsp;&nbsp;&nbsp;&nbsp;border-radius: 50%;

&nbsp;&nbsp;&nbsp;&nbsp;background-color: #fff;

&nbsp;&nbsp;&nbsp;&nbsp;transition: background 200ms;

&nbsp;&nbsp;&nbsp;&nbsp;outline: none;

&nbsp;&nbsp;&nbsp;&nbsp;&:hover {

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;width: 16px;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;height: 16px;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;display: block;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;background: url("../../assets/img/navigation/close.svg") no-repeat;

&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;}

&nbsp;&nbsp;input:valid ~ div {

&nbsp;&nbsp;&nbsp;&nbsp;display: block;

&nbsp;&nbsp;}

&nbsp;&nbsp;.ok {

&nbsp;&nbsp;&nbsp;&nbsp;background: url("../../assets/img/navigation/ok.svg") no-repeat;

&nbsp;&nbsp;}

&nbsp;&nbsp;.err {

&nbsp;&nbsp;&nbsp;&nbsp;background: url("../../assets/img/navigation/close_gray.svg") no-repeat;

&nbsp;&nbsp;}

}

</style>

Real-World Applications

  1. CRM Systems: Efficient filtering and selection from large directories.
  2. E-Commerce: Product filtering based on attributes.
  3. Tag Management: Category handling in CMS systems.

Conclusion

This article demonstrates how to create an interactive UI component that:

  • Simplifies the integration of filters and search functionality in web applications.
  • Enhances data input management and validation.
  • Offers extensive customization options through props and events.


ChipsMultiSelect showcases the power of Vue 3 in building interactive UI components. Its flexibility and robust functionality make it a valuable tool for web developers, seamlessly integrating into projects to enhance user experience.


Source Code: https://github.com/lyashov/ChipsMultiSelect.git

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.