Re從零開始的UI庫編寫生活之表單

FlyTeng_1874發表於2019-06-25

構想

表單是一個元件庫中必不可少的一部分,但無論是表單的輸入輸出還是校驗,都非常容易與業務程式碼有相當緊密的耦合。這就會導致很多的問題,比如元件經常不能很好地適應需求,需要經歷複雜的樣式功能調整,除錯起來很麻煩等等。

這個時候就會心想如果有一套能很好地與業務邏輯劃清界限,與校驗邏輯解耦,簡潔易用的表單元件就好了。

我們知道表單是一種與業務關聯程度很高的元件,所以我們理應避重就輕,僅僅去關心view層,將校驗邏輯單獨抽離出來封裝成一個工具類,供開發者更加有條理地整理表單邏輯,留給開發者更多靈活處理的空間,將業務部分完全交由開發者去處理。

總的來說就是要力求將表單元件從業務中解耦出來,同時又要方便易用,使業務邏輯清晰,權衡好這一點非常關鍵。

想要的效果

表單的輸入輸出和校驗完全由開發者去自由控制,表單作為view層的元件只需要對輸入輸出的資料作出正確響應,並且這些互動效果完全使用css去編寫。

開始設計

首要問題

全由css去寫互動效果會遇到一個不可避免的問題,那就是我們應該怎樣去儲存元件的狀態呢? 如果使用 css 的hover偽類效果可以將所儲存下來的狀態瞬時響應出來。但如果想將狀態持久地展示出來呢?

js觸發器

從js的角度去看其實是非常簡單的一件事,無非就是將一個元件的一個狀態儲存到一個變數裡,然後再加一個判斷就可以將狀態正確地響應出來。

//虛擬碼
const clicked = false
someComponent.onclick=function(){
    if(clicked){
        someComponent.className.add('fadeOut')
        clicked = false
    }else{
        someComponent.className.add('fadeIn')
        clicked = true
    }
}
複製程式碼

css觸發器

我們要想方設法在css中構造一種結構去儲存狀態,這個觸發器的核心就是瀏覽器的checkbox元件,不知細心的你有沒有曾經想過,其實瀏覽器原生的checkbox元件是能夠對使用者是否checked作出不同響應的,這就意味著這個瀏覽器原生元件中自帶的checked屬性就像變數一樣標記著這個元件的狀態,恰恰好我們能用css中的:checked偽類去‘監聽’這個‘變數’,在監聽的同時用相鄰兄弟選擇器+去處理後續響應,這樣我們的css觸發器就呼之欲出了。

<!--trigger.html 一個簡單的觸發器-->
...
<input type="checkbox" id="trigger"/>
<div class="content">響應內容</div>
複製程式碼
//trigger.css
.content{
    color:red;
}
#trigger:checked + .content{
    color:blue;
}

複製程式碼
  • 配合使用:checked偽類以及+相鄰選擇器可以儲存兩種狀態,使用這種方案的相容性最好,適用所有類似情況。
  • 配合使用:checked偽類,~+兄弟選擇器可以最多儲存3種狀態,但這種方法只能適用於一部分情況(這裡如何儲存3種狀態的魔法會在後面的系列會講到)。

前端的特效都是障眼法

是不是覺得就這樣觸發器的應用範圍有些侷限,因為我們無法保證它的樣式。放心,還有一個很神奇的標籤可以幫觸發器脫胎換骨,label標籤的for屬效能將對應id的input元件關聯起來,這樣我們就可以利用這個特性巧妙地將觸發器的控制部分與響應部分割槽分開。

<!--trigger.html 一個完整的觸發器-->
<!-- 表面的觸發部分 -->
<label class="AnyStyleWhatYouLike" for="trigger">控制器</label>
...
<!-- 實際的響應部分 -->
<div class="trigger-container">
    <input type="checkbox" id="trigger" style="display:none"/>
    <div class="content">響應內容</div>
</div>
複製程式碼
//trigger.css
.content{
    color:red;
}
#trigger:checked + .content{
    color:blue;
}

複製程式碼

使用這種觸發器結構就能設計出很多以往不敢想象的元件了,這些表單元件現已整合在SluckUI中,到SluckUI的表單標籤中就能看到完整的Demo。

純css開發的表單元件

完整版 _input.scss

image
image

表單工具-將常用的邏輯封裝起來

還記得在構想中定下的目標嗎?我們雖然已經將表單元件抽離在view層中,讓開發者使用表單元件的靈活性大大提高了,但付出的代價卻是開發者需要額外編寫的邏輯變多了,這意味著開發效率的降低。所以我們要找到一個新的平衡點,對錶單常用的操作進行適當的封裝,幫助開發者整理表單的業務邏輯,其中最重要的就是表單的校驗邏輯。

表單校驗類(Validator)

這個校驗類的設計參考自《JavaScript設計模式》一書,使用配置模式。目標是通過簡單的配置即可達到表單校驗的目的。

首先我們先想象一下在實際使用中怎樣才能使表單程式碼簡潔明瞭,從使用方式入手能幫助我們構建出這個類的藍圖。

第一步

在進行校驗之前應該先配置好相應的資訊,通常這一步會在建構函式中完成。

