vue3 implements the select drop-down option

  Uh huh ~ the first post. Sorry for your poor writing 👀  

My student 🐶, I usually make a small living allowance by contacting small projects outside. Before, I always used the version of Vue2.x for projects. I just learned Vue3 in the summer vacation and started directly with Vue3 when thinking of new projects

Effect display

Well, if you don't say much, let's show the effect style to the big guys first:

Component difficulties

Because the drop-down box may be blocked in some cases, the drop-down box here is attached to the body tag, and the options in the drop-down box are often written in the form of < slot > slots, which will trouble many Xiaobai. They don't understand how to associate responsive events and data between the drop-down box and the trigger drop-down button

Use of components

<tk-select selected="Please select">
    <template #selectDropDown>
        <tk-select-item value="Latest case">Latest case</tk-select-item>
        <tk-select-item value="Hottest case">Hottest case</tk-select-item>
    </template>
</tk-select>

<hr>

<tk-select>
    <template #selectDropDown>
        <tk-select-item value="Yangzhou City">Yangzhou City</tk-select-item>
        <tk-select-item value="Nanjing City">Nanjing City</tk-select-item>
        <tk-select-item value="Wuxi City">Wuxi City</tk-select-item>
        <tk-select-item value="Xuzhou City">Xuzhou City</tk-select-item>
        <tk-select-item value="Suzhou">Suzhou</tk-select-item>
        <tk-select-item value="Zhenjiang City">Zhenjiang City</tk-select-item>
    </template>
</tk-select>

Parameter description  

tk-select   The parent tag of the option under select must contain a named slot  # selectDropDown   Can be used normally

AttributeDescription

Accepted Values

Default
electedThe value selected by default. If it is not filled in or empty, the first one in the slot is selected by default   tk-select-item   Values in--

tk-select-item   It is the option sub label (option label) under Select,   tk-select-item   You can continue to write other   HTML   The specific value of each item is determined by props   value   decision

AttributeDescriptionAccepted ValuesDefault
valueData returned by default for word options   (required)--

v-modal

have access to   v-modal   Real time acquisition   Drop down options   Selected value

be careful:   there   v-modal   There is no two-way binding, which is only used to obtain   select   The value selected in can only be used to obtain. Actively modifying its value has no effect and is not supported   v-model   Modifier

<tk-select v-model="selectValue">
    ...
</tk-select>

<script>
import { ref } from 'vue';
export default {
    setup(){
        // Receive the value selected by select
        const selectValue = ref();
        
        return {
            selectValue
        }
    }
}
</script>

Realization idea

First look at the directory structure

src
 |
 |
 |-- components
 |      |
 |      |-- select
 |            |
 |            |-- select.vue
 |            |-- select-item.vue
 |            |-- selectBus.js
 |
 |
 |-- utils
 |    |-- token.js

Two  . vue   What's the file for? There's nothing to say,   selectBus.js   solve   Vue3   Cannot be installed in   eventBus   Problems,   token.js   For each group   select   And   select-item   Mutual binding

 

First, let's look at the contents of selectBus.js

Let's see first   vue3   What did the official website say   View official website

To say human words means not to be like   vue2   For such a pleasant installation of Bus, you need to implement your own event interface or use a third-party plug-in. The specific implementation scheme is also given on the official website here

// selectBus.js
import emitter from 'tiny-emitter/instance'

export default {
    $on: (...args) => emitter.on(...args),
    $once: (...args) => emitter.once(...args),
    $off: (...args) => emitter.off(...args),
    $emit: (...args) => emitter.emit(...args)
}

The select.vue file is our parent component

vue3   newly added  < teleport>   Tag, you can mount the elements in the tag to any location,   View official documents .

// Report usage
// Mount < H1 > on the body

<teleport to="body">
    <h1>title</h1>
</teleport>

select   It is mainly composed of trigger drop-down button TK select button and drop-down list TK select drop down. The options in the drop-down box will be inserted by slots in the future

