Blazor和Vue對比學習(基礎1.9):表單輸入繫結和驗證,VeeValidate和EditFrom

functionMC 發表於 2022-05-19
Vue

這是基礎部分的最後一章,內容比較簡單,算是為基礎部分來個HappyEnding。我們分三個部分來學習:

  • 表單輸入繫結
  • Vue的表單驗證:VeeValidate
  • Blazor的表單驗證:EditForm

 

一、表單輸入繫結

表單輸入主要涉及前面章節的雙向繫結,但不需要我們自定義,直接使用就行。如果已經掌握了“1.6章節雙向繫結”,那這部分的知識點簡直“灑灑水”。Vue的v-model指令,為所有原生的HTML元件做了一些特殊處理,不僅支援雙向繫結,也支援check或radio的分組。而Blazor,對原生HTML並沒有做特殊處理,所以實現起來比較麻煩(見下例)。對於Blazor,推薦直接使用內建的表單元件,不僅可以直接雙向繫結,還支援驗證。以下例子,為原始HTML標籤的繫結。

//Vue
//以下程式碼展示多行文字框、單行文字框、單行文字框-去空格、單行文字框-數字、核取方塊、多選框、單選框、單選選擇器、多選選擇器的基本使用

<template>
    //多行文字框:
    <textarea v-model="textareaMsg" placeholder="請輸入"></textarea>
    <span>邏輯層值:{{textareaMsg}}</span>

    //單行文字框:
    <input type="text" v-model="inputMsg" placeholder="請輸入" />
    <span>邏輯層值:{{inputMsg}}</span>

    //單行文字框-去空格:
    <input type="text" v-model.trim="inputMsgTrim" placeholder="請輸入" />
    <span>邏輯層值:{{inputMsgTrim}}</span>

    //單行文字框-數字:
    <input type="number" v-model.number="inputNumber" placeholder="請輸入" />
    <span>邏輯層值:{{inputNumber}}</span>

    //核取方塊:
    <input type="checkbox" id="checkbox" v-model="isCheck" />
    <label for="checkbox">{{isCheck?"已選狀態":"未選狀態"}} </label>
    <span>邏輯層值:{{isCheck}}</span>

    //多選框:
    <input type="checkbox" id="vuecheck" value="vue" v-model="checkValue" />
    <label for="vuecheck">vue</label>
    <input type="checkbox" id="blazorcheck" value="blazor" v-model="checkValue" />
    <label for="blazorcheck">blazor</label>
    <span>邏輯層值:{{checkValue}}</span>

    //單選框:
    <input type="radio" id="vueradio" value="vue" v-model="radioValue" />
    <label for="vueradio">vue</label>
    <input type="radio" id="blazorradio" value="blazor" v-model="radioValue" />
    <label for="blazorradio">blazor</label>
    <span>邏輯層值:{{radioValue}}</span>

    //單選選擇器:
    <select v-model="oneSelected">
        <option disabled value="">請選擇</option>
        <option>vue</option>
        <option>blazor</option>
    </select>
    <span>邏輯層值:{{oneSelected}}</span>

    //多選選擇器:
    <select v-model="moreSelected" multiple>
        <option>vue</option>
        <option>blazor</option>
    </select>
    <span>邏輯層值:{{moreSelected}}</span>
</template>

<script setup>
import {ref} from 'vue'
const textareaMsg = ref('') //多行文字框,字串
const inputMsg = ref('') //單行文字框,字串
const inputMsgTrim = ref('') //單行文字框-去空格,字串
const inputNumber = ref() //單行文字框-數字,數值
const isCheck = ref(false) //單選框,布林
const checkValue = ref([]) //多選框,陣列
const radioValue = ref('') //單選框,字串
const oneSelected = ref('') //單選選擇器,字串
const moreSelected = ref([]) //多選選擇器,陣列
</script>
//Blazor
//Blazor的@bind對check和rado的分組,並沒有做出特殊處理

