shineout
Version:
Shein 前端组件库
394 lines (325 loc) • 11.2 kB
text/typescript
import deepEqual from 'deep-eql'
import { unflatten, insertValue, spliceValue, getSthByName } from '../utils/flat'
import { fastClone, deepClone } from '../utils/clone'
import { deepGet, deepSet, deepRemove, objectValues, deepHas } from '../utils/objects'
import { isObject, isArray } from '../utils/is'
import { promiseAll, FormError } from '../utils/errors'
import { FormItemRule } from '../Rule/Props'
import {
updateSubscribe,
errorSubscribe,
changeSubscribe,
VALIDATE_TOPIC,
RESET_TOPIC,
CHANGE_TOPIC,
FORCE_PASS,
ERROR_TYPE,
IGNORE_VALIDATE,
ValidType,
PublishType,
} from './types'
import { FormDatumOptions, FormValid } from './Props'
import { ObjectType } from '../@types/common'
export default class<V extends ObjectType> {
rules: FormDatumOptions<V>['rules']
onChange: FormDatumOptions<V>['onChange']
removeUndefined: FormDatumOptions<V>['removeUndefined']
$defaultValues: V
$inputNames: { [name: string]: boolean }
$values: Partial<V>
$validator: ObjectType<FormValid>
$events: ObjectType<((...args: any) => void)[]>
$errors: ObjectType
updateLock: boolean
deepSetOptions: { removeUndefined: boolean; forceSet: boolean }
formUnmount: boolean
constructor(options: FormDatumOptions<V> = {}) {
const { removeUndefined = true, rules, onChange, value, error, initValidate, defaultValue } = options
this.rules = rules
this.onChange = onChange
this.removeUndefined = removeUndefined
// store names
this.$inputNames = {}
// store values
this.$values = {}
// store default value, for reset
this.$defaultValues = { ...defaultValue } as V
this.$validator = {}
this.$events = {}
// handle global errors
this.$errors = {}
this.updateLock = false
this.deepSetOptions = { removeUndefined, forceSet: true }
const initValue = 'value' in options ? value : defaultValue
if (initValue) this.setValue(initValue, initValidate ? undefined : IGNORE_VALIDATE)
if (error) this.resetFormError(error)
}
handleChange() {
if (this.onChange) this.onChange(this.getValue())
}
reset() {
this.$errors = {}
this.setValue(unflatten(fastClone(this.$defaultValues)), FORCE_PASS, true)
this.handleChange()
this.dispatch(RESET_TOPIC)
}
setLock(lock: boolean) {
this.updateLock = lock
}
get(name: string | (string[])): any {
if (Array.isArray(name)) return name.map(n => this.get(n))
return deepGet(this.$values, name)
}
set(value: ObjectType): void
set(name: string | string[], value: any, pub?: boolean): void
set(name: string | string[] | ObjectType, value?: any, pub?: boolean) {
if (isObject(name)) {
value = objectValues(name as ObjectType)
name = Object.keys(name)
}
if (isArray(name)) {
this.setArrayValue(name, value)
return
}
if (typeof name === 'string') {
if (value === this.get(name)) return
deepSet(this.$values, name, value, this.deepSetOptions)
if (this.$inputNames[name]) {
this.dispatch(updateSubscribe(name), value, name)
this.dispatch(changeSubscribe(name))
}
if ((value !== null && typeof value === 'object') || pub) this.publishValue(name, FORCE_PASS)
this.dispatch(CHANGE_TOPIC)
this.handleChange()
}
}
setArrayValue(names: string[], values: any[]) {
names.forEach((name, index) => {
deepSet(this.$values, name, values[index], this.deepSetOptions)
})
names.forEach((name, index) => {
if (this.$inputNames[name]) {
this.dispatch(updateSubscribe(name), values[index], name)
this.dispatch(changeSubscribe(name))
}
})
this.dispatch(CHANGE_TOPIC)
this.handleChange()
}
insert(name: string, index: number, value: any) {
this.insertError(name, index, undefined)
const val = this.get(name)
if (val) {
val.splice(index, 0, value)
this.publishValue(name, IGNORE_VALIDATE)
this.publishError(name)
// insert value into Form in onAppend will trigger Form onChange
this.handleChange()
} else {
this.set(name, [value])
}
}
splice(name: string, index: number) {
this.spliceError(name, index)
const list = this.get(name)
list.splice(index, 1)
this.publishValue(name, IGNORE_VALIDATE)
this.publishError(name)
// remove value from Form in onRemove will trigger Form onChange
this.handleChange()
}
remove(name: string) {
deepRemove(this.$values, name)
}
publishValue(name: string, type: PublishType) {
const na = `${name}[`
const no = `${name}.`
Object.keys(this.$inputNames)
.filter(n => n.indexOf(na) === 0 || n.indexOf(no) === 0)
.forEach(n => {
this.dispatch(updateSubscribe(n), this.get(n), n, type)
})
}
getError(name: string, firstHand?: boolean) {
if (firstHand) return this.$errors[name]
return getSthByName(name, this.$errors)
}
resetFormError(error: ObjectType<string | Error> = {}) {
if (!this.$errors['']) this.$errors[''] = {}
let items: ObjectType
if (Object.keys(error).length) {
items = Object.keys(error).reduce(
(data, item) => {
data[item] = error[item] instanceof Error ? (error[item] as Error) : new Error(error[item] as string)
return data
},
{} as ObjectType<Error>
)
} else {
items = Object.keys(this.$errors['']).reduce(
(data, name) => {
data[name] = undefined
return data
},
{} as ObjectType
)
}
Object.keys(items).map(n => this.setFormError(n, items[n]))
}
removeFormError(name: string) {
if (!this.$errors[''] || !this.$errors[''][name]) return
this.setFormError(name, undefined)
}
setFormError(name: string, error?: Error) {
if (!this.$errors['']) return
if (error === undefined) delete this.$errors[''][name]
else this.$errors[''][name] = error
this.dispatch(errorSubscribe(name), this.getError(name), name, ERROR_TYPE)
this.dispatch(updateSubscribe(name))
}
setError(name: string, error?: Error, pub?: boolean) {
if (error === undefined) delete this.$errors[name]
else this.$errors[name] = error
this.dispatch(errorSubscribe(name), this.getError(name), name, ERROR_TYPE)
if (pub) this.publishError(name)
}
insertError(name: string, index: number, error?: Error) {
insertValue(this.$errors, name, index, error)
}
spliceError(name: string, index: number) {
spliceValue(this.$errors, name, index)
}
publishError(name: string) {
const na = `${name}[`
const no = `${name}.`
Object.keys(this.$inputNames)
.filter(n => n.indexOf(na) === 0 || n.indexOf(no) === 0)
.forEach(n => {
this.dispatch(errorSubscribe(n), this.getError(n), n, ERROR_TYPE)
})
}
getRule(name: string): FormItemRule<any> {
if (!this.rules) return []
const a = deepGet(this.rules, name) as FormItemRule<any>
return a || []
}
getValue() {
return deepClone(this.$values)
}
setValue(v: any = {}, type?: typeof IGNORE_VALIDATE | typeof FORCE_PASS, forceSet?: boolean) {
const values = isObject(v) ? v : {}
if (values !== v) {
console.warn('Form value must be an Object')
}
// 兼容 value 传入 null 等错误等值
if (!forceSet && deepEqual(values, this.$values)) return
this.$values = deepClone(values)
// wait render end.
setTimeout(() => {
Object.keys(this.$inputNames)
.sort((a, b) => a.length - b.length)
.forEach(name => {
this.dispatch(updateSubscribe(name), this.get(name), name, type)
this.dispatch(changeSubscribe(name))
})
// for flow
this.dispatch(CHANGE_TOPIC)
})
}
bind(name: string, fn: (...args: any) => void, value: any, validate: FormValid) {
if (this.$inputNames[name]) {
console.warn(`There is already an item with name "${name}" exists. The name props must be unique.`)
}
if (value !== undefined && this.get(name) == null) {
this.set(name, value, true)
this.dispatch(changeSubscribe(name))
this.dispatch(CHANGE_TOPIC)
}
if (!(name in this.$defaultValues) && value) this.$defaultValues[name as keyof V] = fastClone(value)
this.$validator[name] = validate
this.$inputNames[name] = true
this.subscribe(updateSubscribe(name), fn)
this.subscribe(errorSubscribe(name), fn)
}
unbind(name: string | string[], _cb?: any, reserveAble?: boolean) {
if (Array.isArray(name)) {
name.forEach(n => this.unbind(n))
return
}
this.unsubscribe(updateSubscribe(name))
this.unsubscribe(errorSubscribe(name))
delete this.$inputNames[name]
delete this.$validator[name]
delete this.$errors[name]
delete this.$defaultValues[name]
// when setData due to unmount not delete value
if (this.updateLock) return
if (!deepHas(this.$values, name)) return
if (reserveAble) return
deepRemove(this.$values, name)
if (!this.formUnmount) {
setTimeout(() => {
this.handleChange()
})
}
}
dispatch(name: string, ...args: any) {
const event = this.$events[name]
if (!event) return
event.forEach(fn => fn(...args))
}
subscribe(name: string, fn: (...args: any) => void) {
if (!this.$events[name]) this.$events[name] = []
const events = this.$events[name]
if (events.includes(fn)) return
events.push(fn)
}
unsubscribe(name: string, fn?: (...args: any) => void) {
if (!this.$events[name]) return
if (fn) this.$events[name] = this.$events[name].filter(e => e !== fn)
else delete this.$events[name]
}
validate(type?: ValidType) {
return new Promise((resolve, reject) => {
const keys = Object.keys(this.$validator)
const values = this.getValue()
const validates = [
...keys.map(k => this.$validator[k](this.get(k), values, type)),
...(this.$events[VALIDATE_TOPIC] || []).map(fn => fn()),
]
Promise.all(validates)
.then(res => {
const error = res.find(r => r !== true)
if (error === undefined) resolve(true)
else reject(error)
})
.catch(e => {
reject(new FormError(e))
})
})
}
validateFieldsByName(name: string, type?: ValidType) {
if (!name || typeof name !== 'string') {
return Promise.reject(new Error(`Name expect a string, get "${name}"`))
}
const validations: Promise<any>[] = []
const values = this.getValue()
Object.keys(this.$validator).forEach(n => {
if (n === name || n.indexOf(name) === 0) {
validations.push(this.$validator[n](this.get(n), values, type))
}
})
return promiseAll(validations)
}
validateFields(names: string[], type?: ValidType) {
if (!Array.isArray(names)) names = [names]
const validates = names.map(n => this.validateFieldsByName(n, type))
return promiseAll(validates)
}
validateClear() {
const keys = Object.keys(this.$validator)
const validates = keys.map(k => this.$validator[k](FORCE_PASS))
Promise.all(validates)
this.$errors = {}
}
}