如何实现一个自定义的校验规则,当你需要使用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