如何封装一个自定义错误规则校验

如何实现一个自定义的校验规则,当你需要使用form的样式但是不想使用form的校验时,可以自定义一个错误校验

核心代码

Index

import './index.less'
export { default as VForm } from './component/VForm'
export { default as VItem } from './component/VItem'
export { default as VButton } from './component/VButton'

less文件

// 样式根据使用的组件库 自己调整下
@inputErrorBoderColor: #ef5b33, #88333e;
.v-item-wrap {
    position: relative;
    .v-item-message {
        color: @error-color;
        line-height: 20px;
        position: absolute;
        word-break: keep-all;
        white-space: nowrap;
    }
    &.v-error {
        .ant-input,
        .ant-input-number,
        .ant-input-affix-wrapper:hover .ant-input {
            border-color: @error-color !important;
        }
        .ant-select {
            .ant-select-selection {
                border-color: @inputErrorBoderColor;
            }
        }
    }
}

VForm


import PropTypes from 'prop-types'
import React, { Component, Children } from 'react'
import Validator from '../core/validator'
class VForm extends Component {
    static propTypes = {
        conditions: PropTypes.array,
    }
    static childContextTypes = {
        validator: PropTypes.object,
    }
    constructor(props) {
        super(props)
        this.validator = new Validator(props.conditions)
    }
    getChildContext() {
        return { validator: this.validator }
    }
    exec() { // 获取校验结果
        const result = this.validator.execAll()
        const error = result.findIndex(item => item.error) > -1
        return { error, result }
    }
    execSome(ids) { 
        const result = this.validator.execSome(ids)
        const error = result.findIndex(item => item.error) > -1
        return { error, result }
    }
    reset() { // 重置
        this.validator.reset()
    }
    render() {
        let { children, className, tag = 'div' } = this.props
        return React.createElement(tag, { className }, children)
    }
    UNSAFE_componentWillReceiveProps(props) {
        this.validator.setConditions(props.conditions)
    }
}

export default VForm

Validator (核心)

// Validator
import compute from './compute' // 规则实现
import * as Rule from './rule' // 规则定义
class Validator {
    constructor(conditions) {
        // 初始化条件
        this.conditions = this.init(conditions)
        // 监听对象
        this.listeners = Object.create(null)
        // 值对象
        this.values = Object.create(null)
        // 组对象
        this.groups = Object.create(null)
        // 映射验证条件
        this.keys = Object.create(null)
        // 状态对象
        this.states = Object.create(null)
        // 异步验证计数器
        this.counter = 0
    }
    init(conditions) {
        conditions = conditions || []
        conditions.forEach(cond => {
            cond.trigger = cond.trigger || ['change', 'blur'] //触发校验行为
        })
        return conditions
    }
    exec(id, key, value, trigger) {
        // 记录值
        this.values[id] = value
        // 寻找验证条件
        let cond = this.conditions.find(item => item.key == key)
        if (!cond) return
        // 是否被触发
        let isTrigger = cond.trigger.indexOf(trigger) !== -1
        if (!isTrigger) return
        let state = this.states[id]
        let repeatGroup = this.groups[key]
        let repeatRule = cond.rules.find(i => i.test == Rule.REPEAT)
        // 进行重复校验处理
        // 1. 必须有重复验证规则
        // 2. 拥有相同规则的验证项必须有2个及以上
        if (repeatRule && repeatGroup.length > 1) {
            let map = new Map()
            // 根据值计算出相同值的id数据
            repeatGroup.forEach(item => {
                let value = this.values[item]
                let ids = map.get(value) || []
                ids.push(item)
                map.set(value, ids)
            })
            map.forEach(ids => {
                ids.forEach(item => {
                    let itemState = this.states[item]
                    itemState.error = ids.length > 1
                    itemState.message = repeatRule.message
                    itemState.reason = 'repeat'
                    this.listeners[item]()
                })
            })
        }
        if (state.error && state.reason == 'repeat') {
            return
        }
        for (let i = 0; i < cond.rules.length; i++) {
            let result = this.valid(value, cond.rules[i], key, id)
            result.reason = 'valid'
            this.states[id] = result
            this.listeners[id]()
            if (result.error) break
        }
    }
    valid(value, rule, key, id) {
        // 验证结果
        let result = { error: false, message: rule.message, option: null }
        if (rule == Rule.REPEAT) {
            return result
        }
        // 验证方法
        let computeFn = compute[rule.test]
        if (!computeFn) return result
        // 验证参数,目前只有重复和回调验证时需要用到
        let option = null
        // 重复验证时,合成校验参数
        if (rule.test === Rule.REPEAT) {
            let group = this.groups[key]
            option = group.filter(i => i != id).map(i => ({ id: i, value: this.values[i] }))
        }
        result.error = computeFn(value, rule.express, option)
        return result
    }
    get(id) {
        return this.states[id] || { error: false, message: '', option: null }
    }
    add(id, key, value, listener) {
        this.keys[id] = key
        this.values[id] = value
        this.states[id] = { error: false, message: '', option: null }
        this.listeners[id] = listener
        // 重复验证时,将相同验证的条件的项放到一个组里面
        let group = this.groups[key] || []
        group.push(id)
        this.groups[key] = group
    }
    remove(id) {
        delete this.keys[id]
        delete this.values[id]
        delete this.states[id]
        delete this.listeners[id]
    }
    cache(id, value) {
        this.values[id] = value
    }
    reset() {
        for (const id in this.states) {
            let state = this.states[id]
            state.error = false
            this.listeners[id]()
        }
    }
    execAll() {
        const result = []
        for (const id in this.keys) {
            this.exec(id, this.keys[id], this.values[id], 'change')
            result.push(Object.assign({}, this.states[id], { id }))
        }
        return result
    }
    execSome(ids) {
        const result = []
        for (const id of ids) {
            this.exec(id, this.keys[id], this.values[id], 'change')
            result.push(Object.assign({}, this.states[id], { id }))
        }
        return result
    }
    destroy(id, key) {
        delete this.keys[id]
        delete this.values[id]
        delete this.states[id]
        delete this.listeners[id]
        let group = this.groups[key]
        if (group) {
            group = group.filter(item => item != id)
            this.groups[key] = group
        }
    }
    setConditions(conditions) {
        this.conditions = this.init(conditions)
    }
}

