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
Attribute | Description | Accepted Values | Default |
---|---|---|---|
elected | The 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
Attribute | Description | Accepted Values | Default |
---|---|---|---|
value | Data 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