Re从零开始的UI库编写生活之表单

1,289 阅读8分钟

构想

表单是一个组件库中必不可少的一部分,但无论是表单的输入输出还是校验,都非常容易与业务代码有相当紧密的耦合。这就会导致很多的问题,比如组件经常不能很好地适应需求,需要经历复杂的样式功能调整,调试起来很麻烦等等。

这个时候就会心想如果有一套能很好地与业务逻辑划清界限,与校验逻辑解耦,简洁易用的表单组件就好了。

我们知道表单是一种与业务关联程度很高的组件,所以我们理应避重就轻,仅仅去关心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

从零开始系列传送门