從0搭建vue3組件庫Input組件
本篇文章將為我們的組件庫添加一個(gè)新成員:Input
組件。其中Input
組件要實(shí)現(xiàn)的功能有:
基礎(chǔ)用法
禁用狀態(tài)
尺寸大小
輸入長(zhǎng)度
可清空
密碼框
帶Icon的輸入框
文本域
自適應(yīng)文本高度的文本域
復(fù)合型輸入框
每個(gè)功能的實(shí)現(xiàn)代碼都做了精簡(jiǎn),方便大家快速定位到核心邏輯,接下來就開始對(duì)這些功能進(jìn)行一一的實(shí)現(xiàn)。
基礎(chǔ)用法
首先先新建一個(gè)input.vue
文件,然后寫入一個(gè)最基本的input
輸入框
<template> <div class="k-input"> <input class="k-input__inner" /> </div> </template>
然后在我們的 vue 項(xiàng)目examples
下的app.vue
引入Input
組件
<template> <div class="Shake-demo"> <Input /> </div> </template> <script lang="ts" setup> import { Input } from "kitty-ui"; </script>
此時(shí)頁面上便出現(xiàn)了原生的輸入框,所以需要對(duì)這個(gè)輸入框進(jìn)行樣式的添加,在input.vue
同級(jí)新建style/index.less
,Input
樣式便寫在這里
.k-input { font-size: 14px; display: inline-block; position: relative; .k-input__inner { background-color: #fff; border-radius: 4px; border: 1px solid #dcdfe6; box-sizing: border-box; color: #606266; display: inline-block; font-size: inherit; height: 40px; line-height: 40px; outline: none; padding: 0 15px; width: 100%; &::placeholder { color: #c2c2ca; } &:hover { border: 1px solid #c0c4cc; } &:focus { border: 1px solid #409eff; } } }
接下來要實(shí)現(xiàn)Input
組件的核心功能:雙向數(shù)據(jù)綁定。當(dāng)我們?cè)?vue 中使用input
輸入框的時(shí)候,我們可以直接使用v-model
來實(shí)現(xiàn)雙向數(shù)據(jù)綁定,v-model
其實(shí)就是value @input
結(jié)合的語法糖。而在 vue3 組件中使用v-model
則表示的是modelValue @update:modelValue
的語法糖。比如Input
組件為例
<Input v-model="tel" />
其實(shí)就是
<Input :modelValue="tel" @update:modelValue="tel = $event" />
所以在input.vue
中我們就可以根據(jù)這個(gè)來實(shí)現(xiàn)Input
組件的雙向數(shù)據(jù)綁定,這里我們使用setup
語法
<template> <div class="k-input"> <input class="k-input__inner" :value="inputProps.modelValue" @input="changeInputVal" /> </div> </template> <script lang="ts" setup> //組件命名 defineOptions({ name: "k-input", }); //組件接收的值類型 type InputProps = { modelValue?: string | number; }; //組件發(fā)送事件類型 type InputEmits = { (e: "update:modelValue", value: string): void; }; //withDefaults可以為props添加默認(rèn)值等 const inputProps = withDefaults(defineProps<InputProps>(), { modelValue: "", }); const inputEmits = defineEmits<InputEmits>(); const changeInputVal = (event: Event) => { inputEmits("update:modelValue", (event.target as HTMLInputElement).value); }; </script>
到這里基礎(chǔ)用法
就完成了,接下來開始實(shí)現(xiàn)禁用狀態(tài)
禁用狀態(tài)
這個(gè)比較簡(jiǎn)單,只要根據(jù)props
的disabled
來賦予禁用類名即可
<template> <div class="k-input" :class="styleClass"> <input class="k-input__inner" :value="inputProps.modelValue" @input="changeInputVal" :disabled="inputProps.disabled" /> </div> </template> <script lang="ts" setup> //... type InputProps = { modelValue?: string | number; disabled?: boolean; }; //... //根據(jù)props更改類名 const styleClass = computed(() => { return { "is-disabled": inputProps.disabled, }; }); </script>
然后給is-disabled
寫些樣式
//... .k-input.is-disabled { .k-input__inner { background-color: #f5f7fa; border-color: #e4e7ed; color: #c0c4cc; cursor: not-allowed; &::placeholder { color: #c3c4cc; } } }
尺寸
按鈕尺寸包括medium
,small
,mini
,不傳則是默認(rèn)尺寸。同樣的根據(jù)props
的size
來賦予不同類名
const styleClass = computed(() => { return { "is-disabled": inputProps.disabled, [`k-input--${inputProps.size}`]: inputProps.size, }; });
然后寫這三個(gè)類名的不同樣式
//... .k-input.k-input--medium { .k-input__inner { height: 36px; &::placeholder { font-size: 15px; } } } .k-input.k-input--small { .k-input__inner { height: 32px; &::placeholder { font-size: 14px; } } } .k-input.k-input--mini { .k-input__inner { height: 28px; &::placeholder { font-size: 13px; } } }
繼承原生 input 屬性
原生的input
有type
,placeholder
等屬性,這里可以使用 vue3 中的useAttrs
來實(shí)現(xiàn)props
穿透.子組件可以通過v-bind
將props
綁定
<template> <div class="k-input" :class="styleClass"> <input class="k-input__inner" :value="inputProps.modelValue" @input="changeInputVal" :disabled="inputProps.disabled" v-bind="attrs" /> </div> </template> <script lang="ts" setup> //... const attrs = useAttrs(); </script>
可清空
通過clearable
屬性、Input
的值是否為空以及是否鼠標(biāo)是否移入來判斷是否需要顯示可清空?qǐng)D標(biāo)。圖標(biāo)則使用組件庫的Icon
組件
<template> <div class="k-input" @mouseenter="isEnter = true" @mouseleave="isEnter = false" :class="styleClass" > <input class="k-input__inner" :disabled="inputProps.disabled" v-bind="attrs" :value="inputProps.modelValue" @input="changeInputVal" /> <div @click="clearValue" v-if="inputProps.clearable && isClearAbled" v-show="isFoucs" class="k-input__suffix" > <Icon name="error" /> </div> </div> </template> <script setup lang="ts"> //... import Icon from "../icon/index"; //... //雙向數(shù)據(jù)綁定&接收屬性 type InputProps = { modelValue?: string | number; disabled?: boolean; size?: string; clearable?: boolean; }; //... const isClearAbled = ref(false); const changeInputVal = (event: Event) => { //可清除clearable (event.target as HTMLInputElement).value ? (isClearAbled.value = true) : (isClearAbled.value = false); inputEmits("update:modelValue", (event.target as HTMLInputElement).value); }; //清除input value const isEnter = ref(true); const clearValue = () => { inputEmits("update:modelValue", ""); }; </script>
清除圖標(biāo)部分 css 樣式
.k-input__suffix { position: absolute; right: 10px; height: 100%; top: 0; display: flex; align-items: center; cursor: pointer; color: #c0c4cc; }
密碼框 show-password
通過傳入show-password
屬性可以得到一個(gè)可切換顯示隱藏的密碼框。這里要注意的是如果傳了clearable
則不會(huì)顯示切換顯示隱藏的圖標(biāo)
<template> <div class="k-input" @mouseenter="isEnter = true" @mouseleave="isEnter = false" :class="styleClass" > <input ref="ipt" class="k-input__inner" :disabled="inputProps.disabled" v-bind="attrs" :value="inputProps.modelValue" @input="changeInputVal" /> <div class="k-input__suffix" v-show="isShowEye"> <Icon @click="changeType" :name="eyeIcon" /> </div> </div> </template> <script setup lang="ts"> //... const attrs = useAttrs(); //... //顯示隱藏密碼框 showPassword const ipt = ref(); Promise.resolve().then(() => { if (inputProps.showPassword) { ipt.value.type = "password"; } }); const eyeIcon = ref("browse"); const isShowEye = computed(() => { return ( inputProps.showPassword && inputProps.modelValue && !inputProps.clearable ); }); const changeType = () => { if (ipt.value.type === "password") { eyeIcon.value = "eye-close"; ipt.value.type = attrs.type || "text"; return; } ipt.value.type = "password"; eyeIcon.value = "browse"; }; </script>
這里是通過獲取input
元素,然后通過它的type
屬性進(jìn)行切換,其中browse
和eye-close
分別是Icon
組件中眼睛開與閉,效果如下
帶 Icon 的輸入框
通過prefix-icon
和suffix-icon
屬性可以為Input
組件添加首尾圖標(biāo)。
可以通過計(jì)算屬性
判斷出是否顯示首尾圖標(biāo),防止和前面的clearable
和show-password
沖突.這里代碼做了
<template> <div class="k-input"> <input ref="ipt" class="k-input__inner" :class="{ ['k-input--prefix']: isShowPrefixIcon }" :disabled="inputProps.disabled" v-bind="attrs" :value="inputProps.modelValue" @input="changeInputVal" /> <div class="k-input__prefix" v-if="isShowPrefixIcon"> <Icon :name="inputProps.prefixIcon" /> </div> <div class="k-input__suffix no-cursor" v-if="isShowSuffixIcon"> <Icon :name="inputProps.suffixIcon" /> </div> </div> </template> <script setup lang="ts"> //... type InputProps = { prefixIcon?: string; suffixIcon?: string; }; //... //帶Icon輸入框 const isShowSuffixIcon = computed(() => { return ( inputProps.suffixIcon && !inputProps.clearable && !inputProps.showPassword ); }); const isShowPrefixIcon = computed(() => { return inputProps.prefixIcon; }); </script>
相關(guān)樣式部分
.k-input__suffix, .k-input__prefix { position: absolute; right: 10px; height: 100%; top: 0; display: flex; align-items: center; cursor: pointer; color: #c0c4cc; font-size: 15px; } .no-cursor { cursor: default; } .k-input--prefix.k-input__inner { padding-left: 30px; } .k-input__prefix { position: absolute; width: 20px; cursor: default; left: 10px; }
在app.vue
中使用效果如下
<template> <div class="input-demo"> <Input v-model="tel" suffixIcon="edit" placeholder="請(qǐng)輸入內(nèi)容" /> <Input v-model="tel" prefixIcon="edit" placeholder="請(qǐng)輸入內(nèi)容" /> </div> </template> <script lang="ts" setup> import { Input } from "kitty-ui"; import { ref } from "vue"; const tel = ref(""); </script> <style lang="less"> .input-demo { width: 200px; } </style>
文本域
將type
屬性的值指定為textarea
即可展示文本域模式。它綁定的事件以及屬性和input
基本一樣
<template> <div class="k-textarea" v-if="attrs.type === 'textarea'"> <textarea class="k-textarea__inner" :style="textareaStyle" v-bind="attrs" ref="textarea" :value="inputProps.modelValue" @input="changeInputVal" /> </div> <div v-else class="k-input" @mouseenter="isEnter = true" @mouseleave="isEnter = false" :class="styleClass" > ... </div> </template>
樣式基本也就是focus
,hover
改變 border 顏色
.k-textarea { width: 100%; .k-textarea__inner { display: block; padding: 5px 15px; line-height: 1.5; box-sizing: border-box; width: 100%; font-size: inherit; color: #606266; background-color: #fff; background-image: none; border: 1px solid #dcdfe6; border-radius: 4px; &::placeholder { color: #c2c2ca; } &:hover { border: 1px solid #c0c4cc; } &:focus { outline: none; border: 1px solid #409eff; } } }
可自適應(yīng)高度文本域
組件可以通過接收autosize
屬性來開啟自適應(yīng)高度,同時(shí)autosize
也可以傳對(duì)象形式來指定最小和最大行高
type AutosizeObj = { minRows?: number maxRows?: number } type InputProps = { autosize?: boolean | AutosizeObj }
具體實(shí)現(xiàn)原理是通過監(jiān)聽輸入框值的變化來調(diào)整textarea
的樣式,其中用到了一些原生的方法譬如window.getComputedStyle(獲取原生css對(duì)象)
,getPropertyValue(獲取css屬性值)
等,所以原生js
忘記的可以復(fù)習(xí)一下
... const textareaStyle = ref<any>() const textarea = shallowRef<HTMLTextAreaElement>() watch(() => inputProps.modelValue, () => { if (attrs.type === 'textarea' && inputProps.autosize) { const minRows = isObject(inputProps.autosize) ? (inputProps.autosize as AutosizeObj).minRows : undefined const maxRows = isObject(inputProps.autosize) ? (inputProps.autosize as AutosizeObj).maxRows : undefined nextTick(() => { textareaStyle.value = calcTextareaHeight(textarea.value!, minRows, maxRows) }) } }, { immediate: true })
其中calcTextareaHeight
為
const isNumber = (val: any): boolean => { return typeof val === 'number' } //隱藏的元素 let hiddenTextarea: HTMLTextAreaElement | undefined = undefined //隱藏元素樣式 const HIDDEN_STYLE = ` height:0 !important; visibility:hidden !important; overflow:hidden !important; position:absolute !important; z-index:-1000 !important; top:0 !important; right:0 !important; ` const CONTEXT_STYLE = [ 'letter-spacing', 'line-height', 'padding-top', 'padding-bottom', 'font-family', 'font-weight', 'font-size', 'text-rendering', 'text-transform', 'width', 'text-indent', 'padding-left', 'padding-right', 'border-width', 'box-sizing', ] type NodeStyle = { contextStyle: string boxSizing: string paddingSize: number borderSize: number } type TextAreaHeight = { height: string minHeight?: string } function calculateNodeStyling(targetElement: Element): NodeStyle { //獲取實(shí)際textarea樣式返回并賦值給隱藏的textarea const style = window.getComputedStyle(targetElement) const boxSizing = style.getPropertyValue('box-sizing') const paddingSize = Number.parseFloat(style.getPropertyValue('padding-bottom')) + Number.parseFloat(style.getPropertyValue('padding-top')) const borderSize = Number.parseFloat(style.getPropertyValue('border-bottom-width')) + Number.parseFloat(style.getPropertyValue('border-top-width')) const contextStyle = CONTEXT_STYLE.map( (name) => `${name}:${style.getPropertyValue(name)}` ).join(';') return { contextStyle, paddingSize, borderSize, boxSizing } } export function calcTextareaHeight( targetElement: HTMLTextAreaElement, minRows = 1, maxRows?: number ): TextAreaHeight { if (!hiddenTextarea) { //創(chuàng)建隱藏的textarea hiddenTextarea = document.createElement('textarea') document.body.appendChild(hiddenTextarea) } //給隱藏的teatarea賦予實(shí)際textarea的樣式以及值(value) const { paddingSize, borderSize, boxSizing, contextStyle } = calculateNodeStyling(targetElement) hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`) hiddenTextarea.value = targetElement.value || targetElement.placeholder || '' //隱藏textarea整個(gè)高度,包括內(nèi)邊距padding,border let height = hiddenTextarea.scrollHeight const result = {} as TextAreaHeight //判斷boxSizing,返回實(shí)際高度 if (boxSizing === 'border-box') { height = height + borderSize } else if (boxSizing === 'content-box') { height = height - paddingSize } hiddenTextarea.value = '' //計(jì)算單行高度 const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize if (isNumber(minRows)) { let minHeight = singleRowHeight * minRows if (boxSizing === 'border-box') { minHeight = minHeight + paddingSize + borderSize } height = Math.max(minHeight, height) result.minHeight = `${minHeight}px` } if (isNumber(maxRows)) { let maxHeight = singleRowHeight * maxRows! if (boxSizing === 'border-box') { maxHeight = maxHeight + paddingSize + borderSize } height = Math.min(maxHeight, height) } result.height = `${height}px` hiddenTextarea.parentNode?.removeChild(hiddenTextarea) hiddenTextarea = undefined return result }
這里的邏輯稍微復(fù)雜一點(diǎn),大致就是創(chuàng)建一個(gè)隱藏的textarea
,然后每次當(dāng)輸入框值發(fā)生變化時(shí),將它的value
賦值為組件的textarea
的value
,最后計(jì)算出這個(gè)隱藏的textarea
的scrollHeight
以及其它padding
之類的值并作為高度返回賦值給組件中的textarea
最后在app.vue
中使用
<template> <div class="input-demo"> <Input v-model="tel" :autosize="{ minRows: 2 }" type="textarea" suffixIcon="edit" placeholder="請(qǐng)輸入內(nèi)容" /> </div> </template>
復(fù)合型輸入框
我們可以使用復(fù)合型輸入框來前置或者后置我們的元素,如下所示
這里我們借助 vue3 中的slot
進(jìn)行實(shí)現(xiàn),其中用到了useSlots
來判斷用戶使用了哪個(gè)插槽,從而展示不同樣式
import { useSlots } from "vue"; //復(fù)合輸入框 const slots = useSlots();
同時(shí)template
中接收前后兩個(gè)插槽
<template> <div class="k-input" @mouseenter="isEnter = true" @mouseleave="isEnter = false" :class="styleClass" > <div class="k-input__prepend" v-if="slots.prepend"> <slot name="prepend"></slot> </div> <input ref="ipt" class="k-input__inner" :class="inputStyle" :disabled="inputProps.disabled" v-bind="attrs" :value="inputProps.modelValue" @input="changeInputVal" /> <div class="k-input__append" v-if="slots.append"> <slot name="append"></slot> </div> </div> </template> <script setup lang="ts"> import { useSlots } from "vue"; const styleClass = computed(() => { return { ["k-input-group k-input-prepend"]: slots.prepend, ["k-input-group k-input-append"]: slots.append, }; }); //復(fù)合輸入框 const slots = useSlots(); </script>
最后給兩個(gè)插槽寫上樣式就實(shí)現(xiàn)了復(fù)合型輸入框啦
.k-input.k-input-group.k-input-append, .k-input.k-input-group.k-input-prepend { line-height: normal; display: inline-table; width: 100%; border-collapse: separate; border-spacing: 0; .k-input__inner { border-radius: 0 4px 4px 0; } //復(fù)合輸入框 .k-input__prepend, .k-input__append { background-color: #f5f7fa; color: #909399; vertical-align: middle; display: table-cell; position: relative; border: 1px solid #dcdfe6; border-radius: 4 0px 0px 4px; padding: 0 20px; width: 1px; white-space: nowrap; } .k-input__append { border-radius: 0 4px 4px 0px; } } .k-input.k-input-group.k-input-append { .k-input__inner { border-top-right-radius: 0px; border-bottom-right-radius: 0px; } }
在app.vue
中使用
<template> <div class="input-demo"> <Input v-model="tel" placeholder="請(qǐng)輸入內(nèi)容"> <template #prepend> http:// </template> </Input> <Input v-model="tel" placeholder="請(qǐng)輸入內(nèi)容"> <template #append> .com </template> </Input> </div> </template>
總結(jié)
一個(gè)看似簡(jiǎn)單的Input
組件其實(shí)包含的內(nèi)容還是很多的,做完之后會(huì)發(fā)現(xiàn)對(duì)自己很多地方都有提升和幫助。
如果你對(duì)vue3組件庫開發(fā)也感興趣的話可以關(guān)注我,組件庫的所有實(shí)現(xiàn)細(xì)節(jié)都在以往文章里,包括環(huán)境搭建
,自動(dòng)打包發(fā)布
,文檔搭建
,vitest單元測(cè)試
等等。
源碼地址
kitty-ui: 一個(gè)使用Vite+Ts搭建的Vue3組件庫
到此這篇關(guān)于從0搭建vue3組件庫: Input組件的文章就介紹到這了,更多相關(guān)vue3組件庫Input組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用vis-timeline繪制甘特圖并實(shí)現(xiàn)時(shí)間軸的中文化(案例代碼)
這篇文章主要介紹了使用vis-timeline繪制甘特圖并實(shí)現(xiàn)時(shí)間軸的中文化(案例代碼),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-02-02vue項(xiàng)目運(yùn)行npm?install報(bào)錯(cuò)問題及解決
這篇文章主要介紹了vue項(xiàng)目運(yùn)行npm?install報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08elementUI多選框反選的實(shí)現(xiàn)代碼
這篇文章主要介紹了elementUI多選框反選的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04vue3中使用vuex和vue-router的詳細(xì)步驟
這篇文章主要介紹了vue3中使用vuex和vue-router的步驟,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12vue滾動(dòng)條滾動(dòng)到頂部或者底部的方法
這篇文章主要給大家介紹了關(guān)于vue滾動(dòng)條滾動(dòng)到頂部或者底部的相關(guān)資料,文中通過代碼示例介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08Vue路由跳轉(zhuǎn)傳參或者打開新頁面跳轉(zhuǎn)問題
這篇文章主要介紹了Vue路由跳轉(zhuǎn)傳參或者打開新頁面跳轉(zhuǎn)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03vue結(jié)合el-upload實(shí)現(xiàn)騰訊云視頻上傳功能
這篇文章主要介紹了vue結(jié)合el-upload實(shí)現(xiàn)騰訊云視頻上傳功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07關(guān)于Vue3中defineProps用法圖文詳解
在vue3中組件傳參有很多種方式,和v2大差不差,但是有的地方還是有很多的區(qū)別,下面這篇文章主要給大家介紹了關(guān)于Vue3中defineProps用法的相關(guān)資料,需要的朋友可以參考下2022-11-11Vue自定義render統(tǒng)一項(xiàng)目組彈框功能
這篇文章主要介紹了Vue自定義render統(tǒng)一項(xiàng)目組彈框功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06