Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 43 additions & 34 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -165,50 +173,51 @@ export type InternalFormInstance = Omit<FormInstance, 'validateFields'> & {
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;
};
}
21 changes: 12 additions & 9 deletions src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ValidateMessages,
InternalValidateFields,
InternalFormInstance,
ValidateErrorEntity,
} from './interface';
import { HOOK_MARK } from './FieldContext';
import { allPromiseFinish } from './utils/asyncUtil';
Expand Down Expand Up @@ -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<Store | ValidateErrorEntity | string[]> = summaryPromise
.then(
(): Promise<Store | string[]> => {
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({
Expand All @@ -487,9 +490,9 @@ export class FormStore {
});

// Do not throw in console
returnPromise.catch(e => e);
returnPromise.catch<ValidateErrorEntity>(e => e);

return returnPromise;
return returnPromise as Promise<Store | ValidateErrorEntity>;
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/utils/NameMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ interface KV<T> {
/**
* NameMap like a `Map` but accepts `string[]` as key.
*/
class NameMap<T = any> {
class NameMap<T> {
private list: KV<T>[] = [];

public clone(): NameMap<T> {
const clone = new NameMap();
const clone: NameMap<T> = new NameMap();
clone.list = this.list.concat();
return clone;
}
Expand Down Expand Up @@ -50,12 +50,12 @@ class NameMap<T = any> {
this.list = this.list.filter(item => !matchNamePath(item.key, key));
}

public map(callback: (kv: KV<T>) => any) {
public map<U>(callback: (kv: KV<T>) => 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;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/asyncUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FieldError } from '../interface';
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
let hasError = false;
let count = promiseList.length;
const results: any[] = [];
const results: FieldError[] = [];

if (!promiseList.length) {
return Promise.resolve([]);
Expand Down
38 changes: 25 additions & 13 deletions src/utils/validateUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ValidateOptions,
ValidateMessages,
RuleObject,
Rule,
StoreValue,
} from '../interface';
import NameMap from './NameMap';
import { containsNamePath, getNamePath, setValues } from './valueUtil';
Expand All @@ -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, string>): string {
return template.replace(/\$\{\w+\}/g, (str: string) => {
const key = str.slice(2, -1);
return kv[key];
Expand All @@ -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<string, string | number>),
name,
enum: (rule.enum || []).join(', '),
};

const replaceFunc = (template: string, additionalKV?: Record<string, any>) => {
const replaceFunc = (template: string, additionalKV?: Record<string, string>) => {
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') {
Expand All @@ -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<string[]> {
Expand All @@ -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 = [];
Expand All @@ -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),
),
);
Expand All @@ -117,7 +129,7 @@ async function validateRule(
*/
export function validateRules(
namePath: InternalNamePath,
value: any,
value: StoreValue,
rules: RuleObject[],
options: ValidateOptions,
) {
Expand All @@ -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
Expand Down
37 changes: 20 additions & 17 deletions src/utils/valueUtil.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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);
Expand All @@ -38,32 +38,36 @@ 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;
}

/**
* 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<T>(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<T>(store: T, ...restValues: T[]): T {
return restValues.reduce(
(current: Store, newStore: Store) => internalSetValues(current, newStore),
(current: T, newStore: T): T => internalSetValues<T>(current, newStore),
store,
);
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}