# 來做元件吧!-ip位址設定輸入欄(Vue) ###### tags: `來做元件-vue` ### 前言-為何而來 > **親,為了讓你茁壯,去寫文章吧! by 我的助教** 所以就來吧! ## 今天要來做一個ip 位址設定輸入元件 > 什麼是ip設定輸入?完全聽不懂這是什麼東西呀? 在網路的世界裡,每個端點就像一戶人家,你想去拜訪總要知道人家的地址才行呀!所以在網路世界裡就叫做ip位址,所謂的ip 就是Internet Protocol的縮寫。 那一般分成ipv4以及ipv6 可以看作是郵遞區號3碼跟3+2碼,詳細就不敘述,此次是針對ipv4格式來做設計。 ipv4的格式則是,四段數字用dot分開,每段數字為0–255之間的數值,如 ``` 192.168.0.1 ``` 所以這樣需求就出現了! ### 需求 1. 四個input來做輸入 2. 每格需要限制數值0–255 3. 要有流暢性,輸入完一格接著輸入下一格 ### 搭建環境 那因為目前工作環境是使用vue3.0+TailwindCss 所以就來使用我工作小幫手!Vitawind Creator 來建立好環境吧! [vitawind](https://medium.com/r/?url=https%3A%2F%2Fvitawind.vercel.app%2Fscaffolding%2Fcreator%2F) 使用真的非常簡單只要把指令貼在terminal裡記得先到你想要的路徑上 就能開心一鍵完成! ### 初步layout 好了!建立好環境後,首先就是先切出四格input吧! 那因為這四格都是重複的element所以就來做個component來共用! ```javascript= //component <template> <div class="flex space-x-3"> <input type="text" class="h-full w-full rounded-md border border-gray-300 p-1 text-center md:w-20" /> </div> </template> //parent <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3"> <IpsetInput /> </form> </div> </main> </template> ``` ok~建立好之後會有一個輸入框 ![](https://i.imgur.com/cDimQUI.png) 不過我們還需要dot所以這邊再加上一個label ```javascript= <label class="self-end">.</label> ``` 接著要讓他重複出現四個,所以我們要用到v-for,並且注意要給他key他才能讓每一個輸入框都是unique。 綁定v-model並且在component內使用@input搭配emit把輸入的數值回傳到parent。 ```javascript= //component <script setup> import { defineProps } from "vue"; defineProps({ index: Number, modelValue: String, }); defineEmits(["update:modelValue"]); defineExpose({ dom }); </script> <template> <div class="flex space-x-3"> <input type="text" class="h-full w-full rounded-md border border-gray-300 p-1 text-center md:w-20" maxlength="3" :key="index" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" ref="dom" /> <label v-if="index !== 4" class="self-end">.</label> </div> </template> //parent <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive } from "vue"; const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" /> </form> </div> </main> </template> ``` 這邊的v-model是使用動態的方法去抓取。 另外dot因為其實只有三個,所以下了條件讓他不會出現第四個。 ![](https://i.imgur.com/JZy2ln2.png) 看起來真不錯!而且也能輸入內容了呢! ### 數值驗證功能 接著就要開始增加需求想要的一些限制。 首先我們可以在input內增加**maxlength="3"** 來限制輸入只能三位數。 然後我們希望當使用者填入錯誤的格式的時候要好好的提醒他錯在哪(要把使用者當~~智障~~ 都沒用過的人) 這邊我們先給他一個submit的按鈕讓他有提交功能。這邊喜歡用:class來分組一下class看起來也比較清晰簡短~ ```javascript= //component <form class="flex space-x-3"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> ``` 我們希望在提交的過程中,對input的值做驗證,這裡我選擇使用取得DOM的數值來做,於是我們必須要能夠操作component的DOM 首先我們在component用ref來綁定。 ```javascript= //component <script setup> import { ref, defineProps, defineEmits, defineExpose } from "vue"; const dom = ref(null); defineProps({ index: Number, modelValue: String, }); defineEmits(["update:modelValue"]); defineExpose({ dom }); </script> <template> <div class="flex space-x-3"> <input type="text" class="h-full w-full rounded-md border border-gray-300 p-1 text-center md:w-20" maxlength="3" :key="index" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" ref="dom" /> <label v-if="index !== 4" class="self-end">.</label> </div> </template> ``` 我們命名一個dom綁在inpute上,而為了讓parent可以知道這個ref,所以用了defineExpose讓他暴露出來。 接著就在parent的component一樣使用ref來綁定。 ```javascript= <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive, ref } from "vue"; //此處為綁定的ref const ref1 = ref(null); const ref2 = ref(null); const ref3 = ref(null); const ref4 = ref(null); const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="ref + index" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> </div> </main> </template> ``` <br /> 好了我們就利用vue dev tool 來看一下綁定的DOM狀況 ![](https://i.imgur.com/aUz5VVx.png) **什麼???為什麼是<font color=red>null!!</font> ~~*花惹發~!*~~** 無論怎麼測試,假如想在元件上綁定ref都會是null。 結果在一次的~~團隊合作~~找到原因,在vue最新版本使用上會有這個問題,而目前測試在3.2.22版本左右仍可以使用這樣的方法綁定。 那麼,就只能跳回舊版本嗎??這是個issue該發了嗎? 其實不用,~~因為其實這是個new feature!~~ 在新版本若要動態綁定的話,就得換個方法。 ```javascript= <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive, ref } from "vue"; //在這邊改用一個array型別的ref 然後動態的把抓到的每一個element與指定的ref做綁定 const refs = ref([]); const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="(el) => (refs[index] = el)" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> </div> </main> </template> ``` 當中的黑魔法還沒有很理解,*~~還是感謝飛天小女警的幫忙又平安度過了一天。~~* ![](https://i.imgur.com/oopmNDT.png) 瞧!這不是回來了嗎!~~連爺爺!~~ 於是我們就可以歡天喜地地繼續做下一個步驟。 這邊我們想要先去判斷數值是不是我們想要的,判斷的條件就是必須是0-255之間的數字。 ```javascript= function Validate(val) { if (isNaN(Number(val)) || Number(val) > 255) return false; return true; } ``` 再來要讓這個輸入值送出時去做判斷所以使用一個Submit。 ```javascript= <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive, ref } from "vue"; const refs = ref([]); function Validate(val) { if (isNaN(Number(val)) || Number(val) > 255) return false; return true; } function Submit() { refs.value.map((item) => console.log(Validate(item.dom.value))); } const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="(el) => (refs[index] = el)" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> </div> </main> </template> ``` Ok!現在就看一下測試結果 ![](https://i.imgur.com/HPajZYc.png) 沒錯 當中會有一個false。那麼我們只要有任意一個出現false我們就提示error。 這邊要先在輸入框下方增加一個error提示 ```javascript= <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex h-[152px] flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="(el) => (refs[index] = el)" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> //提示訊息 <strong class="text-red-500" v-if="true">Format is not correct!!</strong> </div> </main> </template> ``` 再來就是只要有一個false就讓他跳出error ```javascript= <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive, ref } from "vue"; const refs = ref([]); function Validate(val) { if (isNaN(Number(val)) || Number(val) > 255 || val === "") return true; } //設置一個isError state const isError = ref(false); function Submit() { //當重新送出時候先reset state isError.value = false; refs.value.map((item) => { if (Validate(item.dom.value)) { isError.value = true; } }); } const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex h-[152px] flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="(el) => (refs[index] = el)" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> <strong class="text-red-500" v-if="isError" >Format is not correct!!</strong > </div> </main> </template> ``` 看起來起了作用了。 ![](https://i.imgur.com/ijOpegM.png) ### 連續移動下一個輸入框 終於到了最後一個階段(夭壽久! 這時候我們就得用新的技能就是vue的watch去監聽現在的狀況,條件是當輸入超過三位數的時候會focus到下一個input,而在刪除的時候會跳回上一個focus。 ```javascript= //parent watch( () => ip.ip1, () => { if (ip.ip1.length >= 3) { ip.ip2 = ip.ip1.substring(3); ip.ip1 = ip.ip1.substring(0, 3); refs.value[2].dom.focus(); } } ); ``` 加了這段之後當輸入完第一個輸入框你不需要tab就能跳到第二格。 並且在第二格以後也設立刪除可以退到前一格的條件。 ```javascript= //parent <script setup> import IpsetInput from "./components/IpsetInput.vue"; import { reactive, ref, watch } from "vue"; const refs = ref([]); function Validate(val) { if (isNaN(Number(val)) || Number(val) > 255 || val === "") return true; } const isError = ref(false); function Submit() { isError.value = false; refs.value.map((item) => { if (Validate(item.dom.value)) { isError.value = true; } }); } const ip = reactive({ ip1: "", ip2: "", ip3: "", ip4: "", }); watch( () => ip.ip1, () => { if (ip.ip1.length >= 3) { ip.ip2 = ip.ip1.substring(3); ip.ip1 = ip.ip1.substring(0, 3); refs.value[2].dom.focus(); } } ); watch( () => ip.ip2, () => { if (ip.ip2.length >= 3) { ip.ip3 = ip.ip2.substring(3); ip.ip2 = ip.ip2.substring(0, 3); refs.value[3].dom.focus(); } if (ip.ip2.length === 0 && ip.ip1.length > 0) { refs.value[1].dom.focus(); } } ); watch( () => ip.ip3, () => { if (ip.ip3.length >= 3) { ip.ip4 = ip.ip3.substring(3); ip.ip3 = ip.ip3.substring(0, 3); refs.value[4].dom.focus(); } if (ip.ip3.length === 0 && ip.ip2.length > 0) { refs.value[2].dom.focus(); } } ); watch( () => ip.ip4, () => { if (ip.ip4.length === 0 && ip.ip3.length > 0) { refs.value[3].dom.focus(); } } ); </script> <template> <main class="flex h-screen w-screen items-center justify-center bg-slate-500"> <div class="min-w-1/6 flex h-[152px] flex-col space-y-5 rounded-md bg-white p-4 shadow-md" > <h2>IP input</h2> <form class="flex space-x-3" @submit.prevent="Submit"> <IpsetInput v-model="ip['ip' + index]" v-for="index in 4" :index="index" :key="index" :ref="(el) => (refs[index] = el)" /> <div> <button :class="[ 'rounded-md px-3 py-1', 'bg-green-500 text-white', 'hover:bg-green-700', 'transition-colors duration-300', ]" > Submit </button> </div> </form> <strong class="text-red-500" v-if="isError" >Format is not correct!!</strong > </div> </main> </template> ``` ok!這個元件就完成了! [Github](https://github.com/alphatero/components-by-vue/commit/a3d30931b3a7d6f8d9e4a9b8c867f23d7bc98dc8) [Vercel Demo](https://components-by-vue.vercel.app/)