<div class="content">
    多行文字框:
    <textarea @bind="textareaMsg" @bind:event="oninput"></textarea>
    <h6>邏輯層值:@textareaMsg</h6>
</div>

<div class="content">
    單行文字框:
    <input @bind="inputMsg" @bind:event="oninput" />
    <h6>邏輯層值:@inputMsg</h6>
</div>

<div class="content">
    單行文字框-去空格:
    <input @bind="InputTrimMsg" @bind:event="oninput" />
    <h6>邏輯層值:@InputTrimMsg</h6>
</div>

<div class="content">
    單行文字框-數字:
    <input type="number" @bind="inputNumber" @bind:event="oninput" />
    <h6>邏輯層值:@inputNumber</h6>
</div>

<div class="content">
    核取方塊:
    <input type="checkbox" id="isChecked" @bind="isChecked" />
    <label for="isChecked">選中</label>
    <h6>邏輯層值:@(isChecked?"選中狀態":"未選狀態")</h6>
</div>

//多選框,無法使用@bind。通過雙向繫結的屬性和事件來實現【後面補充一種使用@bind的實現方式】
<div class="content">
    多選框:
    <input type="checkbox" id="vuecheck" name="moreLikeCodes" value="@moreLikeCodes" @onchange=Vuecheck />
    <label for="vuecheck">vue</label>
    <input type="checkbox" id="blazorcheck" name="moreLikeCodes" value="@moreLikeCodes" @onchange=Blazorcheck />
    <label for="blazorcheck">blazor</label>
    <h6>邏輯層值:@(String.Join(",", moreLikeCodes.ToArray()))</h6>
</div>

//單選框,無法使用@bind。通過雙向繫結的屬性和事件來實現【後面補充一種使用@bind的實現方式】
<div class="content">
    單選框:
    <input type="radio" id="vueradio" name="oneLikeCode" value="@oneLikeCode" @[email protected](()=>{oneLikeCode="vue";}) />
    <label for="vueradio">vue</label>
    <input type="radio" id="blazorradio" name="oneLikeCode" value="@oneLikeCode" @[email protected](()=>{oneLikeCode="blazor";}) />
    <label for="blazorradio">blazor</label>
    <h6>邏輯層值:@oneLikeCode</h6>
</div>

<div class="content">
    單選選擇器:
    <select @bind="singleSelected">
        <option value="">請選擇</option>
        <option value="vue">vue</option>
        <option value="blazor">blazor</option>
    </select>
    <h6>邏輯層值:@singleSelected</h6>
</div>

@code {

    private string textareaMsg = "";
    private string inputMsg = "";
    public int inputNumber { get; set; }

    //通過設定set,實現去除空格功能
    private string inputTrimMsg = "";
    public string InputTrimMsg
    {
        get { return inputTrimMsg; }
        set { inputTrimMsg = value.Trim(); }
    }

    private bool isChecked;

    //多選框
    private List<string> moreLikeCodes = new List<string>();
    private void Vuecheck(ChangeEventArgs e)
    {
        if (Convert.ToBoolean(e.Value))
        {
            moreLikeCodes.Add("vue");
        }
        else
        {
            moreLikeCodes.Remove("vue");
        }
    }
    private void Blazorcheck(ChangeEventArgs e)
    {
        if (Convert.ToBoolean(e.Value))
        {
            moreLikeCodes.Add("blazor");
        }
        else
        {
            moreLikeCodes.Remove("blazor");
        }
    }

    //單選框
    private string? oneLikeCode;
    //單選下拉框
    private string? singleSelected;
}
//Blazor
//補充另外一種實現多選框的方式,直接用@bind指令

@if (selectedLikeCodes is not null)
{
    foreach (var item in selectedLikeCodes)
    {
        <input type="checkbox" @bind="@item.IsSelected" />
        <span>@item.Name</span>
    }

    <h5>我喜歡的程式碼:</h5>
    @foreach (var item in selectedLikeCodes.Where(c => c.IsSelected))
    {
        <div>@item.Name</div>
    }
}


