UNPKG

shineout

Version:

Shein 前端组件库

394 lines (325 loc) 11.2 kB
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 = {} } }