<!-- select.vue -->
<template>
  <!-- Drop down box -->
  <div class="tk-select"> 
      <div ref="select_button" class="tk-select-button" @click="selectOpen = !selectOpen">
          <!-- Selected content -->
          <span>{{selctValue}}</span>
          <!-- Right small arrow -->
          <div class="select-icon" :class="{'selectOpen':selectOpen}">
              <i class="fi fi-rr-angle-small-down"></i>
          </div>
      </div>
      <!-- Drop down box -->
      <teleport to="body">
          <transition name="select">
            <div ref="select_dropdown" v-show="selectOpen" :style="dropdownStyle" class="tk-select-dropdown">
                <ul>
                    <slot name="selectDropDown"></slot>
                </ul>
            </div>
          </transition>
      </teleport>
  </div>
</template>
First, solve the problem of opening & closing and positioning the drop-down list
import { ref, onDeactivated } from 'vue';
export default {
    // Get button
    const select_button = ref(null);
    // Get drop-down box
    const select_dropdown = ref(null);
    
    // Drop down box position parameter
    const dropdownPosition = ref({x:0,y:0,w:0})

    // Drop down box position
    const dropdownStyle = computed(()=>{
        return {
            left: `${dropdownPosition.value.x}px`,
            top:  `${dropdownPosition.value.y}px`,
            width: `${dropdownPosition.value.w}px`
        }
    })
    
    // Calculate drop-down box position
    function calculateLocation(){
        var select_button_dom = select_button.value.getBoundingClientRect()
        dropdownPosition.value.w = select_button_dom.width
        dropdownPosition.value.x = select_button_dom.left
        dropdownPosition.value.y = select_button_dom.top + select_button_dom.height + 5
    }
    
    // Recalculate the position each time the drop-down box opens
    watch(selectOpen,(val)=>{
        if(val)
            // Calculation location
            calculateLocation();
    })
    
    // ---------------------------------Add a little decoration---------------------------------------
    // Clicking the non button or drop-down box area will also collapse the drop-down box
    window.addEventListener('click',(event)=>{
        if(!select_button.value.contains(event.target) && !select_dropdown.value.contains(event.target) ){
            selectOpen.value = false
        }
    })
    
    // Recalculate the position when the page scrolls or changes size
    window.addEventListener('resize',()=>{
        // Calculate panel position
        calculateLocation();
    })
    window.addEventListener('scroll',()=>{
        // Calculate panel position
        calculateLocation();
    })
    
    // Release these listeners when the component is unloaded
    onDeactivated(() => {
        window.removeEventListener('resize')
        window.removeEventListener('scroll')
        window.removeEventListener('click')
    })
    
    return {
        select_button,
        select_dropdown,
        dropdownPosition,
        dropdownStyle,
        calculateLocation
    }
}

Let's continue to look at select-item.vue, which is our sub component

<!-- select-item.vue -->
<template>
  <li class="tk-select-item" :class="{'active':active}" @click="chooseSelectItem">
      <slot></slot>
  </li>
</template>

<script>
// Introduce Bus
import Bus from './selectBus'
export default {
    setup(props){
        // When the option is clicked
        function chooseSelectItem(){
            // Return the value of the clicked item to select
            Bus.$emit('chooseSelectItem',props.value);
        }
    }
}
</script>

stay   select.vue   Receive events in

setup(){
    // Selected content
    const selctValue = ref('');
    ...
    onMounted(()=>{
        Bus.$on('chooseSelectItem',(res)=>{
            // Modify display value
            selctValue.value = res.value
            // Close drop-down box
            selectOpen.value = false
        })
    })
    ...
}

Here, the drop-down option box is basically completed. It is perfect when we add the first drop-down option on the page, but if there are two selections on the page, the problem comes. We find that when one of the options is selected, the value displayed by the other selection also changes. We need to bind a group of select & select items, Let the Bus know which select item the event comes from when accepting

In vue2, we usually get the instance's parent and then look for the parent class select layer by layer, but we can't get the correct parent in vue3 setup, so I thought of sending a token when the select is created. I'm talking about passing this token to all subclasses. Well, theory exists and Practice begins

provide & inject

Using provide in vue can transmit data to descendants like subclasses and grandchildren, and descendants use inject to receive data   View official documents

Distribute token token  

Here you can imitate UUID in Java

// token.js
function code() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}

export function tokenFun() {
    return (code() + code() + "-" + code() + "-" + code() + "-" + code() + "-" + code() + code() + code());
}

  stay   select   Generate on Creation   token   And send it to future generations