@code {

    private List<LikeCode> selectedLikeCodes = new List<LikeCode>();

    private List<LikeCode> likeCodeItems = new List<LikeCode>
    {
        new LikeCode{Name="vue",IsSelected=false},
        new LikeCode{Name="blazor",IsSelected=false},
        new LikeCode{Name="maui",IsSelected=false}
    };

    protected override void OnInitialized()
    {
        selectedLikeCodes = likeCodeItems.Select(e => new LikeCode { Name = e.Name, IsSelected = e.IsSelected }).ToList();
    }

    private class LikeCode
    {
        public string? Name;
        public bool IsSelected;
    }
}

 

 

二、Vue的表單驗證:VeeValidate

Vue沒有內建表單驗證功能,官方推薦使用第三方庫VeeValidate。而Blazor,封裝了一套內建表單元件,自帶驗證功能,我們主要學習這套元件的使用。本章節關於表單驗證的知識點,均只涉及基本的使用,關於進一步的深入使用、理解、甚至定製擴充套件功能,我們將在進階學習中繼續展開。

 

1、VeeValidate的安裝、元件式的簡單使用

VeeValidate實現表單驗證有兩種方式,一種是元件式,主要使用<Form><Field>;另外一種是組合式API的方式,可以和其它表單元件更好融合,是更推薦的方式,後面的學習也以此為主。特別說一下VeeValidate的安裝,有些坑,如下所示:

1)網路上搜尋VeeValidate的安裝使用,大多數會提示:①安裝,npm install vee-validate;②在main.js中引入,import VeeValidate from vee-validate③在main.js中安裝外掛,Vue.use( VeeValidate)app.use( VeeValidate)。實際上,在Vue3,只要①就可以了。估計是Vue3引入組合式API之後,VeeValidate也是以組合式API的方式引入。如果是Vue2,可能還是需要三步來引入。

2)完成①後,直接在元件中import就可以使用相關元件或API,以元件式為例:import { Field, Form, ErrorMessage } from 'vee-validate'

3VeeValidate官方推薦結合yup來使用。yup可以理解為驗證函式的語法糖,如下例所示(使用元件方式)。yup需要另外安裝,npm install yup;然後在元件中引入,import * as yup from yup’。我們後面的案例,均使用更簡單的驗證方式yup。

//函式驗證和yup驗證方式的對比
//【特別說明一下】:以往我們使用HTML表單元件,都需要v-model邏輯層的響應式資料,但在VeeValidate裡,你找不到了
//<Form>幫我們託管了,資料也可以通過API拿到!
//欄位級驗證 <template> <Form> <Field name="field" :rules="isRequired" /> <ErrorMessage name="field" /> </Form> </template> <script setup> import { Field, Form, ErrorMessage } from 'vee-validate' import * as yup from 'yup' //函式驗證方式,value為欄位輸入值 function isRequired(value){ if (value && value.trim()){ return ture } return 'This is required' } //yup驗證方式 const isRequired = yup.string().required() </script> //表單級驗證 <template> <Form @submit="submit" :validation-schema="mySchema"> <Field name="email" /> <ErrorMessage name="email" /> <Field name="password" type="password" /> <ErrorMessage name="password" /> <button>Submit</button> </Form> </template> <script setup> import { Field, Form, ErrorMessage } from 'vee-validate' import * as yup from 'yup' //函式驗證方式 const mySchema = { email(value) { //驗證規則... }, password(value){ //驗證規則... } } //yup驗證方式 const mySchema = yup.object({ email: yup.string().required().email(), password: yup.string().required().min(8), }) </script>

 

2、VeeValidate的組合式API使用

VeeValidate起頭的API主要有兩個,useFielduseForm。從字面就知道,一個是關於欄位的,一個是關於表單的。

1)useFieldconst{ value, errorMessage } = useField('fieldName', yup.string().required().min(8))

①接受:欄位name、欄位級的驗證方法、欄位級初始值等入參

