diff --git a/src/interface.ts b/src/interface.ts index 95426748..0a04c605 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -3,8 +3,10 @@ import { ReducerAction } from './useForm'; export type InternalNamePath = (string | number)[]; export type NamePath = string | number | InternalNamePath; +type StoreBaseValue = string | number | boolean; +export type StoreValue = StoreBaseValue | Store | StoreBaseValue[]; export interface Store { - [name: string]: any; + [name: string]: StoreValue; } export interface Meta { @@ -70,6 +72,12 @@ export type RuleObject = BaseRule | ArrayRule; export type Rule = RuleObject | RuleRender; +export interface ValidateErrorEntity { + values: Store; + errorFields: { name: InternalNamePath; errors: string[] }; + outOfDate: boolean; +} + export interface FieldEntity { onStoreChange: (store: any, namePathList: InternalNamePath[] | null, info: NotifyInfo) => void; isFieldTouched: () => boolean; @@ -165,50 +173,51 @@ export type InternalFormInstance = Omit & { getInternalHooks: (secret: string) => InternalHooks | null; }; +type ValidateMessage = string | (() => string); export interface ValidateMessages { - default?: string; - required?: string; - enum?: string; - whitespace?: string; + default?: ValidateMessage; + required?: ValidateMessage; + enum?: ValidateMessage; + whitespace?: ValidateMessage; date?: { - format?: string; - parse?: string; - invalid?: string; + format?: ValidateMessage; + parse?: ValidateMessage; + invalid?: ValidateMessage; }; types?: { - string?: string; - method?: string; - array?: string; - object?: string; - number?: string; - date?: string; - boolean?: string; - integer?: string; - float?: string; - regexp?: string; - email?: string; - url?: string; - hex?: string; + 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?: string; - min?: string; - max?: string; - range?: string; + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; }; number?: { - len?: string; - min?: string; - max?: string; - range?: string; + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; }; array?: { - len?: string; - min?: string; - max?: string; - range?: string; + len?: ValidateMessage; + min?: ValidateMessage; + max?: ValidateMessage; + range?: ValidateMessage; }; pattern?: { - mismatch?: string; + mismatch?: ValidateMessage; }; } diff --git a/src/useForm.ts b/src/useForm.ts index d31bd28f..f1af42bd 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -15,6 +15,7 @@ import { ValidateMessages, InternalValidateFields, InternalFormInstance, + ValidateErrorEntity, } from './interface'; import { HOOK_MARK } from './FieldContext'; import { allPromiseFinish } from './utils/asyncUtil'; @@ -470,13 +471,15 @@ export class FormStore { this.triggerOnFieldsChange(resultNamePathList); }); - const returnPromise = summaryPromise - .then(() => { - if (this.lastValidatePromise === summaryPromise) { - return this.store; - } - return Promise.reject([]); - }) + const returnPromise: Promise = summaryPromise + .then( + (): Promise => { + if (this.lastValidatePromise === summaryPromise) { + return Promise.resolve(this.store); + } + return Promise.reject([]); + }, + ) .catch((results: { name: InternalNamePath; errors: string[] }[]) => { const errorList = results.filter(result => result && result.errors.length); return Promise.reject({ @@ -487,9 +490,9 @@ export class FormStore { }); // Do not throw in console - returnPromise.catch(e => e); + returnPromise.catch(e => e); - return returnPromise; + return returnPromise as Promise; }; } diff --git a/src/utils/NameMap.ts b/src/utils/NameMap.ts index dc2e7484..eebc0346 100644 --- a/src/utils/NameMap.ts +++ b/src/utils/NameMap.ts @@ -9,11 +9,11 @@ interface KV { /** * NameMap like a `Map` but accepts `string[]` as key. */ -class NameMap { +class NameMap { private list: KV[] = []; public clone(): NameMap { - const clone = new NameMap(); + const clone: NameMap = new NameMap(); clone.list = this.list.concat(); return clone; } @@ -50,12 +50,12 @@ class NameMap { this.list = this.list.filter(item => !matchNamePath(item.key, key)); } - public map(callback: (kv: KV) => any) { + public map(callback: (kv: KV) => U) { return this.list.map(callback); } public toJSON(): { [name: string]: T } { - const json: any = {}; + const json: { [name: string]: T } = {}; this.map(({ key, value }) => { json[key.join('.')] = value; return null; diff --git a/src/utils/asyncUtil.ts b/src/utils/asyncUtil.ts index e12ba294..6c837c56 100644 --- a/src/utils/asyncUtil.ts +++ b/src/utils/asyncUtil.ts @@ -3,7 +3,7 @@ import { FieldError } from '../interface'; export function allPromiseFinish(promiseList: Promise[]): Promise { let hasError = false; let count = promiseList.length; - const results: any[] = []; + const results: FieldError[] = []; if (!promiseList.length) { return Promise.resolve([]); diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 483484ae..eaa9f6f3 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -7,6 +7,8 @@ import { ValidateOptions, ValidateMessages, RuleObject, + Rule, + StoreValue, } from '../interface'; import NameMap from './NameMap'; import { containsNamePath, getNamePath, setValues } from './valueUtil'; @@ -16,7 +18,7 @@ import { defaultValidateMessages } from './messages'; * Replace with template. * `I'm ${name}` + { name: 'bamboo' } = I'm bamboo */ -function replaceMessage(template: string, kv: { [name: string]: any }): string { +function replaceMessage(template: string, kv: Record): string { return template.replace(/\$\{\w+\}/g, (str: string) => { const key = str.slice(2, -1); return kv[key]; @@ -27,20 +29,30 @@ function replaceMessage(template: string, kv: { [name: string]: any }): string { * We use `async-validator` to validate rules. So have to hot replace the message with validator. * { required: '${name} is required' } => { required: () => 'field is required' } */ -function convertMessages(messages: ValidateMessages, name: string, rule: RuleObject) { - const kv: { [name: string]: any } = { - ...rule, +function convertMessages( + messages: ValidateMessages, + name: string, + rule: RuleObject, +): ValidateMessages { + const kv = { + ...(rule as Record), name, enum: (rule.enum || []).join(', '), }; - const replaceFunc = (template: string, additionalKV?: Record) => { + const replaceFunc = (template: string, additionalKV?: Record) => { if (!template) return null; return () => replaceMessage(template, { ...kv, ...additionalKV }); }; /* eslint-disable no-param-reassign */ - function fillTemplate(source: { [name: string]: any }, target: { [name: string]: any } = {}) { + type Template = + | { + [name: string]: string | (() => string) | { [name: string]: Template }; + } + | string; + + function fillTemplate(source: Template, target: Template = {}) { Object.keys(source).forEach(ruleName => { const value = source[ruleName]; if (typeof value === 'string') { @@ -57,12 +69,12 @@ function convertMessages(messages: ValidateMessages, name: string, rule: RuleObj } /* eslint-enable */ - return fillTemplate(setValues({}, defaultValidateMessages, messages)); + return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages; } async function validateRule( name: string, - value: any, + value: StoreValue, rule: RuleObject, options: ValidateOptions, ): Promise { @@ -78,7 +90,7 @@ async function validateRule( [name]: [cloneRule], }); - const messages = convertMessages(options.validateMessages, name, cloneRule); + const messages: ValidateMessages = convertMessages(options.validateMessages, name, cloneRule); validator.messages(messages); let result = []; @@ -94,13 +106,13 @@ async function validateRule( : message), ); } else { - result = [messages.default()]; + result = [(messages.default as (() => string))()]; } } if (!result.length && subRuleField) { const subResults: string[][] = await Promise.all( - value.map((subValue: any, i: number) => + (value as StoreValue[]).map((subValue: StoreValue, i: number) => validateRule(`${name}.${i}`, subValue, subRuleField, options), ), ); @@ -117,7 +129,7 @@ async function validateRule( */ export function validateRules( namePath: InternalNamePath, - value: any, + value: StoreValue, rules: RuleObject[], options: ValidateOptions, ) { @@ -132,7 +144,7 @@ export function validateRules( } return { ...currentRule, - validator(rule: any, val: any, callback: any) { + validator(rule: Rule, val: StoreValue, callback: (error?: string) => void) { let hasPromise = false; // Wrap callback only accept when promise not provided diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 9172a745..c5ec0f1b 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -1,6 +1,6 @@ import setIn from 'lodash/fp/set'; import get from 'lodash/get'; -import { InternalNamePath, NamePath, Store } from '../interface'; +import { InternalNamePath, NamePath, Store, StoreValue } from '../interface'; import { toArray } from './typeUtil'; /** @@ -19,12 +19,12 @@ export function getValue(store: Store, namePath: InternalNamePath) { return value; } -export function setValue(store: any, namePath: InternalNamePath, value: any) { +export function setValue(store: Store, namePath: InternalNamePath, value: StoreValue): Store { const newStore = setIn(namePath, value, store); return newStore; } -export function cloneByNamePathList(store: any, namePathList: InternalNamePath[]) { +export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]) { let newStore = {}; namePathList.forEach(namePath => { const value = getValue(store, namePath); @@ -38,7 +38,7 @@ export function containsNamePath(namePathList: InternalNamePath[], namePath: Int return namePathList && namePathList.some(path => matchNamePath(path, namePath)); } -function isObject(obj: any) { +function isObject(obj: StoreValue) { return typeof obj === 'object' && obj !== null; } @@ -46,24 +46,28 @@ function isObject(obj: any) { * 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(store: Store | any[], values: Store | any[] = {}) { - const isArray: boolean = Array.isArray(store); - const newStore = isArray ? [...(store as any)] : { ...store }; +function internalSetValues(store: T, values: T): T { + const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T; + + 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 value const recursive = isObject(prevValue) && isObject(value) && !Array.isArray(value); - newStore[key] = recursive ? internalSetValues(prevValue, value) : value; + newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : value; }); return newStore; } -export function setValues(store: Store, ...restValues: Store[]) { +export function setValues(store: T, ...restValues: T[]): T { return restValues.reduce( - (current: Store, newStore: Store) => internalSetValues(current, newStore), + (current: T, newStore: T): T => internalSetValues(current, newStore), store, ); } @@ -79,7 +83,8 @@ export function matchNamePath( } // Like `shallowEqual`, but we not check the data which may cause re-render -export function isSimilar(source: any, target: any) { +type SimilarObject = string | number | {}; +export function isSimilar(source: SimilarObject, target: SimilarObject) { if (source === target) { return true; } @@ -107,12 +112,10 @@ export function isSimilar(source: any, target: any) { }); } -export function defaultGetValueFromEvent(...args: any[]) { - const arg = args[0]; - - if (arg && arg.target && 'value' in arg.target) { - return arg.target.value; +export function defaultGetValueFromEvent(event: Event) { + if (event && event.target && 'value' in event.target) { + return (event.target as HTMLInputElement).value; } - return arg; + return event; }