export default Validator

rule

// 非空
export const REQUIRED = 'required'
// 数值
export const NUMBER = 'number'
// 正则
export const REG = 'reg'
// 邮箱
export const EMAIL = 'email'
// 网址
export const URL = 'url'
// 手机号
export const PHONE = 'phone'
// 身份证
export const IDCARD = 'idcard'
// ipv4
export const IP = 'ip'
// 长度
export const LENGTH = 'length'
// 范围
export const RANGE = 'range'
// 日期
export const DATE = 'date'
// 重复
export const REPEAT = 'repeat'
// 回调
export const CALLBACK = 'callback'

compute

import * as Rule from './rule'
/*
 * 所有方法均是计算是否合法
 * 如果错误返回true
 */
 
/**
 * 基本校验,校验数据是否为null或者undefined
 * @param value 数据
 */
function baiscValidate(value) {
    if (value === null || typeof value === 'undefined') {
        return true
    }
    return false
}
function baiscStringValidate(value) {
    if (baiscValidate(value)) {
        return true
    }
    if (typeof value !== 'string') {
        return true
    }
    return false
}
function computeRequired(value) {
    if (baiscValidate(value)) {
        return true
    }
    if (typeof value === 'string' && !value) {
        return true
    }
    return false
}
function computeNumber(value) {
    // 如果是number直接通过
    if (typeof value === 'number') {
        return false
    }
    if (baiscStringValidate(value)) {
        return true
    }
    if (!value) {
        return false
    }
    let reg = /^[\+\-]?\d*\.?\d+(?:[Ee][\+\-]?\d+)?$/
    return !reg.test(value)
}
function computeReg(value, express) {
    if (baiscStringValidate(value)) {
        return true
    }
    if (value === '') {
        return false
    }
    return !express.test(value)
}
function computeStringWithReg(value, reg) {
    if (baiscStringValidate(value)) {
        return true
    }
    // 空字符判断通过
    if (!value) {
        return false
    }
    return !reg.test(value)
}
function computeEmail(value) {
    let reg = /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/
    return computeStringWithReg(value, reg)
}
function computeUrl(value) {
    let reg = /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/
    return computeStringWithReg(value, reg)
}
function computePhone(value) {
    let reg = /^1[34578]\d{9}$/
    return computeStringWithReg(value, reg)
}
function computeIDCard(value) {
    let reg = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
    return computeStringWithReg(value, reg)
}
function computeIP(value, express) {
    let reg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
    if (express == 6) {
        return false
    }
    return computeStringWithReg(value, reg)
}
function computeLength(value, express) {
    if (baiscValidate(value)) {
        return true
    }
    if (!value) {
        return false
    }
    if ((typeof value === 'string' || value instanceof Array) && value.length !== express) {
        return true
    }
    return false
}
function computeRange(value, express) {
    if (typeof value === 'string' || value instanceof Array) {
        if (express.min && express.max && (value.length < express.min || value.length > express.max)) {
            return true
        } else if (express.min && value.length < express.min) {
            return true
        } else if (express.max && value.length > express.max) {
            return true
        }
    }
    if (typeof value === 'number') {
        if (express.min && express.max && (value < express.min || value > express.max)) {
            return true
        } else if (express.min && value < express.min) {
            return true
        } else if (express.max && value > express.max) {
            return true
        }
    }
    if (!value) {
        return false
    }
    return false
}
function computeDate(value) {
    if (baiscStringValidate(value)) {
        return true
    }
    if (!value) {
        return false
    }
    let args = value.split(' ')
    let reg = /^(\d{4})[-\\](\d{2})[-\\](\d{2})$/
    if (args.length == 2) {
        reg = /^(\d{4})[-\\](\d{2})[-\\](\d{2}) (\d{2}):(\d{2}):(\d{2})$/
    }
    if (!reg.test(value)) {
        return true
    }
    try {
        let date = new Date(value).toUTCString()
        if (date == 'Invalid Date') {
            return true
        }
    } catch (e) {
        return true
    }
    return false
}
function computeRepeat(value, express, option) {
    if (baiscStringValidate(value)) {
        return true
    }
    return option.findIndex(item => item.value == value) > 0
}
function computeCallback(value, express) {
    if (baiscValidate(value)) {
        return true
    }
    if (!express || typeof express !== 'function') {
        return false
    }
    let result = express(value)
    if (typeof result === 'undefined') {
        return false
    }
    if (typeof result === 'boolean') {
        return !result
    }
    return false
}
const compute = {}