export:欄位value、錯誤資訊errorMessage、欄位元資訊meta、值變化處理函式handleChange、欄位重置resetField等一大堆API。使用物件解構的方式來接收,大多數解構出來的值,也是響應式。

2)useFielduseForm:const{ handleSubmit, isSubmitting } = useForm()

①接受:表單級驗證方法物件、表單級初始值物件等入參

export:表單提交handleSubmit、提交狀態isSubmitting、表單元資訊meta等一大堆API。使用物件解構的方式來接收,大多數解構出來的值,也是響應式。

3)如上所述,VeeValidate的使用套路很簡單,直來直去的三步走,入參-解構拿API-使用API

 4)下面來個大例子,大多數API都有涉及和講解,很全了哦!!!

<template>    
    <form @submit="onSubmit">
        <div class="content50">
          *姓名:
            <input v-model="name" />
            <span>{{nameError}}</span>
        </div>        
        <div class="content50">
          *性別:
            <input type="radio" id="sexmale" value="1" name="sex" v-model="sex" />
            <label for="sexmale">男</label>
            <input type="radio" id="sexfemale" value="0" name="sex"  v-model="sex" />
            <label for="sexfemale">女</label>
            <span>{{sexError}}</span>
        </div>        
        <div class="content50">
          *年齡:
            <input type="number" v-model="age" />
            <span>{{ageError}}</span>
        </div>        
        <div class="content50">
          是否已婚:
            <input type="checkbox" v-model="marriage" />
            <span>{{marriageError}}</span>
        </div>        
        <div class="content50">
          學歷:
            <select v-model="graduate">
                <option value="" disabled>請選擇</option>
                <option value="小學">小學</option>
                <option value="初中">初中</option>
                <option value="高中">高中</option>
                <option value="本科">本科</option>
                <option value="研究生">研究生</option>
            </select>
            <span>{{graduateError}}</span>
        </div>        
        <div class="content50">
          入職日期:
            <input type="date" v-model="entrydate" />
            <span>{{entrydateError}}</span>
        </div>        
        <div class="content50">
          愛好:
            <input type="checkbox" id="likebasketball" value="basketball" v-model="likes" />
            <label for="likebasketball">basketball</label>
            <input type="checkbox" id="likefootball" value="football" v-model="likes" />
            <label for="likefootball">football</label>
            <input type="checkbox" id="likeswim" value="swim" v-model="likes" />
            <label for="likeswim">swim</label>
            <input type="checkbox" id="likecode" value="code" v-model="likes" />
            <label for="likecode">code</label>
            <span>{{likesError}}</span>
        </div>        
        <div class="content50">
          自我介紹(使用handleChange,失焦時才觸發驗證,改變驗證的敏感性):
            <textarea @change="handleChange" :value="introduce"></textarea>
            <span>{{introduceError}}</span>
        </div>
        <input type="submit" />
    </form>
    
    <div class="content200">
        <h3>表單所有欄位值</h3>
        <h6>{{values}}</h6>
        <h3>表單所有錯誤資訊</h3>
        <h6>{{errors}}</h6>
    </div>    
</template>

<script setup>
import {ref} from 'vue'
import { useForm,useField } from 'vee-validate';
import * as yup from 'yup';

// 不需要宣告以下響應式變數,這些工作由Vee隱式完成
// const name = ref('')
// const sex = ref()
// .......