// select.vue
import {tokenFun} from '@/utils/token'
import {provide, getCurrentInstance} from 'vue';

...

setup(){

    ...
    
    // Get instance
    const page = getCurrentInstance()

    var token = 'select-' + tokenFun();
    // Cache token
    page.token = token
    // Send token to child element
    provide('token',token)
  
  return {
      token
  }
}

In this way, we will bring a token every time we use the bus to send data after the subclass receives it

// select-item.vue
import {ref, getCurrentInstance, inject} from 'vue';

...

setup(){

    ...
    
    // Get instance
    const page = getCurrentInstance();
    
    // Receive token
    const token = inject('token');
    // Cache token
    page.token = token
    
    // Select drop-down
    function chooseSelectItem(){
        // Bring a token when sending data using Bus
        Bus.$emit('chooseSelectItem',{token: token,value: props.value});
    }
}

stay   select.vue   Verify the token after listening to the Bus

onMounted(()=>{
    Bus.$on('chooseSelectItem',(res)=>{
        // Judge whether the token carried by the descendant sending data is the same as the instance
        if(res.token === page.token){
            // Modify display value
            selctValue.value = res.value
            // Close drop-down box
            selectOpen.value = false
        }
    })
}) 

It's done, so we have a select drop-down option, and the drop-down part is hung on the body tag

All codes

select.vue

<template>
  <!-- Drop down box -->
  <div class="tk-select"> 
      <div ref="select_button" class="tk-select-button" @click="selectOpen = !selectOpen">
          <!-- Selected content -->
          <span>{{selctValue}}</span>
          <div class="select-icon" :class="{'selectOpen':selectOpen}">
              <i class="fi fi-rr-angle-small-down"></i>
          </div>
      </div>
      <!-- Drop down box -->
      <teleport to="body">
          <transition name="select">
            <div ref="select_dropdown" v-show="selectOpen" :style="dropdownStyle" class="tk-select-dropdown">
                <ul>
                    <slot name="selectDropDown"></slot>
                </ul>
            </div>
          </transition>
      </teleport>
  </div>
</template>

<script>
import {tokenFun} from '@/utils/token'
import Bus from './selectBus'
import {ref,onMounted,computed,watch,onDeactivated,provide,getCurrentInstance} from 'vue';
export default {
    name: 'TkSelect',
    props: {
        selected: String
    },
    setup(props,ctx){

        const page = getCurrentInstance()

        // Get button
        const select_button = ref(null);
        const select_dropdown = ref(null);

        // Open status
        const selectOpen = ref(false);

        // Selected content
        const selctValue = ref('');

        // Drop down box position
        const dropdownPosition = ref({x:0,y:0,w:0})

        // Drop down box position
        const dropdownStyle = computed(()=>{
            return {
                left: `${dropdownPosition.value.x}px`,
                top:  `${dropdownPosition.value.y}px`,
                width: `${dropdownPosition.value.w}px`
            }
        })

        watch(selectOpen,(val)=>{
            if(val)
                // Calculation location
                calculateLocation();
        })

        watch(selctValue,()=>{
            ctx.emit('update:modelValue', selctValue.value)
        })

        // Calculation location
        function calculateLocation(){
            var select_button_dom = select_button.value.getBoundingClientRect()
            dropdownPosition.value.w = select_button_dom.width
            dropdownPosition.value.x = select_button_dom.left
            dropdownPosition.value.y = select_button_dom.top + select_button_dom.height + 5
        }

        window.addEventListener('click',(event)=>{
            if(!select_button.value.contains(event.target) && !select_dropdown.value.contains(event.target) ){
                selectOpen.value = false
            }
        })
         window.addEventListener('touchstart',(event)=>{
            if(!select_button.value.contains(event.target) && !select_dropdown.value.contains(event.target) ){
                selectOpen.value = false
            }
        })

        window.addEventListener('resize',()=>{
            // Calculate panel position
            calculateLocation();
        })
        window.addEventListener('scroll',()=>{
            // Calculate panel position
            calculateLocation();
        })

        onDeactivated(()=>{
            window.removeEventListener('resize')
            window.removeEventListener('scroll')
            window.removeEventListener('click')
            window.removeEventListener('touchstart')
            Bus.$off('chooseSelectItem');
        })

        var token = 'select-' + tokenFun();
        // Get the generated token
        page.token = token
        // Send token to child element
        provide('token',token)

        onMounted(()=>{
             Bus.$on('chooseSelectItem',(res)=>{
                 if(res.token === page.token){
                    selctValue.value = res.value
                    selectOpen.value = false
                    Bus.$emit('chooseActive',{token:token,value:selctValue.value})
                 }
            })
            if(props.selected){
                selctValue.value = props.selected
                Bus.$emit('chooseActive',{token:token,value:selctValue.value})
            }else{
                selctValue.value = ctx.slots.selectDropDown()[0].props.value
                Bus.$emit('chooseActive',{token:token,value:selctValue.value})
            }
        })

        return {
            selectOpen,
            selctValue,
            select_dropdown,
            select_button,
            dropdownStyle,
            dropdownPosition,
            calculateLocation,
            token
        }
    }
}
</script>

