import RawAsyncValidator from'async-validator';import*as React from'react';import{
InternalNamePath,
ValidateOptions,
ValidateMessages,
RuleObject,
StoreValue,}from'./interface';import{ defaultValidateMessages }from'./messages';import _ from'lodash';const isObject = _.isObject;const warning = console.warn;/**
* Copy values into store and return a new values object
* ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
*/function internalSetValues<T>(store:T,values: any):T{constnewStore: any =(Array.isArray(store)?[...store]:{...store })asT;if(!values){return newStore;}
Object.keys(values).forEach(key=>{const prevValue = newStore[key];const value = values[key];// If both are object (but target is not array), we use recursion to set deep valueconst recursive =isObject(prevValue)&&isObject(value);
newStore[key]= recursive ?internalSetValues(prevValue, value ||{}): value;});return newStore;}function setValues<T>(store:T,...restValues:T[]):T{return restValues.reduce((current:T,newStore:T):T=> internalSetValues<T>(current, newStore),
store,);}// Remove incorrect original ts defineconstAsyncValidator: any = RawAsyncValidator;/**
* Replace with template.
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
*/functionreplaceMessage(template: string,kv: Record<string, string>): string {return template.replace(/\$\{\w+\}/g,(str: string)=>{const key = str.slice(2,-1);return kv[key];});}/**
* We use `async-validator` to validate rules. So have to hot replace the message with validator.
* { required: '${name} is required' } => { required: () => 'field is required' }
*/functionconvertMessages(messages: ValidateMessages,name: string,rule: RuleObject,
messageVariables?: Record<string, string>,): ValidateMessages {const kv ={...(rule as Record<string, string | number>),
name,enum:(rule.enum ||[]).join(', '),};constreplaceFunc=(template: string, additionalKV?: Record<string, string>)=>()=>replaceMessage(template,{...kv,...additionalKV });/* eslint-disable no-param-reassign */functionfillTemplate(source: any,target: any ={}){
Object.keys(source).forEach(ruleName=>{const value = source[ruleName];if(typeof value ==='string'){
target[ruleName]=replaceFunc(value, messageVariables);}elseif(value &&typeof value ==='object'){
target[ruleName]={};fillTemplate(value, target[ruleName]);}else{
target[ruleName]= value;}});return target;}/* eslint-enable */returnfillTemplate(setValues({}, defaultValidateMessages, messages))as ValidateMessages;}asyncfunctionvalidateRule(name: string,value: StoreValue,rule: RuleObject,options: any,
messageVariables?: Record<string, string>,): Promise<string[]>{const cloneRule ={...rule };// We should special handle array validateletsubRuleField: any =null;if(cloneRule && cloneRule.type ==='array'&& cloneRule.defaultField){
subRuleField = cloneRule.defaultField;delete cloneRule.defaultField;}const validator =newAsyncValidator({[name]:[cloneRule],});constmessages: ValidateMessages =convertMessages(
options.validateMessages,
name,
cloneRule,
messageVariables,);
validator.messages(messages);let result =[];try{await Promise.resolve(validator.validate({[name]: value },{...options }));}catch(errObj: any){if(errObj.errors){
result = errObj.errors.map(({ message }: any,index: any)=>// Wrap ReactNode with `key`
React.isValidElement(message)? React.cloneElement(message,{key:`error_${index}`}): message,);}else{
console.error(errObj);
result =[(messages.default as()=> string)()];}}if(!result.length && subRuleField){constsubResults: string[][]=await Promise.all((value as StoreValue[]).map((subValue: StoreValue,i: number)=>validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables),),);return subResults.reduce((prev, errors)=>[...prev,...errors],[]);}return result;}/**
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/exportfunctionvalidateRules(namePath: InternalNamePath,value: StoreValue,rules: RuleObject[],options: ValidateOptions,validateFirst: boolean,
messageVariables?: Record<string, string>,){const name = namePath.join('.');// Fill rule with contextconstfilledRules: RuleObject[]= rules.map(currentRule=>{const originValidatorFunc = currentRule.validator;if(!originValidatorFunc){return currentRule;}return{...currentRule,validator(rule: RuleObject,val: StoreValue,callback:(error?: string)=>void){lethasPromise: any =false;// Wrap callback only accept when promise not providedconstwrappedCallback:any=(...args: string[])=>{// Wait a tick to make sure return type is a promise
Promise.resolve().then(()=>{warning(!hasPromise,'Your validator function has already return a promise. `callback` will be ignored.',);if(!hasPromise){callback(...args);}});};// Get promiseconst promise =originValidatorFunc(rule, val, wrappedCallback);
hasPromise =
promise &&typeof promise.then ==='function'&&typeof promise.catch ==='function';/**
* 1. Use promise as the first priority.
* 2. If promise not exist, use callback with warning instead
*/warning(hasPromise,'`callback` is deprecated. Please return a promise instead.');if(hasPromise){(promise as Promise<void>).then(()=>{callback();}).catch(err=>{callback(err);});}},};});const rulePromises = filledRules.map(rule=>validateRule(name, value, rule, options, messageVariables),);constsummaryPromise: Promise<string[]>=(validateFirst
?finishOnFirstFailed(rulePromises):finishOnAllFailed(rulePromises)).then((errors: string[]): string[]| Promise<string[]>=>{if(!errors.length){return[];}return Promise.reject<string[]>(errors);});// Internal catch error to avoid console error log.
summaryPromise.catch(e=> e);return summaryPromise;}asyncfunctionfinishOnAllFailed(rulePromises: Promise<string[]>[]): Promise<string[]>{return Promise.all(rulePromises).then((errorsList: any): string[]| Promise<string[]>=>{consterrors: string[]=[].concat(...errorsList);return errors;});}asyncfunctionfinishOnFirstFailed(rulePromises: Promise<string[]>[]): Promise<string[]>{let count =0;returnnewPromise(resolve=>{
rulePromises.forEach(promise=>{
promise.then(errors=>{if(errors.length){resolve(errors);}
count +=1;if(count === rulePromises.length){resolve([]);}});});});}
./utils/interface.ts
import{ ReactElement }from'react';interfaceUpdateAction{type:'updateValue';namePath: InternalNamePath;value: StoreValue;}interfaceValidateAction{type:'validateField';namePath: InternalNamePath;triggerName: string;}export type ReducerAction = UpdateAction | ValidateAction;export type InternalNamePath =(string | number)[];export type NamePath = string | number | InternalNamePath;export type StoreValue = any;exportinterfaceStore{[name: string]: StoreValue;}exportinterfaceMeta{touched: boolean;validating: boolean;errors: string[];name: InternalNamePath;}/**
* Used by `setFields` config
*/exportinterfaceFieldDataextendsPartial<Omit<Meta,'name'>>{name: NamePath;
value?: StoreValue;}export type RuleType =|'string'|'number'|'boolean'|'method'|'regexp'|'integer'|'float'|'object'|'enum'|'date'|'url'|'hex'|'email';
type Validator=(rule: RuleObject,value: StoreValue,callback:(error?: string)=>void,)=> Promise<void>|void;export type RuleRender=(form: FormInstance)=> RuleObject;interfaceBaseRule{enum?: StoreValue[];
len?: number;
max?: number;
message?: string | ReactElement;
min?: number;
pattern?: RegExp;
required?: boolean;
transform?:(value: StoreValue)=> StoreValue;
type?: RuleType;
validator?: Validator;
whitespace?: boolean;/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
validateTrigger?: string | string[];}interfaceArrayRuleextendsOmit<BaseRule,'type'>{type:'array';
defaultField?: RuleObject;}export type RuleObject = BaseRule | ArrayRule;export type Rule = RuleObject | RuleRender;exportinterfaceValidateErrorEntity{values: Store;errorFields:{name: InternalNamePath; errors: string[]}[];outOfDate: boolean;}exportinterfaceFieldEntity{onStoreChange:(store: Store,namePathList: InternalNamePath[]|null,info: NotifyInfo)=>void;isFieldTouched:()=> boolean;isFieldValidating:()=> boolean;validateRules:(options?: ValidateOptions)=> Promise<string[]>;getMeta:()=> Meta;getNamePath:()=> InternalNamePath;getErrors:()=> string[];props:{
name?: NamePath;
rules?: Rule[];
dependencies?: NamePath[];};}exportinterfaceFieldError{name: InternalNamePath;errors: string[];}exportinterfaceValidateOptions{
triggerName?: string;
validateMessages?: ValidateMessages;}export type InternalValidateFields=(nameList?: NamePath[],
options?: ValidateOptions,)=> Promise<Store>;export type ValidateFields=(nameList?: NamePath[])=> Promise<Store>;interfaceValueUpdateInfo{type:'valueUpdate';source:'internal'|'external';}export type NotifyInfo =| ValueUpdateInfo
|{type:'validateFinish'|'reset';}|{type:'setField';data: FieldData;}|{type:'dependenciesUpdate';/**
* Contains all the related `InternalNamePath[]`.
* a <- b <- c : change `a`
* relatedFields=[a, b, c]
*/relatedFields: InternalNamePath[];};exportinterfaceCallbacks{
onValuesChange?:(changedValues: Store,values: Store)=>void;
onFieldsChange?:(changedFields: FieldData[],allFields: FieldData[])=>void;
onFinish?:(values: Store)=>void;
onFinishFailed?:(errorInfo: ValidateErrorEntity)=>void;}exportinterfaceInternalHooks{dispatch:(action: ReducerAction)=>void;registerField:(entity: FieldEntity)=>()=>void;useSubscribe:(subscribable: boolean)=>void;setInitialValues:(values: Store,init: boolean)=>void;setCallbacks:(callbacks: Callbacks)=>void;getFields:(namePathList?: InternalNamePath[])=> FieldData[];setValidateMessages:(validateMessages: ValidateMessages)=>void;}exportinterfaceFormInstance{// Origin Form APIgetFieldValue:(name: NamePath)=> StoreValue;getFieldsValue:(nameList?: NamePath[]|true, filterFunc?:(meta: Meta)=> boolean)=> Store;getFieldError:(name: NamePath)=> string[];getFieldsError:(nameList?: NamePath[])=> FieldError[];isFieldsTouched(nameList?: NamePath[], allFieldsTouched?: boolean): boolean;isFieldsTouched(allFieldsTouched?: boolean): boolean;isFieldTouched:(name: NamePath)=> boolean;isFieldValidating:(name: NamePath)=> boolean;isFieldsValidating:(nameList: NamePath[])=> boolean;resetFields:(fields?: NamePath[])=>void;setFields:(fields: FieldData[])=>void;setFieldsValue:(value: Store)=>void;validateFields: ValidateFields;// New APIsubmit:()=>void;}export type InternalFormInstance = Omit<FormInstance,'validateFields'>&{validateFields: InternalValidateFields;/**
* Passed by field context props
*/
prefixName?: InternalNamePath;/**
* Form component should register some content into store.
* We pass the `HOOK_MARK` as key to avoid user call the function.
*/getInternalHooks:(secret: string)=> InternalHooks |null;};// eslint-disable-next-line @typescript-eslint/no-explicit-anyexport type EventArgs = any[];
type ValidateMessage = string |(()=> string);exportinterfaceValidateMessages{default?: ValidateMessage;
required?: ValidateMessage;enum?: ValidateMessage;
whitespace?: ValidateMessage;
date?:{
format?: ValidateMessage;
parse?: ValidateMessage;
invalid?: ValidateMessage;};
types?:{
string?: ValidateMessage;
method?: ValidateMessage;
array?: ValidateMessage;
object?: ValidateMessage;
number?: ValidateMessage;
date?: ValidateMessage;
boolean?: ValidateMessage;
integer?: ValidateMessage;
float?: ValidateMessage;
regexp?: ValidateMessage;
email?: ValidateMessage;
url?: ValidateMessage;
hex?: ValidateMessage;};
string?:{
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;};
number?:{
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;};
array?:{
len?: ValidateMessage;
min?: ValidateMessage;
max?: ValidateMessage;
range?: ValidateMessage;};
pattern?:{
mismatch?: ValidateMessage;};}
./utils/messages.ts
const typeTemplate ="'${name}' is not a valid ${type}";exportconst defaultValidateMessages ={default:"Validation error on field '${name}'",required:"'${name}'是必填项",enum:"'${name}' must be one of [${enum}]",whitespace:"'${name}' cannot be empty",date:{format:"'${name}' is invalid for format date",parse:"'${name}' could not be parsed as date",invalid:"'${name}' is invalid date",},types:{string: typeTemplate,method: typeTemplate,array: typeTemplate,object: typeTemplate,number: typeTemplate,date: typeTemplate,boolean: typeTemplate,integer: typeTemplate,float: typeTemplate,regexp: typeTemplate,email: typeTemplate,url: typeTemplate,hex: typeTemplate,},string:{len:"'${name}' must be exactly ${len} characters",min:"'${name}' must be at least ${min} characters",max:"'${name}' cannot be longer than ${max} characters",range:"'${name}' must be between ${min} and ${max} characters",},number:{len:"'${name}' must equal ${len}",min:"'${name}' cannot be less than ${min}",max:"'${name}' cannot be greater than ${max}",range:"'${name}' must be between ${min} and ${max}",},array:{len:"'${name}' must be exactly ${len} in length",min:"'${name}' cannot be less than ${min} in length",max:"'${name}' cannot be greater than ${max} in length",range:"'${name}' must be between ${min} and ${max} in length",},pattern:{mismatch:"'${name}' does not match pattern ${pattern}",},};