paint-brush
Use These Components to Create a Modern UI Using Vue3by@liashov
144 reads

Use These Components to Create a Modern UI Using Vue3

by Evgenii_LiashovDecember 6th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

ChipsMultiSelect combines the features of a dropdown list, visual selection in the form of "chips," and built-in filtering functionality. The component integrates a drop down list, a set of chips, and an input field for filtering.
featured image - Use These Components to Create a Modern UI Using Vue3
Evgenii_Liashov HackerNoon profile picture


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.
  2. 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.
  3. 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.
  4. 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.
  2. Chip List (ChipsList):
    • Displays a collection of selected chips.
    • Handles user interactions.
  3. 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({

  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:


<script setup>

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>


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

  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>


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