this.Validator = new Validator() //初始化校驗類
//配置需要校驗的欄位
this.Validator.config = {
    list: ['isEmpty','isArrayEmpty'], //檢測空值和空陣列
    id: ['isEmpty','isInt'], //檢測空值和整數
    name: ['isEmpty'] //檢測空值
};
複製程式碼

第二步

在提交資料時,應該要對想要校驗的資料進行判斷,isSubmit方法會返回一個boolean值來判斷結果是否符合預期

...
if(this.Validator.isSubmit({
    list:[1,2,3],
    id:456,
    name:'asdf'
})){
    //do some...
}
...
複製程式碼

第三步

在呼叫完isSubmit方法之後,可以使用formatRes('youKey')得到校驗的結果,在需要的地方給出相應的提示。

this.Validator.formatRes('list') //return string
複製程式碼

以React為例的使用方式

import React, { Component } from 'react'
importal { Validator ,http } form 'slucky';

export default class Register extends Component {
    constructor(){
        this.state={
            name:'',
            email:'',
            password:''
        }
        this.Validator = new Validator() //初始化校驗類
        Validator.types.isEmptyTest = {
            validate(value) {
                return value !== '';
            },
            instruction: '不為空自定義校驗'
        };
        //配置需要校驗的欄位
        this.Validator.config = {
            name: ['isEmpty','isEmptyTest'],
            email: ['isEmpty'],
            password: ['isEmpty']
        };
    }
    handelClickSubmit=()=>{
        const {name, email, password} = this.state
        //isSubmit只檢測
        if(this.Validator.isSubmit(this.state)){
            //傳送表單
            http.post({
                name,
                email,
                password
            })
        }
        //更新校驗資訊
        this.forceUpdate();
    }
    
    render() {
        const { res } = this.state
        return (
            <div>
                name:
                <input type="text" onChange={(e)=>{this.setState({name:e.target.value})}}/>
                {this.Validator.formatRes('name')}
                
                email:
                <input type="text" onChange={(e)=>{this.setState({email:e.target.value})}}/>
                {this.Validator.formatRes('email')}
                
                password:
                <input type="text" onChange={(e)=>{this.setState({password:e.target.value})}}/>
                {this.Validator.formatRes('password')}
                
                <button onClick={this.handelClickSubmit}></button>
            </div>
        )
    }
}
複製程式碼

到目前為止,我們已經確定好應該怎樣去使用這個校驗類了。

image

那如何實現的呢?

核心變數

data //使用者傳入的需要校驗的資料
config //使用者傳入的配置
result //校驗輸出的結果
types //用於儲存不同的校驗邏輯
複製程式碼

核心方法

思路很簡單,只要將使用者輸入的資料與使用者配置的校驗邏輯進行一個判斷就ok了,一下子就能概括出來。

具體需要做的是

  1. 遍歷使用者輸入的資料
  2. 找到資料所對應的校驗器名稱
  3. 呼叫校驗器去校驗資料
  4. 輸出結果
// Validator.jsx

class Validator{
    constructor() {
        this.config = {}
        this.result = {}
        this.data = {}
    }
    
    ...
    // 校驗使用者傳入的資料
    validate(data) {
        this.data = data;
        this.result = {};
        //遍歷使用者傳入的資料
        for (const item in data) {
          if (data.hasOwnProperty(item)) {
            const val = data[item];
            //給出判斷結果
            const res = this.validateItem(item, val);
            if (res) {
              this.result[item] = res;
            }
          }
        }
        return this.result;
    }
    //判斷使用者資料是否符合校驗器的預期
    validateItem(item, val) {
        const checkerList = this.config[item];
        if (!checkerList) {
          return false;
        }
        const result = {
          key: item,
          isValid: true,
          message: []
        };
    
        // 一個欄位的校驗器可以有多個,遍歷為某個欄位配置的校驗器
        for (let index = 0; index < checkerList.length; index++) {
          const checkerName = checkerList[index]
          const isValid = Validator.types[checkerName].validate;
          // 在欄位校驗非法時給出錯誤資訊
          if (!isValid.call(this, val)) {
            const instruction = Validator.types[checkerName].instruction;
            result.isValid = false;
            result
              .message
              .push(instruction);
          }
        }
        return result;
    }
    
    // 判斷表單是否符合提交條件
    isSubmit(data = undefined) {
        data && this.validate(data);
        for (const item in this.data) {
          if (this.result[item] !== undefined && !this.result[item].isValid) {
            return false;
          }
        }
        return true;
    }
    ...
}

// 校驗器,用於儲存不同的校驗邏輯
Validator.types = {}

// 自定義校驗器可以在類初始化完成後新增
Validator.types.isEmpty = {
  validate(value) {
    return value !== '';
  },
  instruction: '不能為空'
};
複製程式碼

完整版 Validator.jsx

結束

觸發器的結構可以放心使用,理論上能處理任何類似的地方。篇幅有限,很多地方只寫了原理,更多有趣的實踐盡在SluckyUI中。哈哈,如果你有更多的奇思妙想歡迎多多交流,目前SluckyUI還有很多需要完善的地方,正在持續更新中。

image

image

從零開始系列傳送門

相關文章