//1、表單設定===============================================================================
//1.1初始值,可以省略,但未了控制“多選框”的陣列繫結,需要設定
const initialValues = {
    likes:[],
}
//1.2初始錯誤,首次驗證錯誤時顯示,可以省略,錯誤資訊應該在多語言裡設定,此篇文章略過,大家可以去查文件
const initialErrors ={
    name:'姓名必填,且不能超過20字',
}
//1.3驗證規則,核心
const validationSchema = yup.object({
    name:yup.string().required().max(20),
    sex:yup.number().required().integer().min(0).max(1),
    age:yup.number().required().integer().min(0).max(100),
    marriage:yup.boolean(),
    graduate:yup.string(),
    introduce:yup.string().min(50),
    entrydate:yup.date(),

})
//1.4表單useForm
//入參:validationSchema-驗證規則,initialValues-初始值,initialErrors-首次驗證錯誤時顯示
//API①:values-表單各欄位值,errors-表單驗證錯誤資訊
//API②:handleSubmit-表單驗證成功後回撥,isSubmitting-驗證進度,submitCount-驗證次數
//API③:resetForm-表單重置,不入參則以初始值重置,可以物件方式以指定值重置
//API④:setValues-以物件方式設定表單各欄位值,setFieldValue-單獨設定指定欄位值,入參為欄位名和值
//API⑤:setErrors-以物件方式設定表單各欄位錯誤資訊,setFieldError-單獨設定指定欄位錯誤資訊,入參為欄位名和錯誤資訊
const { values,errors,handleSubmit,isSubmitting,submitCount,resetForm,setValues,setFieldValue,setErrors,setFieldError } = useForm({
    validationSchema: validationSchema,
    initialValues: initialValues,
    initialErrors: initialErrors,
})



//2、欄位設定===============================================================================
//欄位useField
//入參:欄位名稱【注,入參的第二個引數可以設定驗證規則,如useField('name',yup.string().required()),但一般在表單層級上統一設定】
//API①:value-繫結欄位(值為ref響應式,用computedRef包裝),errorMessage-錯誤資訊
//API②:handleChange-Vee的驗證只根據欄位值變化響應,和DOM事件無關。handleChange可以將DOM事件與驗證響應關聯,從而改變驗證響應的時機
const {value:name,errorMessage:nameError} = useField('name')
const {value:sex,errorMessage:sexError} = useField('sex')
const {value:age,errorMessage:ageError} = useField('age')
const {value:marriage,errorMessage:marriageError} = useField('marriage')
const {value:graduate,errorMessage:graduateError} = useField('graduate')
const {value:entrydate,errorMessage:entrydateError} = useField('entrydate')
const {value:likes,errorMessage:likesError} = useField('likes')
const {value:introduce,errorMessage:introduceError,handleChange} = useField('introduce')


//3、表單行為===============================================================================
//提交表單:驗證成功的處理和驗證失敗的處理
//第一個引數:驗證成功回撥,入參為表單資料
//第二個引數:驗證失敗回撥,入參為表單資料、錯誤資訊、驗證結果
//驗證成功後,呼叫API,resetForm(),重置表單
const onSubmit = handleSubmit((values)=>{
    console.log("傳送表單資料"+values)
    resetForm(); //重置表單
},(values,errors,results)=>{
    console.log("表單資料"+values)
    console.log("錯誤資訊"+errors)
    console.log("驗證結果"+results)
})

</script>

 

 

 

三、Blazor的表單驗證:EditForm

Blazor自帶驗證體系,使用起來非常方便,一般的驗證功能都能實現,如果想實現更復雜的邏輯,也可以進一步深入。有幾點需要說明:

1、需要使用Blazor自帶的一整套元件,包括<EditForm><InputText>等,進階的話,也可以繼承InputBase<T>自己開發

2、驗證的基本過程:

1)表單元件初始化後,會建立一個EditContext物件,這是表單的上下文,不僅以鍵值對<FileIdentifier,FieldState>的方式儲存著各個欄位的元資訊,還提供了一系列方法

2)這個EditContext以CascadingValue聯級的方式提供給表單元件使用

3)每次有資料更新時,觸發驗證,並建立一個新的EditContext,驗證結果會儲存到一個ValidationMessageStore

3、如果表單元件繫結了集合或複雜型別,將無法驗證。這個時候有一個實驗性的元件可以使用,<ObjectGraphDataAnnotationsValidator />

4、接下來和Vue一樣,直接幹一個例項!!!