compute[Rule.REQUIRED] = computeRequired
compute[Rule.NUMBER] = computeNumber
compute[Rule.REG] = computeReg
compute[Rule.EMAIL] = computeEmail
compute[Rule.URL] = computeUrl
compute[Rule.PHONE] = computePhone
compute[Rule.IDCARD] = computeIDCard
compute[Rule.IP] = computeIP
compute[Rule.LENGTH] = computeLength
compute[Rule.RANGE] = computeRange
compute[Rule.DATE] = computeDate
compute[Rule.REPEAT] = computeRepeat
compute[Rule.CALLBACK] = computeCallback

export default compute

VItem

import PropTypes from 'prop-types'
import React, { Component } from 'react'
import uuid from 'uuid'
import Validator from '../core/validator'

class VItem extends Component {
    static propTypes = {
        for: PropTypes.string.isRequired,
        state: PropTypes.object,
    }
    static contextTypes = {
        validator: PropTypes.object,
    }
    id = uuid()
    validator = null
    onBlur = e => {
        this.validator.exec(this.id, this.props.for, this.props.children.props.value, 'blur')
    }
    onFocus = e => {
        this.validator.exec(this.id, this.props.for, this.props.children.props.value, 'focus')
    }
    onChange = e => {
        let value = null
        if (e === undefined || e === null) {
            value = undefined
        } else if (e.target && e.target.value !== undefined) {
            value = e.target.value
        } else {
            value = e
        }
        this.validator.exec(this.id, this.props.for, value, 'change')
    }
    handleStateChange = () => { // 改变状态
        let { state } = this.props
        let { vitem, vmessage } = this.refs
        let { error, message, validating } = this.validator.get(this.id)
        if (state) {
            error = state.error
            message = state.message
        }
        let className = this.getClassName(error, validating)
        vitem.className = className
        vmessage.style.display = error ? 'block' : 'none'
        vmessage.innerHTML = error ? message : ''
    }
    getClassName (error, validating) {
        let classnames = ['v-item-wrap']
        if (validating) {
            classnames.push('v-validating')
        } else if (error) {
            classnames.push('v-error')
        }
        return classnames.join(' ')
    }
    createTwoChains (event) {
        const eventFn = this.props.children.props[event]
        return (a, b, c, d, e, f, g) => {
            this[event].call(this, a)
            eventFn && eventFn(a, b, c, d, e, f, g)
        }
    }
    exec () {
        this.validator.exec(this.id, this.props.for, this.props.children.props.value, 'change')
        let { error } = this.validator.get(this.id)
        return !error
    }
    render () {
        let { style } = this.props
        let child = React.Children.only(this.props.children)
        let newChildProps = {
            onBlur: this.createTwoChains('onBlur'),
            onFocus: this.createTwoChains('onFocus'),
            onChange: this.createTwoChains('onChange'),
        }
        return (
            <div style={style} className={this.getClassName(false, false)} ref="vitem">
                {React.cloneElement(child, newChildProps)}
                <p className="v-item-message" style={{ display: 'none' }} ref="vmessage" />
            </div>
        )
    }
    componentWillMount () {
        if (this.props.id) {
            this.id = this.props.id
        }
        this.validator = this.context.validator || new Validator([this.props.condition])
        this.validator.add(this.id, this.props.for, this.props.children.props.value, this.handleStateChange)
    }
    UNSAFE_componentWillReceiveProps (nextProps) {
        let { children } = nextProps
        this.validator.cache(this.id, children.props.value)
        // 如果for为空就移出验证信息
        if (!nextProps.for) {
            this.validator.remove(this.id)
        } else {
            if (!this.props.for) {
                this.validator.add(this.id, nextProps.for, nextProps.children.props.value, this.handleStateChange)
            }
        }
    }
    componentDidUpdate () {
        this.handleStateChange()
    }
    componentWillUnmount () {
        this.validator.destroy(this.id, this.props.for)
    }
}