<style lang="scss" scoped>
// Drop down box
.tk-select-button{
    width: 100%;
    height: 48px;
    padding: 0 16px;
    border-radius: 12px;
    font-size: 14px;
    font-weight: 500;
    line-height: 48px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border: #E6E8EC 2px solid;
    background-color: #FCFCFD;
    cursor: pointer;
    transition: border .2s;
}
.tk-select-button:hover{
    border: #23262F 2px solid;
}
.tk-select-button span{
    font-weight: 500;
    user-select: none;
}

// icon
.select-icon{
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: #E6E8EC 2px solid;
    transition: all .2s;
}
.select-icon.selectOpen{
    transform: rotate(180deg);
}

// Drop down box
.tk-select-dropdown{
    position: fixed;
    background-color: #FCFCFD;
}
.tk-select-dropdown ul{
    overflow: hidden;
    border-radius: 12px;
    border: #E6E8EC 2px solid;
    box-shadow: 0 4px 12px rgba(35,38,47 ,0.1);
}

.select-enter-from, .select-leave-to{
    opacity: 0;
    transform: scale(0.9);
}
.select-enter-active, .select-leave-active{
    transform-origin: top center;
    transition: opacity .4s cubic-bezier(0.5, 0, 0, 1.25), transform .2s cubic-bezier(0.5, 0, 0, 1.25);
}
</style>

select-item.vue

<template>
  <li class="tk-select-item" :class="{'active':active}" @click="chooseSelectItem">
      <slot></slot>
  </li>
</template>

<script>
import Bus from './selectBus'
import {ref, getCurrentInstance, inject, onDeactivated} from 'vue';
export default {
    name: "TkSelectItem",
    props: ['value'],
    setup(props){

        const page = getCurrentInstance();

        const active = ref(false);
       
        // Receive token
        const token = inject('token');
        page.token = token
        Bus.$on('chooseActive',(res)=>{
            if(res.token !== page.token)
                return
            if(res.value == props.value)
                active.value = true
            else
                active.value = false
            })

        // Select drop-down
        function chooseSelectItem(){
            Bus.$emit('chooseSelectItem',{token: token,value: props.value});
        }

        onDeactivated(()=>{
            Bus.$off('chooseActive')
        })

        return {
            chooseSelectItem,
            active,
            token
        }
    }
}
</script>

<style lang="scss" scoped>
.tk-select-item.active{
    color: #3772FF;
    background-color: #F3F5F6;
    user-select: none;
}
</style>

token.js

function code() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}

export function tokenFun() {
    return (code() + code() + "-" + code() + "-" + code() + "-" + code() + "-" + code() + code() + code());
}

selectBus.js

import emitter from 'tiny-emitter/instance'

export default {
    $on: (...args) => emitter.on(...args),
    $once: (...args) => emitter.once(...args),
    $off: (...args) => emitter.off(...args),
    $emit: (...args) => emitter.emit(...args)
}

GitHub source address

GitHub source codehttps://github.com/18651440358/vue3-select

The first time I write a post, I'm a little excited and at a loss. Please point out the mistakes or areas that can be optimized. Welcome to discuss
QQ: 2657487207

Tags: Javascript Front-end Vue Vue.js

Posted on Thu, 21 Oct 2021 21:16:32 -0400 by Rebel7284