<div class="container">
    //EditForm的Model屬性,可以直接繫結employee物件,這裡繫結editContext的原因是為了拿到EditContext上下文
    //提交表單的三種情況:OnValidSubmit-驗證成功回撥、OnInValidSubmit-驗證失敗回撥、OnSubmit-無論成功或失敗均回撥
    //OnValidSubmit和OnInValidSubmit可以一起使用
    <EditForm Model="@editContext" OnValidSubmit="@MyValidSubmit">

        //啟動驗證
        <DataAnnotationsValidator/>

        //表單級驗證資訊的容器
        <ValidationSummary/>

        <div class=content50>
            *姓名:
            <InputText id="name" @bind-Value="employee.Name"></InputText>
            //欄位級驗證資訊容器
            <ValidationMessage [email protected](()=>employee.Name) />
        </div>

        <div class="content50">
            *性別:
            <InputRadioGroup @bind-Value="employee.Sex">
                <InputRadio Value="@Sex.male">male</InputRadio>
                <InputRadio Value="@Sex.female">female</InputRadio>
            </InputRadioGroup>
        </div>

        <div class="content50">
            *年齡:
            <InputNumber @bind-Value="employee.Age"></InputNumber>
        </div>

        <div class="content50">
            *學歷:
            <InputSelect @bind-Value="employee.Graduate">
                <option value="@Graduate.primary">primary</option>
                <option value="@Graduate.middle">middle</option>
                <option value="@Graduate.high">high</option>
                <option value="@Graduate.university">university</option>
            </InputSelect>
        </div>

        <div class="content50">
            是否已婚:
            <InputCheckbox @bind-Value="employee.IsMarriage"></InputCheckbox>
        </div>

        <div class="content50">
            入職日期:
            <InputDate @bind-Value="employee.EntryDate"></InputDate>
        </div>

        <div class="content100">
            愛好:
            <InputSelect @bind-Value="employee.Likes">
                <option value="@Like.football">football</option>
                <option value="@Like.basketball">basketball</option>
                <option value="@Like.swim">swim</option>
                <option value="@Like.code">code</option>
            </InputSelect>
        </div>

        <div class="content100">
            自我介紹:
            <InputTextArea @bind-Value="employee.Introduce"></InputTextArea>
        </div>
        <br />
        <button type="submit">提交</button>

    </EditForm>
</div>

@code {
    private Employee employee = new Employee();
    private EditContext? editContext;
    protected override void OnInitialized()
    {
        editContext = new EditContext(employee);
    }

    private void MyValidSubmit()
    {
        Console.WriteLine("驗證通過,提交表單");
    }

    //表單繫結的物件,直接標註驗證規定和錯誤資訊,非常好用
    //這個類還可以前後端共享
    private class Employee
    {
        [Required(ErrorMessage = "姓名必填")]
        [MaxLength(20, ErrorMessage = "最長20字")]
        public string? Name { get; set; } = null; //姓名

        [Required(ErrorMessage = "性別必填")]
        public Sex? Sex { get; set; } = null; //性別

        [Required(ErrorMessage = "年齡必填")]
        [Range(18, 60, ErrorMessage = "只能填18-60歲")]
        public int Age { get; set; } //年齡

        [Required]
        public Graduate Graduate { get; set; } = Graduate.Null;//學歷

        public bool IsMarriage { get; set; } //是否已婚

        [DataType(DataType.Date)]
        public DateTime EntryDate { get; set; } //入職日期

        [Required, MinLength(1, ErrorMessage = "最少選1個"), MaxLength(3, ErrorMessage = "最多選3個")]
        public Like[] Likes { get; set; } = new[] { Like.basketball }; //愛好

        [MaxLength(300, ErrorMessage = "最長300字")]
        public string? Introduce { get; set; } //自我介紹

    }
    //Employee物件用到的列舉型別
    private enum Sex{ male,female }
    private enum Like{ football,basketball,swim,code }
    private enum Graduate{ Null,primary,middle,high,university }
}