export default VItem

VButton

import PropTypes from 'prop-types'
import React, { Component } from 'react'

class VButton extends Component {
    static propTypes = {
        onSubmit: PropTypes.func,
        onBeforSubmit: PropTypes.func,
    }
    static contextTypes = {
        validator: PropTypes.object,
    }
    static defaultProps = {
        disabled: false,
        onSubmit: () => {},
        onBeforSubmit: () => true,
    }
    click(event) {
        let { disabled, onBeforSubmit } = this.props
        if (disabled || !onBeforSubmit()) {
            return
        }
        const result = this.context.validator.execAll()
        const error = result.findIndex(item => item.error) > -1
        this.props.onSubmit({ event, error, result })
    }
    render() {
        let child = React.Children.only(this.props.children)
        let newChildProps = {
            onClick: e => this.click(e),
        }
        return React.cloneElement(child, newChildProps)
    }
}

export default VButton

使用方式

createRule(创建规则)

import __ from '@public/i18n'
/**
 * 创建必要规则
 */
export function createRequiredRule(key, title) {
    return { key, rules: [{ test: 'required', message: title }] }
}
/**
 * 创建字符长度规则
 */
export function createRequiredRangeRule(key, title, required, max) {
    let rule = { key, rules: [] }
    if (required) {
        rule.rules.push({ test: 'required', message: title })
    }
    rule.rules.push({ test: 'range', message: __('valid.range', { max }), express: { max } })
    return rule
}
/**
 * 创建对象非空规则
 */
export function createObjectRule(key, title, pk = 'id') {
    return { key, rules: [{ test: 'callback', message: title, express: value => !!value[pk] }] }
}
/**
 * 创建数组非空规则
 */
export function createArrayRule(key, title) {
    return { key, rules: [{ test: 'callback', message: title, express: value => value.length > 0 }] }
}
/**
 * 创建数值规则
 */
export function createNumberRule(key, required, messages = {}) {
    let rule = { key, rules: [] }
    messages = Object.assign({}, { required: __('field'), valided: __('valid.number') }, messages)
    if (required) {
        rule.rules.push({ test: 'required', message: __('messages.required') })
    }
    rule.rules.push({ test: 'number', message: __('messages.valided') })
    return rule
}

/**
 * 创建编码规则
 */
export function createCodeRule(key = 'code', title) {
    return {
        key,
        rules: [
            { test: 'required', message: title },
            { test: 'reg', express: /^[a-zA-Z0-9_]*$/, message: __('placeholder.code') },
        ],
    }
}

import React, { Component } from 'react'
import { Modal, Form, Input } from 'antd'
import { VForm, VItem } from '@components/ui/validator'

const formRef = React.createRef()

class MyForm extends Component {
    render () {
        let { visible, title, onCancel, onSave } = this.props
        return (
            <Modal
                visible={visible}
                title={title}
                onCancel={onCancel}
                onOk={() => {
                    let resulet = formRef.current.exec() // 获取校验信息结果
                    if (resulet.error) return
        			...
                }}>
                <VForm
                    tag={Form}
                    ref={formRef}
                    conditions={[
                        createRequiredRule('name', __('field.name')),
                        createRequiredRule('code', __('field.code')),
                    ]}>
                    <Form.Item {...FORM_LABEL_LAYOUT} label="名称" required>
                        <VItem for="name">
                            <Input
                                value={name}
                                onChange={e => { }}
                            />
                        </VItem>
                    </Form.Item>
                </VForm>
            </Modal>
        )
    }
}

export default MyForm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MaxLoongLvs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值