從後端到前端之Vue(六)表單組件
- 2020 年 9 月 2 日
- 筆記
- 前端之 —— Vue
表單組件
做項目的時候會遇到一個比較頭疼的問題,一個大表單裡面有好多控制項,一個一個做設置太麻煩,更頭疼的是,需求還總在變化,一會多選、一會單選、一會下拉的,變來變去的煩死寶寶了。
那麼怎麼解決這個問題呢?我們可以做一個組件來搞定這些煩人的事情。我們使用Vue.js基於原生HTML來做一套表單控制項!
前端不管是哪種框架、類庫,其基礎都是HTML、CSS和JavaScript,不管用什麼方式寫項目,我們都有必要先了解一下基礎知識。所以呢我們先來看看HTML5的表單和表單元素都有哪些屬性以及功能。
HTML5原生的表單和表單元素
要想做好表單組件,必須先知道HTML5裡面的表單和表單元素都有哪些屬性,以及屬性的效果和作用,否則的話可能折騰半天才發現,原來HTML5已經自帶了這個功能!
比如要實現這樣一個功能:文本框只能輸入數字,然後要加上兩個按鈕,按一個數值+1,按另一個數值-1。以前要寫js程式碼實現,現在只需要把type改成number就可以了。而且可以對輸入的文字做攔截,非數字根本輸入不進去,這樣就不用我們自己再去寫程式碼實現了。所以磨刀不誤砍柴工,我們先來整理一下,表單和表單屬性都有哪些屬性。
表單屬性
首先是表單(<form>)的屬性,<form>的屬性主要是對錶單元素做一個統一設定,比如表單里的元素是否需要自動完成的功能,以及提交的時候是否需要做驗證等。如果某個表單元素不符合這個統一設定的話,可以給表單元素單獨設置屬性進行說明。這樣就更靈活和方便了。
其他的就是通過submit按鈕對錶單進行一些控制的屬性了。不過這些都是針對錶單提交的,我們現在基本都是ajax,所以這些屬性基本都用不上了。
表單元素的分類
表單元素都有哪些?說到分類就有點頭疼,<intut>一開始理解是文本類的,輸入嘛,結果現實卻很豐滿,提交按鈕也用這個,還有單選和多選也是這個input。多行文本反倒不是這個了,而是單獨的一個。不過不管那麼多了,還是從使用的角度來分類:文本框類和選擇類。
<intut>的type增加了一些新的類型,在PC機的瀏覽器裡面看,區分不是很大,但是到了手機瀏覽器裡面,區分就比較大了,主要是可以控制打開的輸入法的默認模式。比如number,打開輸入法之後,直接就是數字方式,這個就很人性化,方便用戶操作。
我們先看一下分類:
HTML5新增特性
新增的特性(好吧也不算新了,都好多年了),主要是對文本框的增強,增加了一些類型以及輔助功能,比如增加了一個備選框(<datalist>)的功能,這個還是比較實用的吧。
在手機網頁里的展現效果
表單元素在PC瀏覽器里是什麼樣子的,大家很容易看到,那麼在手機瀏覽器里是什麼樣子的呢?先看一下表單整體效果:
注意看那幾個帶下三角的,那個不是下拉列表框,而是日期相關的,可以選擇日期時間等。具體效果我們一個一個看。
-
單行文本type=”text”
還是老樣子的文本框,也是使用最多的表單元素。還是原來的樣子,不貼圖了。 -
多行文本<textarea > </textarea>
一般會被富文本編輯器代替。還是不貼圖了。 -
密碼 type=”password”
這個就不一樣了,系統不同表現也不同,比如某系統會變成系統特定的輸入法,而不是用戶設定的輸入法,並且不讓截屏,所以我只好拍照片了。
-
日期 type=”date”
手機瀏覽器裡面,如何方便的輸入日期?很簡單,只需要設置type=」date」就可以了,至於效果如何嗎,就要看手機系統、版本、瀏覽器、輸入法的了。我手頭測試設備很少,不能全面測試,舉一個作為例子,大家看一下效果圖:
-
日期時間 type=”datetime-local”
這個不僅可以選擇日期,還可以選擇時間。不過要注意type=”datetime”是不行的(各大瀏覽器都不支援),要用type=”datetime-local”才行。不多說了,看圖。
-
時間 type=”time”
單獨選擇時間的。
-
月份 type=”month”
選擇年和月的,不是只有月份。
-
周 type=”week”
同理,也是選擇年和周,一年內第多少周。
-
上傳文件和圖片 type=”file”
上傳功能,有一個特性,可以調用手機攝影機(後攝)拍照,然後還可以訪問拍下來的照片,於是就產生了一種功能,掃碼二維碼。在網上找了半天,已經有測試成功的了。以前以為手機瀏覽器無法掃二維碼呢,現在看來也是可以的。悄悄的透露一下地址://blog.csdn.net/yisuowushinian/article/details/50548742
-
選擇顏色 type=”color”
可以調用系統的調色板,選擇顏色,不過對於我這樣沒有藝術細胞的人,只能是瞎點。
-
數字 type=”number”
這個首先可以設定輸入法為數字狀態,不用再從中文改成數字了,省去用戶的一個步驟,有些版本還可以限制用戶更換狀態。想輸入英文、漢字是沒戲了,不過還是有點小問題,因為小數點、正負號、e都屬於合法字元,所以可以輸入,但是卻沒有判斷數量和位置。
比如小數點可以輸入n個,+-號可以任意位置輸入。這就有點鬱悶了。還有科學計數法的e,這個我都忽略了,看到能輸入e想了半天才想起來想的很周到,但是我輸入eeee,也是可以的。既然都做了限制,為啥不順便限制一下數量呢?
-
電話號碼 type=”tel”
這個嘛,有些版本會設定輸入法為數字狀態,有些版本就沒啥效果了,如果電話號裡面只有數字的話,建議用number的方式。
-
電子郵件 type=”email”
這個也基本沒啥效果。輸入法應該出現@、 .com、 163.com 這類的快捷輸入的,可是沒有發現。不知道其他系統或者輸入法有沒有。
-
網址 type=”url”
可以設定輸入法為英文狀態,但是沒有出現// 、.com、.cn、www 這類的快捷輸入。
-
滑塊 type=”range”
這個好像以前就有,只是不常用。不貼圖了,也沒啥彈出效果,直接拽就好。 -
查詢 type=”search”
這個唯一特點就是輸入資訊後,右側可以出現一個「X」,按一下就會清空文本框。
-
datalist
這個是給文本框提供一個像下拉列表框那樣的備選項,還是比較實用的,只是有一個小問題,他自帶過濾功能,比如輸入 a 那麼只會保留a開頭的備選項,其他的 就都消失了。如果輸入了資訊只會,想換成其他備選項的話,就需要先清空才行,否則其他選項是不會出現了。不過好在我們有search,還記得他有什麼功能嗎?那個x。好了這兩個似乎是絕配了。
Vue組件的基礎知識
表單這一塊為啥要做成組件呢?因為要復用呀。一個表單裡面有很多很多文本框、下拉列表框,一個項目又有很多很多的表單?如果一個一個的設置屬性,是不是太麻煩。如果需求變化了,要先找到這個*.vue,然後再去修改對應的屬性。好麻煩的說。如果做成組件,不僅僅可以達到復用的效果,還可以做到業務需求和程式碼的分離!
組件之hello word
先做一個簡單的組件看看組件到底是啥樣子的。下面是官網的例子(加了一個props),我們來分析一下。
Vue.component('button-counter', {
props:["value"],
data: function() {
return {
count: 0
}
},
template: `<button v-on:click="count++">
You clicked me {{ count }} times.{{value}}</button>`
});
new Vue({ el: '#components-demo' });
<div id="components-demo">
<button-counter value="3"></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
首先使用Vue.component(‘button-counter’, {}) 註冊一個組件。第一個參數就是組件的名稱,後面的參數是組件的實現程式碼,其中包括屬性(props)、內部變數(data)、模板(template)、方法(methods)等。其實組件和vue的實例還是很像的,最明顯的就是多了個屬性(props)和模板。
屬性(props)是把組件外部的數據傳遞到組件內部,是一個很基礎的數據傳遞方式。可以傳遞的數據類型也沒有限制。數字、文本、對象都可以。
模板呢,就是組件內部的結構,編寫方式和vue的實例是很像的。這裡有個主意的地方,一開始我沒注意看,「template:」後面跟的是啥?不是單引號哦,而是鍵盤左上角esc鍵下面的那個。這個符號終於派上用場了。是不是一直沒按過?
用這個符號框起來的可以直接換行,這樣就不用一行一行的「+』」了。
頁面里使用
data使用了function的形式,這個是在組件復用的時候區分多個組件的內部數據的。如果不用function的形式,復用的多個組件,將會共用同一個data值。
然後就是做一個vue的實例,對div進行託管。
表單是使用率最高的一個地方了,項目再小也要有個表單,那麼如何更好的做好表單呢?我們思路就是————依賴注入。這個可不是sql注入攻擊,大家不要弄混淆了。那麼如何實現呢?讓我們一一分析。
組件的特點和優勢
我們為啥要做表單組件呢?首先要看看組件的優勢了,優勢都有哪些呢?封裝和復用、切換表單元素的形式、適配各種UI。
- 復用和封裝
等等,原生的表單元素不是也可以復用嗎?為啥還要弄個組件?這個就要做一個對比了。用原生的方式做一個下拉列表框是啥樣的呢?
<select>
<option value ="1">男</option>
<option value ="0">女</option>
</select>
要寫好幾行太麻煩了,如果封裝一下,各種設置由組件內部解決,外部傳參數就可以了,那麼是不是可以很方便呢?
不管多複雜的表單元素,一行搞定,其他的交給組件內部處理。
- 可以隨意切換「形態」
經理說,這個下拉列表框改成單選的形式吧,這樣用戶選著方便。
於是我們只好改成這樣:
<input type="radio" checked="checked" name="Sex" value="1" />男性
<input type="radio" name="Sex" value="0" />女性
過兩天經理又說了:哎呀,還是下拉列表框好看,你再改回去吧。
。。。。。。
真的嗎?算了,經理說啥就是啥。看在工資的份上我忍!
經理的要求必須做到,沒有討論的餘地。那麼怎麼辦呢?只能改自己了。當然不是翻來覆去的手敲,而是做成組件!
比如:複選改單選,單選改下拉列表框。通過表單元素組件,改一下就可搞定。
- 適配器
現在vue有好多好多UI組件,用哪個好呢?現在我們可以基於原生html封裝一個表單控制項,那麼以後呢?是不是可以針對其他UI組件封裝一個表單元素控制項呢?然後只要能夠保證介面不變,那麼我們依賴這個組件寫的程式碼就不需要改變。
最終要實現——換UI就換UI,不影響業務邏輯的程式碼。
現在看看寫表單組件是不是很有必要了呢?
組件的雙向綁定
對於表單元素,還有一個需要注意的地方,那就是數據的雙向綁定!我們先來個簡單的練練手,對 input 封裝一下。
Vue.component('my-input', {
props:["value"],
template: `<input type="text"
:value="value"
@input="$emit('input',$event.target.value)"
:key="1">`
});
var form = new Vue({
el: '#components-demo',
data:{
formValue:{
v1:'22'
}
}
});
<div id="components-demo">
<my-input v-model="formValue.v1"></my-input>
{{formValue.v1}}
</div>
先看一下頁面里的使用方法,是不是很熟悉的味道,熟悉的v-model,熟悉的大括弧。
然後再看組件內部實現。
首先定義一個屬性(props)value,用於接收組件外面傳遞的數據,然後模板裡面要做兩件事情:接收參數、返回用戶輸入的數據。
v-model是一個語法糖,外面可以直接用,但是組件內部就不能直接用了,必須拆成兩塊才行:一個是 :value給文本框賦值;另一個是監聽input事件(程式碼第五行),然後使用$emit向組件外部傳遞值。$emit有兩個參數,第一個參數是外部監聽的事件,第二個參數(含後面的參數)是要傳遞出去的數值。
可能大家看著有點暈,兩個input是咋回事,我們來改變一下,就都明白了。
@change="$emit('event1',Date.now())"
模板里加入這樣一行。change是文本框的原生change事件,這裡必須用原生的,不能用自定義的。
@change是讓vue監聽這個事件。
$emit 是向組件外面傳遞消息,第一個參數event1,是外部vue監聽的事件,這個可以自定義,寫啥都行。
第二個參數是要傳遞的值,這裡Date.now()是當前時間,寫$event.target.value 的話,就是文本框的值。也就是說,寫啥都行,都可以傳遞出去。
<my-input v-model="formValue.v1" @event1="fun"></my-input>
methods:{
fun:function(data) {
alert(data);
}
}
外部監聽自定義event1事件,然後調用methods里的方法fun。
如果理解了,那麼組件的消息傳遞算是基本掌握了。
表單元素組件
- 需要哪些屬性
表單元素組件需要設置多少屬性呢?這就是苦力活了,既然把input封裝進來了,那麼他的原生屬性都應該能夠支援,就是說要在外部可以設置。那麼怎麼辦呢,如果一個個傳遞那還不如用原生的呢,所以我們設置一個對象屬性,直接傳遞一個對象過來,這樣就簡單了。
熟悉了表單元素的屬性之後,我們可以定義一個json來保存這些屬性:
c1:{
//輔助
controlId: '150', // 編號,區別同一個表單里的其他控制項
controlType: 101, // 用類型編號表示type
colName: '姓名', //中文名稱
isClear: false, //isClear 連續添加時是否恢復默認值
//通用
disabled: false, // 是否禁用
required: true, //必填
pattern: '', //用正則做驗證。
tabIndex:0, // tab 鍵順序
class:'cssTxt input_t1',
title: '', //提示資訊
//多行文本
rows:5, //行數
cols:100, //列數
//文本框類
name:'',
value: '',
placeholder: '請輸入姓名',
readonly: false, //只讀
size: 10, // 字元寬度
maxlength: 20, //最大字元數
autocomplete: 'on', //off
min: 1, // 最小值
max: 100, // 最大值
step: 1, // 步長
multiple: false, //是否可以有多個值,用於上傳文件
listKey:'browsers', //備選文字標識
//選擇類和備選文字的選項
list: [{
"id": "1",
"name": "北京",
"check": false
}
, {
id: '2',
name: '上海',
check: false
}, {
id: '3',
name: '廣州',
check: false
}
]
}
因為json的結構可以非常靈活的組合,所以這裡設計一個大而全的結構,把所有需要的屬性都放在一起,使用的時候,可以根據元素類型靈活取捨。
看到這裡大家可能想,這樣太複雜了,還不如直接使用原生的呢。大家先別急,看完下面這三點然後在下結論。
- 不是所有類型都需要這些屬性,每一個類型用到的並不多。
- 可以寫一個輔助工具來生成這個json,並不需要我們手擼程式碼。
- 可以根據文檔自動生成這個json。
比如純文本框(type=」text」)可以簡化為:
colName:{
controlId: 'colName', //ColumnID
controlType: 116,
class:'cssTxt input_t1',
placeholder: '請填寫標題',
size: 30,
maxlength: 50
}
這樣是不是簡單多了呢?其實最簡單的設置只需要 controlType 即可,其他的都可以不設置,但是也就意味著只能用默認的文本框,沒有辦法進行其他的設置。總之還是要看你要對錶單進行多少設置。
文本框類的表單元素組件
說了這麼多,還沒看到程式碼,是不是等不急了呢?其實程式碼也沒啥好說的,就是用了最笨的方法,一點一點設置屬性。
Vue.component('my-input', {
props:["value","meta"],
data:function(){
return {
type:{ //把編號變成文本的形式
100:'textarea', //多行文本框
101:'text', //單行文本框
102:'password', //密碼
103:'date', //日期
//其他略。。。。。。
}
}
},
methods:{
//text
textInput:function(event, meta){
var returnValue = event.target.value;
//添加自己的監聽事件。本來想寫在一起的,但是不好用,只好分開了。
//vue的回調
this.$emit('input',returnValue);
},
textChange:function(event, meta){
var returnValue = event.target.value;
//添加自己的監聽事件
this.$emit('change', returnValue,event.target, meta);
}
},
template: `<span>
<span v-if="meta.controlType===100">
<!--多行文本框-->
<textarea :id="'c'+meta.controlId"
:name="'c'+meta.controlId"
:class="meta.class"
:readonly="meta.readonly"
:rows="meta.rows"
:cols="meta.cols"
@input="$emit('input',$event.target.value)"
@onkeyup="textChange($event,meta)"
:placeholder="meta.placeholder"
>
</textarea>
</span>
<span v-else-if="meta.controlType>100 && meta.controlType<130 ">
<!--文本框類-->
<input :id="'c'+meta.controlId"
:name="'c'+meta.controlId"
:disabled="meta.disabled"
:class="meta.class"
:type="type[meta.controlType]"
:value="value"
:placeholder="meta.placeholder"
:readonly="meta.readonly"
:size="meta.size"
:maxlength="meta.maxlength"
:autocomplete="meta.autocomplete"
:min="meta.min"
:max="meta.max"
:step="meta.step"
:multiple="meta.multiple"
:list="meta.listKey"
:title="meta.title"
@input="textInput($event,meta)"
@change="textChange($event,meta)"
:key="'ckey_'+meta.controlId">
<!--文本框的備選項-->
<datalist v-if="typeof(meta.listKey)!=='undefined'" :id="meta.listKey">
<option v-for="item in meta.list" :label="item.id" :value="item.name" />
</datalist>
</span>
});
-
屬性
value用於雙向綁定,這個要單獨設置,其他的屬性統統放在meta裡面。這樣介面就固定了,以後需要新的屬性也不用修改介面。 -
內部變數
這個是為了做個替換,因為外部設置的是類型編號,而不是類型名稱,所以內部需要做一個替換,這樣瀏覽器才能識別。
那麼為啥用編號,而不直接用瀏覽器支援的類型呢?因為有些類型要做兩種用途,比如file上傳文件和上傳圖片。兩種方式要做個區分的,比如上傳圖片,可以做個圖片預覽,圖片處理等功能,上傳文件的話,就沒有這些了。所以要做個編號加以區分。另外像多行文本框和下拉列表框用的不是input,沒有type。 -
模板
這裡就很笨了,用v-if根據controlType做判斷,是哪種控制項就渲染對應的模板。然後把屬性一一綁定上就可以了。
然後就是事件的綁定。因為用戶輸入內容後,要通知上層調用者,所以需要加個事件返回用戶輸入值。第一個input是給Vue準備的,加上這個才能實現Vue的雙向綁定。
那麼第二個事件是幹啥的?有的時候我們自己需要知道用戶的輸入操作,依據輸入做些操作,比如聯動下拉列表框。我們要知道第一個下拉列表框的change,然後設置第二個下拉列表框。這個時候就需要我們自己的事件通知。一開始想在一個函數里通知兩個上層事件的,但是沒有成功。所以只好分開了。Emmm,也許可以改成數據驅動的方式,這個還沒太想好。 -
方法
寫了兩個方法,一個是返回給Vue的,實現數據雙向綁定。另一個是給我們自己用的。
選擇類的表單元素組件
選擇類指的是多選組(checkbox)、單選組(radio)、複選框(checkbox)以及下拉列表框。
Vue.component('my-input', {
props:["value","meta"],
methods:{
//select
selectChange:function(event, meta){
var returnValue = '';
var items = event.target.selectedOptions; //選中項的集合
var arr = [];
for (var i=0;i<items.length;i++) {
var item = items[i];
arr.push(item.value);
}
returnValue = arr.join(',');
//添加聯動事件
this.$emit('select', returnValue, meta.nextSelect);
//vue的回調
this.$emit('input',returnValue);
return returnValue;
},
//CheckBox
checkChange: function (event) {
var returnValue = event.target.value;
if (this.meta.controlType === 155) {
//複選框
returnValue = event.target.checked;
}
else{
//修改綁定情況
var selectValue = returnValue;
var arr = [];
for (var key in this.meta.list) {
var item = this.meta.list[key];
if (item.id === selectValue) {
this.meta.list[key].check = event.target.checked;
}
if (item.check) {
arr.push(item.id);
}
}
returnValue = arr.join(',');
}
//調用上級的input事件
this.$emit('input',returnValue);
return returnValue;
},
//radio
radioChange: function (event, meta) {
//單選
var returnValue = '';
var items = event.target.selectedOptions; //選中項的集合
var arr = [];
for (var i=0;i<items.length;i++) {
var item = items[i];
arr.push(item.id);
}
returnValue = arr.join(',');
this.$emit('select', returnValue, meta.nextSelect); //添加聯動事件
return returnValue;
}
},
template: `<span>
<span v-else-if="meta.controlType >= 150 && meta.controlType <= 152 ">
<!--下拉列表框-->
<select :id="'c'+meta.controlId"
:name="'c'+meta.controlId"
:class="meta.class"
:multiple="meta.controlType === 151"
@change="selectChange($event,meta)"
>
<option :key="-2" value="-2" >請選擇</option>
<option
v-for="(item,index) in meta.list"
:key="index"
:value="item.id"
:selected="item.check">
{{item.name}}
</option>
</select>
</span>
<span v-else-if="meta.controlType === 153 ">
<!--單選組 -->
<label role="radio" v-for="item in meta.list" >
<input
type="radio"
:class="meta.class"
:checked="item.check"
:name="'c'+meta.controlId"
:value="item.id"
@input="$emit('input',$event.target.value)"
>
<span>{{item.name}}</span>
</label>
</span>
<span v-else-if="meta.controlType === 154 ">
<!--多選組-->
<label role="checkbox"
v-for="item in meta.list"
class="checkbox_g_t"
:key="'lblchks'+item.id" >
<input :id="'c'+meta.controlId"
type="checkbox"
:name="'c'+meta.controlId"
:class="meta.class"
:value="item.id"
:readonly="meta.readonly"
:key="'chks'+item.id"
@input="checkChange($event)"
>
<span>{{item.name}}</span>
</label>
</span>
<span v-else-if="meta.controlType === 155 ">
<!--複選框-->
<label role="checkbox"
v-for="item in meta.list"
class="checkbox_g_t"
:key="'lblchk'+item.id" >
<input :id="'c'+meta.controlId"
type="checkbox"
:name="'c'+meta.controlId"
:class="meta.class"
:value="item.id"
:readonly="meta.readonly"
:key="'chk'+item.id"
@input="checkChange($event)"
>
<span>{{item.name}}</span>
</label>
</span>
</span>`
});
- 模板
還是老辦法,用v-if判斷渲染哪個模板,然後還是一個一個賦值,然後選項有一個循環,v-for一下就可以了。這裡的選項格式和文本框的備選項格式採用了相同的設置。這樣統一一下比較方便。 - 方法
每類控制項都做一個方法,對應不同的取值方式。不知道有沒有更好的方式,現在用的比較麻煩,期待更好的方法。如果發現了肯定會更新的。
還有個返回值類型的問題,我是習慣返回字元串的形式,比如1,2,3 。而不是數組。因為資料庫里保存的是字元串而不是數組。當然這塊應該能夠靈活一些,打算加一個返回值類型的設置。
輔助工具
這麼複雜的json要怎麼弄?不會告訴我要手擼吧!當然不是,我這麼懶怎麼能手寫呢,當然是弄個工具來輔助了。
輔助工具的思路,首先確定要哪種類型的表單元素,然後根據類型顯示需要設置的屬性,然後就可以點點點了(當然有些屬性需要打幾個字),就可以生成json文件,同時還可以預覽效果。
這個只是第一步哦,後面的還會有根據文檔生成的輔助工具。
文檔在哪裡?做項目總會有個資料庫文檔吧,文檔會描述都有啥表,啥欄位。會介紹一下欄位名稱、欄位類型、欄位大小吧。這樣我們就可以根據這些資訊設置默認的json了。然後不能默認的再點點點一下就可以了。
這個輔助工具就是用的這個表單元素組件寫的,也算是一個實際應用,程式碼比較多,就不貼了。感興趣的話,看下面開源介紹。
開源
源碼下載:Vue表單組件
在線演示:Vue表單組件在線演示
這裡是表單元素組件源碼和demo,還有那個輔助工具。
另外會保持持續更新的,畢竟現在還只是初步學習vue實現的也只是簡單的功能。
下圖是輔助工具的頁面,首先選擇類型,然後預留會有變化,然後按照下面的屬性選擇即可,同時預覽也會有對應的變化。
最後感謝大家的支援!
![感謝感謝]