diff --git a/packages/core/README.md b/packages/core/README.md index 7d8703f2c4a..521a0295080 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,33 +1,1099 @@ -# @uform/core -### 设计理念 +# @ Uform/core + +English | [简体中文](./README.zh-cn.md) + +> The form state core management package does not rely on any third-party UI frameworks. In this package, it mainly includes: +> +> - Manage Form status +> - Management Field status +> - Manage the Validator status +> - Manage dependencies between Form, Field, and Vaidator + +### Backdrop + +In the middle and back-end field, the core is two scenarios, **One is data entry, the other is Data Query + data presentation** , Whether it is data entry or data query, it is implemented by using forms. Only the forms of data presentation are diversified, but they are the easiest to reuse and abstract. Only forms, it involves a lot of Interactive Logic. Therefore, as long as we fundamentally solve the form problem, most of the middle and back-end scenarios are basically solved. UForm is born for this. + + + +### Design Concept + + **Anything comes from Observable Grpah.** -### 核心亮点 -- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 -- 高效更新,精确渲染,无需整树渲染 -- 内置immer.js,智能降级,无需关心浏览器兼容性 -- 更加完备的生命周期钩子 -- 更加完备的校验引擎 - - validateFirst 校验 - - warning 校验(不阻塞提交校验) - - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) - - 校验规则可扩展,正则校验库可扩展 -- 更加灵活的路径解析,匹配,求值,取值引擎 - - 批量匹配数据路径能力 - - 解构求值,解构取值能力 -- 提供了基础表单状态模型之外的状态管理能力 -### 架构图 +### Core highlights + + + +- Time travel, with the help of the first Observable Graph, can record the full state at any time, can also roll back the state to any time, such ability in, retransaction applications and local debugging can maximize their value. +- Efficient update, accurate rendering, no full tree rendering required +- Built-in immer. js, intelligent degradation, no need to care about browser compatibility +- More complete life cycle hook +- More complete verification engine + +- - ValidateFirst verification + - Warning Verification (no blocking submission verification) + - Verification message template engine (a complex verification message solution that does not affect international copy storage) + - The verification rule can be extended, and the regular verification library can be extended. + +- More flexible path parsing, matching, evaluation, value engine + +- - Batch matching data path capability + - Deconstruct evaluation, deconstruct value ability + +- Provides state management capabilities beyond the basic form state model. + + + +### Architecture diagram + + + +![img](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + + + +### Terminology explanation + + + +**FormPath/FormPathPattern** Is an abstract data path form, FormPath is a path class, and FormPathPattern is a path form that can be parsed by FormPath. [Cool-path](https://github.com/janrywang/cool-path) Path parsing matching, ability to evaluate values + + + +**The virtual field** Is a special Field data structure. The difference between the Field and the Field is that it does not manage values. That is to say, it has no relevance to the value of the Form. Usually we use it, more importantly, it acts as a proxy for the status of a UI container. For example, the layout component FormBlock in UForm exists as an independent node in the whole Form Grpah, however, this node type is a VirtualField, but when the final data is submitted, the FormBlock does not pollute the data structure of the submitted data. + + + +**Observable Graph** Form is a unique Observer Tree. With the help of the observer tree, many forms-related internal linkage logic can be implemented. + + + +**Data Path** Is the name attribute of Field/VirtualField, which exists as the data path. + + + +**Node Path** Is the path attribute of Field/VirtualFIeld, which exists as the node path. + + + +For the data path and node path, we can look at the following figure: + + + +![img](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + + + +If there exists such a tree, then: + + + +- The name attribute of field c is a.c, and the path attribute is a.b.c. +- The name attribute of field B is a.b, and the path attribute is a.b. +- The name attribute of field d is a.d, and the path attribute is a.d. +- The name attribute of field e is a.d.e, and the path attribute is a.d.e. + + + +After this explanation, we roughly understand that as long as VirtualField exists in a node path, its data path will skip VirtualField. However, for VirtualField itself, its name attribute contains its own node identification, which is why the name attribute of field B is a.b. + + + +### API + +#### `createForm` + +> Create a Form instance + + + +**Signature** + +``` +createForm(options?: IFormCreatorOptions): IForm +``` + + + +**Usage** + +``` + import { createForm } from '@uform/core' + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } + }) + +const aa = form.registerField({ + path:"aa" +}) + +aa.setState(state=>{ + state.value = 123 +}) +console.log(form.getFormState(state=>state.values)) //{aa:123} +``` + + + +#### `registerValidationFormats` + + + +> Register a regular verification rule set + + + +**Signature** + +``` +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationFormats } from '@uform/core' + + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa", + rules:[{ + format:"number", + message:'This field is not a number.' + }] +}) + +aa.setState(state=>{ + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state=>state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + + + +#### `registerValidationRules` + + + +> The difference between registering a verification rule set and registering formats is that it can register complex verification rules, but the formats are just regular expressions. + + + +**Signature** + +``` +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationRules } from '@uform/core' + + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + + + +#### `registerValidationMTEngine` + + + +> Register a verification message template engine + + + +**Signature** + +``` +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + + + +**Usage** + +``` + import { createForm,registerValidationMTEngine } from '@uform/core' + + registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + + + +#### `setValidationLanguage` + + + +> Set the international language type + + + +**Signature** + +``` +setValidationLanguage(lang: string): void +``` + + + +**Usage** + +``` +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + + + +#### `setValidationLocale` + + + +> Set a language pack + + + +**Signature** + +``` +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + + + +**Usage** + +``` +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US':{ + required:"This field is required." + } +}) +``` + + + +### Classes + + + +#### `new FormPath()` + + + +> The form path engine is responsible for path analysis, matching, evaluation, value, deconstruction evaluation, and deconstruction value. + + + +For more information, see: [ https://github.com/janrywang/cool-path ](https://github.com/janrywang/cool-path) + + + +#### `new FormLifeCyle()` + + + +> Create a lifecycle listener + + + +**Signature** + +``` +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCyle(handler: FormLifeCycleHandler) +new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + + + +**Usage** + +```typescript + import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + + const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + // God mode, full monitoring + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + // Accurate monitoring + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + // Object form accurate listener + } + }), + ] +}) +``` + + + +### Enums + +#### Lifecycletypes + + + +```typescript +enum LifeCycleTypes { +  /** +   * Form LifeCycle +   **/ +  // Form pre-initialization trigger +  ON_FORM_WILL_INIT = 'onFormWillInit', + + // Form initialization trigger +  ON_FORM_INIT = 'onFormInit', + + // Triggered when the form changes +  ON_FORM_CHANGE = 'onFormChange', + + // Triggered when the form is mounted +  ON_FORM_MOUNT = 'onFormMount', + + // Triggered when the form is unloaded +  ON_FORM_UNMOUNT = 'onFormUnmount', + + // Triggered when the form is submitted +  ON_FORM_SUBMIT = 'onFormSubmit', + + // Triggered when the form is reset +  ON_FORM_RESET = 'onFormReset', + + // Triggered when the form submission starts +  ON_FORM_SUBMIT_START = 'onFormSubmitStart', + + // Triggered when the form submission ends +  ON_FORM_SUBMIT_END = 'onFormSubmitEnd', + + // Triggered when the form value changes +  ON_FORM_VALUES_CHANGE = 'onFormValuesChange', + + // Trigger when the form initial value changes +  ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange', + + // Triggered when form validation begins +  ON_FORM_VALIDATE_START = 'onFormValidateStart', + + // Triggered when the form validation ends +  ON_FORM_VALIDATE_END = 'onFormValidateEnd', + + // Triggered when the form event is triggered, used to monitor only manual operations +  ON_FORM_INPUT_CHANGE = 'onFormInputChange', + +  /** +   * FormGraph LifeCycle +   **/ + // Triggered when the form observer tree changes +  ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', + +  /** +   * Field LifeCycle +   **/ + // Triggered when pre-initialized +  ON_FIELD_WILL_INIT = 'onFieldWillInit', + + // Triggered when the field is initialized +  ON_FIELD_INIT = 'onFieldInit', + + // Triggered when the field changes +  ON_FIELD_CHANGE = 'onFieldChange', + + // Triggered when the field event is triggered, used to monitor only manual operations +  ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', + + // Triggered when the field value changes +  ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', + + // Trigger when the initial value of the field changes +  ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange', + + // Triggered when the field is mounted +  ON_FIELD_MOUNT = 'onFieldMount', + + // Trigger when the field is unloaded +  ON_FIELD_UNMOUNT = 'onFieldUnmount' +} +``` + + + +### Interfaces + +#### Iformcreatoroptions + + + +> CreateForm parameter object protocol + + + +```typescript +interface IFormCreatorOptions { +   // Form initial value +   initialValues?: {} + +   // Form value +   values?: {} + +   // Lifecycle listener, here mainly introduced to the instantiated object of FormLifeCycle +   lifecycles?: FormLifeCycle[] + +   // Is it editable, overall control in the Form dimension +   editable?: boolean | ((name: string) => boolean) + +   // Whether to use the dirty check, the default will go immer accurate update +   useDirty?: boolean + +   // Whether to go pessimistic check, stop the subsequent check when the first check fails +   validateFirst?: boolean + +   // Form change event callback +   onChange?: (values: IFormState['values']) => void + +   // Form submission event callback +   onSubmit?: (values: IFormState['values']) => any | Promise + +   // Form reset event callback +   onReset?: () => void + +   // Form verification failure event callback +   onValidateFailed?: (validated: IFormValidateResult) => void +} + +``` + + + +#### Iform + + + +> Form instance object API created by using createForm + + + +```typescript +interface IForm { +  /* +   * Form submission, if the callback parameter returns Promise, +   * Then the entire submission process will hold and load is true. +   * Wait for Promise resolve to trigger the form onFormSubmitEnd event while loading is false +   */ +   submit( +      onSubmit?: (values: IFormState['values']) => any | Promise +    ): Promise<{ +       Validated: IFormValidateResult +       Payload: any //onSubmit callback function return value +   }> +    +   /* +    * Clear the error message, you can pass the FormPathPattern to batch or precise control of the field to be cleared. +    * For example, clearErrors("*(aa,bb,cc)") +    */ +   clearErrors: (pattern?: FormPathPattern) => void +    +   /* +    * Get status changes, mainly used to determine which states in the current life cycle have changed in the form lifecycle hook. +    * For example, hasChanged(state,'value.aa') +    */ +   hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean +    +   /* +    * Reset form +    */ +   reset(options?: { +     // Forced to empty +     forceClear?: boolean +     // Forced check +     validate?: boolean +     // Reset range for batch or precise control of the field to be reset +     selector?: FormPathPattern +   }): Promise +    +   /* +    * Validation form +    */ +   validate(path?: FormPathPattern, options?: { +     // Is it pessimistic check, if the current field encounters the first verification error, stop the subsequent verification process +     first?:boolean +   }): Promise +    +   /* +    * Set the form status +    */ +   setFormState( +     // Operation callback +     callback?: (state: IFormState) => any, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get form status +    */ +   getFormState( +     //transformer +     callback?: (state: IFormState) => any +   ): any +    +   /* +    * Set the field status +    */ +   setFieldState( +     // Field path +     path: FormPathPattern, +     // Operation callback +     callback?: (state: IFieldState) => void, +     // No trigger the event +     silent?: boolean +   ): void +    +   /* +    * Get the field status +    */ +   getFieldState( +     // Field path +     path: FormPathPattern, +     // Transformer +     callback?: (state: IFieldState) => any +   ): any +    +   /* +    * Registration field +    */ +   registerField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field value +    value?: any +    // Field multi-value +    values?: any[] +    // Field initial value +    initialValue?: any +    // Field extension properties +    props?: any +    // Field check rule +    rules?: ValidatePatternRules[] +    // Field is required +    required?: boolean +    // Is the field editable? +    editable?: boolean +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IField +   +  /* +   * Register virtual fields +   */ +  registerVirtualField(props: { +    // Node path +    path?: FormPathPattern +    // Data path +    name?: string +    // Field extension properties +    props?: any +    // Whether the field is dirty check +    useDirty?: boolean +    // Field state calculation container, mainly used to extend the core linkage rules +    computeState?: (draft: IFieldState, prevState: IFieldState) => void +  }): IVirtualField +   +  /* +   * Create a field data operator, which will explain the returned API in detail later. +   */ +  createMutators(field: IField): IMutators +   +  /* +   * Get the form observer tree +   */ +  getFormGraph(): IFormGraph +   +  /* +   * Set the form observer tree +   */ +  setFormGraph(graph: IFormGraph): void +   +  /* +   * Listen to the form life cycle +   */ +  subscribe(callback?: ({ +    type, +    payload +  }: { +    type: string +    payload: any +  }) => void): number +   +  /* +   * Cancel the listening form life cycle +   */ +  unsubscribe(id: number): void +   +  /* +   * Trigger form custom life cycle +   */ +  notify: (type: string, payload?: T) => void +   +  /* +   * Set the field value +   */ +  setFieldValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the field value +   */ +  getFieldValue(path?: FormPathPattern): any +   +  /* +   * Set the initial value of the field +   */ +  setFieldInitialValue(path?: FormPathPattern, value?: any): void +   +  /* +   * Get the initial value of the field +   */ +  getFieldInitialValue(path?: FormPathPattern): any +} +``` + + + +#### Imutators + + + +> The instance API created by crewikiutators is mainly used to operate field data. + + + +```typescript +interface IMutators { + // Changing the field value and multi parameter condition will store all parameters in values + change(...values: any[]): any + // Get focus, trigger active state change + focus(): void + // Lose focus, trigger active / visited status change + blur (): void + // Trigger current field verifier + validate(): Promise + // Whether the value of the current field exists in the values property of form + exist (index?: number | string): Boolean + + /**Array operation method**/ + + // Append data + push(value?: any): any[] + // Pop up tail data + pop (): any[] + // Insert data + insert(index: number, value: any): any[] + // Delete data + remove(index: number | string): any + // Head insertion + unshift(value: any): any[] + // Head ejection + shift(): any[] + // Move element + move($from: number, $to: number): any[] + // Move down + moveDown(index: number): any[] + // Move up + moveUp(index: number): any[] +} +``` + + + +#### The Validator + + + +> Here we mainly list the intermediate type signatures related to verification. + + + +```typescript + type CustomValidator = ( + value: any, + rescription?: ValidateDescription + ) => ValidateResponse + type SyncValidateResponse = null | string | boolean | { + type?: 'error' | 'warning' + message: string + }; + type AsyncValidateResponse = Promise + type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; + + interface IFormValidateResult { + errors: Array<{ + path: string; + messages: string[] + }> + warnings: Array<{ + path: string + messages: string[] + }> +} + +interface ValidateDescription { +     // Regular rule type +     format?: string +     // Custom validator +     validator?: CustomValidator +     // Is it required? +     required?: boolean +     // Customize with regularity +     pattern?: RegExp | string +     // Maximum length rule +     max?: number; +     // Maximum numerical rule +     maximum?: number +     // Exclusive maximum numerical rule +     exclusiveMaximum?: number +     // Exclusive minimum numerical rules +     exclusiveMinimum?: number +     // Minimum value rule +     minimum?: number +     // Minimum length rule +     min?: number +     // Length rule +     len?: number +     // Whether to check the white space +     whitespace?: boolean +     // Enumeration check rules +     enum?: any[] +     // Custom error copy +     message?: string +     // Custom validation rules +     [key: string]: any +} +``` + + + +#### IFormState + + + +> Form the core state + + + +```typescript +interface IFormState { +    +   /**Read-only attribute**/ +    +  // Is it in the original state, pristine is true only when values === initialValues +  pristine: boolean +  // Is it legal, as long as the error length is greater than 0, the valid is false +  valid: boolean +  // Is it illegal, as long as the error length is greater than 0, the valid is true +  invalid: boolean +  // Is it in the check state, it will only be set when calling the validate API +  validating: boolean +  // Is it in the commit state, it will only be set when the submit API is called +  submitting: boolean +  //Error message list +  errors: string[] +  //Alarm message list +  warnings: string[] +   +  /** writable property**/ +   +  // Is it in the loaded state, writable state, as long as validating is true, the state will also be true, the same as false +  loading: boolean +  // Is it in the initial state? +  initialized: boolean +  // Is it editable? +  editable: boolean | ((name: string) => boolean) +  // form value +  values: {} +  // form initial value +  initialValues: {} +  // form mount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger +  mounted: boolean +  // Form unmount, the life cycle hook mentioned earlier, must be triggered by setting the state, the default will not trigger +  unmounted: boolean +  // Form extension properties +  props: FormProps +} +``` + + + +#### IFieldState + + + +> CORE Field status + + + +```typescript +interface IFieldState { +    +  /**Read-only attribute**/ +    +  // State name, FieldState +  displayName?: string +  // Data path +  name: string +  // Node path +  path: string +  // Has been initialized +  initialized: boolean +  // Is it in the original state, the state is true only when value===intialValues +  pristine: boolean +  // Is it in a legal state, as long as the error length is greater than 0, the valid is false +  valid: boolean +  // Is it illegal, as long as the error length is greater than 0, the valid is true +  invalid: boolean +  // Is it in check state? +  validating: boolean +  // Is it modified, if the value changes, the property is true, and will be true throughout the life of the field +  modified: boolean +  // Is it touched? +  touched: boolean +  // Is it activated, when the field triggers the onFocus event, it will be triggered to true, when onBlur is triggered, it is false +  active: boolean +  // Have you ever visited, when the field triggers the onBlur event, it will be triggered to true +  visited: boolean +   +  /** writable property**/ +   +  // Is it visible, note: if the state is false, then the value of the field will not be submitted, and the UI will not display +  visible: boolean +  // Whether to show, note: if the state is false, then the value of the field will be submitted, the UI will not display, similar to the form hidden field +  display: boolean +  // Is it editable? +  editable: boolean +  // Is it in the loading state, note: if the field is in asynchronous verification, loading is true +  loading: boolean +  // Field multi-parameter value, such as when the field onChange trigger, the event callback passed multi-parameter data, then the value of all parameters will be stored here +  values: any[] +  // Field error message +  errors: string[] +  // Field alert message +  warnings: string[] +  // Field value, is equal to values[0] +  value: any +  // Initial value +  initialValue: any +  // Check the rules, the specific type description refers to the following documents +  rules: ValidatePatternRules[] +  // Is it required? +  required: boolean +  // Whether to mount +  mounted: boolean +  // Whether to uninstall +  unmounted: boolean +  // field extension properties +  props: FieldProps +} +``` + + + +#### IVirtualFieldState + + + +> Virtual Field core status + + + +```typescript +interface IVirtualFieldState { +    +   /**Read-only status**/ +    +   // State name, VirtualFieldState +   displayName: string +   // Field data path +   name: string +   // Field node path +   path: string +   // Has been initialized +   initialized: boolean +   +   /** writable status**/ +   +   // Is it visible, note: if the state is false, the UI will not be displayed, the data will not be submitted (because it is a VirtualField) +   visible: boolean +   // Whether to show, note: if the state is false, the UI will not display, the data will not be submitted (because it is VirtualField) +   display: boolean +   // Is it mounted? +   mounted: boolean +   // Has been uninstalled +   unmounted: boolean +   // field extension properties +   props: FieldProps +} +``` + + -![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) +#### IField/IVirtualField -### API -### Interfaces -### Examples +> The instance API created by using registerField/registerVirtualField +```typescript +interface IField/IVirtualField { +   // Batch update container +   batch: (callback?: () => void) => void +   // Get the status +   getState: (callback?: (state: IFieldState) => any) => any +   // Set the status +   setState: ( +      callback?: (state: IFieldState | Draft) => void, +      silent?: boolean +   ) => void +   // Get the source status +   unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any +   // Set the source state +   unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void +   // Get status changes +   hasChanged: (key?: string) => boolean +   // Get the state dirty +   isDirty: (key?: string) => boolean +   // Get state dirty information +   getDirtyInfo: () => StateDirtyMap +} +``` \ No newline at end of file diff --git a/packages/core/README.zh-cn.md b/packages/core/README.zh-cn.md new file mode 100644 index 00000000000..6a1020cf23e --- /dev/null +++ b/packages/core/README.zh-cn.md @@ -0,0 +1,929 @@ +# @uform/core +> 表单状态核心管理包(不依赖任何第三方UI框架),在该包中,它主要做了: +> +> - 管理Form状态 +> - 管理Field状态 +> - 管理Validator状态 +> - 管理Form/Field/Vaidator之间的依赖关系 + +### 背景 + +中后台领域,核心就是两种场景,**一个是数据录入,一个是数据查询+数据展现**,不管是数据录入还是数据查询,都是借助表单来实现,只有数据展现的形式是比较多样化的,但是却是最容易复用和抽象的,只有表单,会涉及大量的交互逻辑,所以,只要我们根本上解决了表单问题,对于中后台场景,基本上解决了大部分中后台场景问题,UForm,就是为此而诞生的。 + +### 设计理念 + +**Anything comes from Observable Grpah.** + +### 核心亮点 + +- 时间旅行,借助首创Observable Graph,可以记录任意时刻的全量状态,也可以将状态回滚至任意时刻,这样的能力在,重事务型应用与本地调试上可以发挥出最大价值 +- 高效更新,精确渲染,无需整树渲染 +- 内置immer.js,智能降级,无需关心浏览器兼容性 +- 更加完备的生命周期钩子 +- 更加完备的校验引擎 + - validateFirst 校验 + - warning 校验(不阻塞提交校验) + - 校验消息模板引擎(不影响国际化文案存储的复杂校验文案消息解决方案) + - 校验规则可扩展,正则校验库可扩展 +- 更加灵活的路径解析,匹配,求值,取值引擎 + - 批量匹配数据路径能力 + - 解构求值,解构取值能力 +- 提供了基础表单状态模型之外的状态管理能力 + +### 架构图 + +![](https://img.alicdn.com/tfs/TB18LXHlVP7gK0jSZFjXXc5aXXa-1428-926.png) + + + +### 术语解释 + +**FormPath/FormPathPattern** 是一个抽象数据路径形式,FormPath是路径类,FormPathPattern是可以被FormPath解析的路径形式,在这里主要使用了 [cool-path](https://github.com/janrywang/cool-path) 路径解析匹配,求值取值能力 + +**VirtualField** 是一个特殊的Field数据结构,它与Field的差异就是,它不管理value,也就是说,它与Form的value是没有关联性的,通常我们使用它,更多的是作为代理一个UI容器的状态,比如:UForm中的布局组件FormBlock,它会在整个Form Grpah中作为一个独立节点而存在,但是这个节点类型就是一个VirtualField,但是最终数据提交的时候,FormBlock并不会污染提交数据的数据结构。 + +**Observable Graph** 是Form独有的观察者树,借助观察者树,可以实现很多表单相关的内部联动逻辑 + +**Data Path** 是Field/VirtualField的name属性,它是作为数据路径而存在 + +**Node Path** 是Field/VirtualFIeld的path属性,它是作为节点路径而存在 + +对于数据路径和节点路径,我们可以看下面这张图: + +![](https://img.alicdn.com/tfs/TB1.rAamG61gK0jSZFlXXXDKFXa-1496-898.png) + +如果存在这样一棵树的话,那么: + +- c字段的name属性则是a.c,path属性是a.b.c +- b字段的name属性是a.b,path属性是a.b +- d字段的name属性是a.d,path属性是a.d +- e字段的name属性是a.d.e,path属性是a.d.e + +这一来解释之后,我们就大概明白了,只要在某个节点路径中,存在VirtualField,那么它的数据路径就会略过VirtualField,但是,对于VirtualField自身这个节点,它的name属性,是包含它自身的节点标识的,这就是为什么b字段的name属性是a.b的原因 + +### API + +--- + +#### `createForm` + +> 创建一个Form实例 + +**签名** + +```typescript +createForm(options?: IFormCreatorOptions): IForm +``` + +**用法** + +```typescript +import { createForm } from '@uform/core' + +const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa" +}) + +aa.setState(state=>{ + state.value = 123 +}) +console.log(form.getFormState(state=>state.values)) //{aa:123} +``` + + + +#### `registerValidationFormats` + +> 注册正则校验规则集 + +**签名** + +```typescript +registerValidationFormats(formats:{ + [formatName in string]: RegExp; +}) : void +``` + + + +**用法** + +```typescript +import { createForm,registerValidationFormats } from '@uform/core' + +registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ +}) + +const form = createForm({ + values:{}, + initialValues:{}, + onChange:(values)=>{ + console.log(values) + } +}) + +const aa = form.registerField({ + path:"aa", + rules:[{ + format:"number", + message:'This field is not a number.' + }] +}) + +aa.setState(state=>{ + state.value = 'hello world' +}) +form.validate() + +console.log(form.getFormState(state=>state.errors)) +/** +[{ + path: 'aa', + messages: [ 'This field is not a number.' ] +}] +**/ +``` + + + +#### `registerValidationRules` + +> 注册校验规则集,与注册formats的差别是,它可以注册复杂校验规则,但是formats只是正则表达式 + +**签名** + +```typescript +registerValidationRules( + rules:{ + [ruleName:string]:(value:any,rule:ValidateDescription)=>boolean + } +) : void +``` + +**用法** + +```typescript +import { createForm,registerValidationRules } from '@uform/core' + +registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123'] +}] +**/ +``` + +#### `registerValidationMTEngine` + +> 注册校验消息模板引擎 + +**签名** + +```typescript +registerValidationMTEngine(callback:(message,context)=>any) : void +``` + +**用法** + +```javascript +import { createForm,registerValidationMTEngine } from '@uform/core' + +registerValidationMTEngine((message,context)=>{ + return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) +}) + +const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + console.log(values) + } +}) + +const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value){ + return value === 123 : 'This field can not be 123 {{scope.outerVariable}}' + }, + scope:{ + outerVariable:'addonAfter' + } + } + ] +}) + +aa.setState(state => { + state.value = '123' +}) +form.validate() + +console.log(form.getFormState(state =>state.errors)) +/** +[{ + path: 'aa', + messages: ['This field can not be 123 addonAfter'] +}] +**/ +``` + + + +#### `setValidationLanguage` + +> 设置国际化语言类型 + +**签名** + +```typescript +setValidationLanguage(lang: string): void +``` + +**用法** + +```javascript +import { setValidationLanguage } from '@uform/core' + +setValidationLanguage('en-US') +``` + + + +#### `setValidationLocale` + +> 设置语言包 + +**签名** + +```typescript +interface ILocaleMessages { + [key: string]: string | ILocaleMessages; +} +interface ILocales { + [lang: string]: ILocaleMessages; +} +setValidationLocale(locale: ILocales) => void +``` + +**用法** + +```javascript +import { setValidationLocale } from '@uform/core' + +setValidationLocale({ + 'en-US':{ + required:"This field is required." + } +}) +``` + + + +### Classes + +#### `new FormPath()` + +> 表单路径引擎,核心负责路径解析,匹配,求值,取值,解构求值,解构取值 + +具体文档参考:https://github.com/janrywang/cool-path + +#### `new FormLifeCyle()` + +> 创建一个生命周期监听器 + +**签名** + +```typescript +type FormLifeCycleHandler = (payload: T, context: any) => void + +new FormLifeCyle(handler: FormLifeCycleHandler) +new FormLifeCyle(...type: LifeCycleTypes, handler: FormLifeCycleHandler...) +new FormLifeCyle(handlerMap: { [key: LifeCycleTypes]: FormLifeCycleHandler }) +``` + +**用法** + +```typescript +import { createForm,FormLifeCycle,LifeCycleTypes } from '@uform/core' + +const form = createForm({ + lifecycles:[ + new FormLifeCycle(({type:LifeCycleTypes,payload:IForm | IField | IVirtualField })=>{ + //上帝模式,全量监听 + }), + new FormLifeCycle( + LifeCycleTypes.ON_FORM_MOUNT, + (payload:IForm | IField | IVirtualField)=>{ + //精确监听 + }), + new FormLifeCycle({ + [LifeCycleTypes.ON_FORM_MOUNT]:(payload:IForm | IField | IVirtualField)=>{ + //对象形式精确监听 + } + }), + ] +}) +``` + + + +### Enums + +--- + +#### LifeCycleTypes + +```typescript +enum LifeCycleTypes { + /** + * Form LifeCycle + **/ + + ON_FORM_WILL_INIT = 'onFormWillInit', //表单预初始化触发 + ON_FORM_INIT = 'onFormInit', //表单初始化触发 + ON_FORM_CHANGE = 'onFormChange', //表单变化时触发 + ON_FORM_MOUNT = 'onFormMount', //表单挂载时触发 + ON_FORM_UNMOUNT = 'onFormUnmount', //表单卸载时触发 + ON_FORM_SUBMIT = 'onFormSubmit', //表单提交时触发 + ON_FORM_RESET = 'onFormReset', //表单重置时触发 + ON_FORM_SUBMIT_START = 'onFormSubmitStart', //表单提交开始时触发 + ON_FORM_SUBMIT_END = 'onFormSubmitEnd', //表单提交结束时触发 + ON_FORM_VALUES_CHANGE = 'onFormValuesChange', //表单值变化时触发 + ON_FORM_INITIAL_VALUES_CHANGE = 'onFormInitialValuesChange',//表单初始值变化时触发 + ON_FORM_VALIDATE_START = 'onFormValidateStart', //表单校验开始时触发 + ON_FORM_VALIDATE_END = 'onFormValidateEnd', //表单校验结束时触发 + ON_FORM_INPUT_CHANGE = 'onFormInputChange', //表单事件触发时触发,用于只监控人工操作 + /** + * FormGraph LifeCycle + **/ + ON_FORM_GRAPH_CHANGE = 'onFormGraphChange', //表单观察者树变化时触发 + + /** + * Field LifeCycle + **/ + + ON_FIELD_WILL_INIT = 'onFieldWillInit', //字段预初始化时触发 + ON_FIELD_INIT = 'onFieldInit', //字段初始化时触发 + ON_FIELD_CHANGE = 'onFieldChange', //字段变化时触发 + ON_FIELD_INPUT_CHANGE = 'onFieldInputChange', //字段事件触发时触发,用于只监控人工操作 + ON_FIELD_VALUE_CHANGE = 'onFieldValueChange', //字段值变化时触发 + ON_FIELD_INITIAL_VALUE_CHANGE = 'onFieldInitialValueChange',//字段初始值变化时触发 + ON_FIELD_MOUNT = 'onFieldMount', //字段挂载时触发 + ON_FIELD_UNMOUNT = 'onFieldUnmount' //字段卸载时触发 +} +``` + + + +### Interfaces + +--- + +#### IFormCreatorOptions + +> createForm参数对象协议 + +```typescript +interface IFormCreatorOptions { + //初始值 + initialValues?: {} + //值 + values?: {} + //生命周期监听器,在这里主要传入FormLifeCycle的实例化对象 + lifecycles?: FormLifeCycle[] + //是否可编辑,在Form维度整体控制 + editable?: boolean | ((name: string) => boolean) + //是否使用脏检查,默认会走immer精确更新 + useDirty?: boolean + //是否走悲观校验,遇到第一个校验失败就停止后续校验 + validateFirst?: boolean + //表单变化事件回调 + onChange?: (values: IFormState['values']) => void + //表单提交事件回调 + onSubmit?: (values: IFormState['values']) => any | Promise + //表单重置事件回调 + onReset?: () => void + //表单校验失败事件回调 + onValidateFailed?: (validated: IFormValidateResult) => void +} +``` + + + +#### IForm + +> 通过createForm创建出来的Form实例对象API + +```typescript +interface IForm { + /* + * 表单提交,如果回调参数返回Promise, + * 那么整个提交流程会hold住,同时loading为true, + * 等待Promise resolve才触发表单onFormSubmitEnd事件,同时loading为false + */ + submit( + onSubmit?: (values: IFormState['values']) => any | Promise + ): Promise<{ + validated: IFormValidateResult + payload: any //onSubmit回调函数返回值 + }> + + /* + * 清空错误消息,可以通过传FormPathPattern来批量或精确控制要清空的字段, + * 比如clearErrors("*(aa,bb,cc)") + */ + clearErrors: (pattern?: FormPathPattern) => void + + /* + * 获取状态变化情况,主要用于在表单生命周期钩子内判断当前生命周期中有哪些状态发生了变化, + * 比如hasChanged(state,'value.aa') + */ + hasChanged(target: IFormState | IFieldState | IVirtualFieldState, path: FormPathPattern): boolean + + /* + * 重置表单 + */ + reset(options?: { + //强制清空 + forceClear?: boolean + //强制校验 + validate?: boolean + //重置范围,用于批量或者精确控制要重置的字段 + selector?: FormPathPattern + }): Promise + + /* + * 校验表单 + */ + validate(path?: FormPathPattern, options?: { + //是否悲观校验,如果当前字段遇到第一个校验错误则停止后续校验流程 + first?:boolean + }): Promise + + /* + * 设置表单状态 + */ + setFormState( + //操作回调 + callback?: (state: IFormState) => any, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取表单状态 + */ + getFormState( + //transformer + callback?: (state: IFormState) => any + ): any + + /* + * 设置字段状态 + */ + setFieldState( + //字段路径 + path: FormPathPattern, + //操作回调 + callback?: (state: IFieldState) => void, + //是否不触发事件 + silent?: boolean + ): void + + /* + * 获取字段状态 + */ + getFieldState( + //字段路径 + path: FormPathPattern, + //transformer + callback?: (state: IFieldState) => any + ): any + + /* + * 注册字段 + */ + registerField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段值 + value?: any + //字段多参值 + values?: any[] + //字段初始值 + initialValue?: any + //字段扩展属性 + props?: any + //字段校验规则 + rules?: ValidatePatternRules[] + //字段是否必填 + required?: boolean + //字段是否可编辑 + editable?: boolean + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IField + + /* + * 注册虚拟字段 + */ + registerVirtualField(props: { + //节点路径 + path?: FormPathPattern + //数据路径 + name?: string + //字段扩展属性 + props?: any + //字段是否走脏检查 + useDirty?: boolean + //字段状态计算容器,主要用于扩展核心联动规则 + computeState?: (draft: IFieldState, prevState: IFieldState) => void + }): IVirtualField + + /* + * 创建字段数据操作器,后面会详细解释返回的API + */ + createMutators(field: IField): IMutators + + /* + * 获取表单观察者树 + */ + getFormGraph(): IFormGraph + + /* + * 设置表单观察者树 + */ + setFormGraph(graph: IFormGraph): void + + /* + * 监听表单生命周期 + */ + subscribe(callback?: ({ + type, + payload + }: { + type: string + payload: any + }) => void): number + + /* + * 取消监听表单生命周期 + */ + unsubscribe(id: number): void + + /* + * 触发表单自定义生命周期 + */ + notify: (type: string, payload?: T) => void + + /* + * 设置字段值 + */ + setFieldValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段值 + */ + getFieldValue(path?: FormPathPattern): any + + /* + * 设置字段初始值 + */ + setFieldInitialValue(path?: FormPathPattern, value?: any): void + + /* + * 获取字段初始值 + */ + getFieldInitialValue(path?: FormPathPattern): any +} +``` + + + +#### Imutators + +> 通过createMutators创建出来的实例API,主要用于操作字段数据 + +```typescript +interface IMutators { + //改变字段值,多参情况,会将所有参数存在values中 + change(...values: any[]): any + //获取焦点,触发active状态改变 + focus(): void + //失去焦点,触发active/visited状态改变 + blur(): void + //触发当前字段校验器 + validate(): Promise + //当前字段的值是否在Form的values属性中存在 + exist(index?: number | string): boolean + + /**数组操作方法**/ + + //追加数据 + push(value?: any): any[] + //弹出尾部数据 + pop(): any[] + //插入数据 + insert(index: number, value: any): any[] + //删除数据 + remove(index: number | string): any + //头部插入 + unshift(value: any): any[] + //头部弹出 + shift(): any[] + //移动元素 + move($from: number, $to: number): any[] + //下移 + moveDown(index: number): any[] + //上移 + moveUp(index: number): any[] + +} +``` + + + +#### Validator + +> 这里主要列举校验相关的中间类型签名 + +```typescript +type CustomValidator = (value: any, rescription?: ValidateDescription) => ValidateResponse +type SyncValidateResponse = null | string | boolean | { + type?: 'error' | 'warning'; + message: string; +}; +type AsyncValidateResponse = Promise +type ValidateResponse = SyncValidateResponse | AsyncValidateResponse; + +interface IFormValidateResult { + errors: Array<{ + path: string; + messages: string[]; + }>; + warnings: Array<{ + path: string; + messages: string[]; + }>; +} + +interface ValidateDescription { + //正则规则类型 + format?: string; + //自定义校验规则 + validator?: CustomValidator; + //是否必填 + required?: boolean; + //自定以正则 + pattern?: RegExp | string; + //最大长度规则 + max?: number; + //最大数值规则 + maximum?: number; + //封顶数值规则 + exclusiveMaximum?: number; + //封底数值规则 + exclusiveMinimum?: number; + //最小数值规则 + minimum?: number; + //最小长度规则 + min?: number; + //长度规则 + len?: number; + //是否校验空白符 + whitespace?: boolean; + //枚举校验规则 + enum?: any[]; + //自定义错误文案 + message?: string; + //自定义校验规则 + [key: string]: any; +} +``` + + + + + +#### IFormState + +> Form核心状态 + +```typescript +interface IFormState { + + /**只读属性**/ + + //是否处于原始态,只有values===initialValues时,pristine为true + pristine: boolean + //是否合法,只要errors长度大于0的时候valid为false + valid: boolean + //是否非法,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态,只有在调用validate API的时候才会被设置 + validating: boolean + //是否处于提交态,只有在调用submit API的时候才会被设置 + submitting: boolean + //错误消息了列表 + errors: string[] + //告警消息列表 + warnings: string[] + + /**可写属性**/ + + //是否处于加载态,可写状态,只要validating为true时,该状态也会为true,为false时同理 + loading: boolean + //是否处于初始态 + initialized: boolean + //是否可编辑 + editable: boolean | ((name: string) => boolean) + //表单值 + values: {} + //表单初始值 + initialValues: {} + //表单挂载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + mounted: boolean + //表单卸载,前面讲到的生命周期钩子,是必须通过设置该状态来触发的,默认不会触发 + unmounted: boolean + //表单扩展属性 + props: FormProps +} +``` + + + +#### IFieldState + +> 核心Field状态 + +```typescript +interface IFieldState { + + /**只读属性**/ + + //状态名称,FieldState + displayName?: string + //数据路径 + name: string + //节点路径 + path: string + //是否已经初始化 + initialized: boolean + //是否处于原始态,只有value===intialValues时的时候该状态为true + pristine: boolean + //是否处于合法态,只要errors长度大于0的时候valid为false + valid: boolean + //是否处于非法态,只要errors长度大于0的时候valid为true + invalid: boolean + //是否处于校验态 + validating: boolean + //是否被修改,如果值发生变化,该属性为true,同时在整个字段的生命周期内都会为true + modified: boolean + //是否被触碰 + touched: boolean + //是否被激活,字段触发onFocus事件的时候,它会被触发为true,触发onBlur时,为false + active: boolean + //是否访问过,字段触发onBlur事件的时候,它会被触发为true + visited: boolean + + /**可写属性**/ + + //是否可见,注意:该状态如果为false,那么字段的值不会被提交,同时UI不会显示 + visible: boolean + //是否展示,注意:该状态如果为false,那么字段的值会提交,UI不会展示,类似于表单隐藏域 + display: boolean + //是否可编辑 + editable: boolean + //是否处于loading状态,注意:如果字段处于异步校验时,loading为true + loading: boolean + //字段多参值,比如字段onChange触发时,给事件回调传了多参数据,那么这里会存储所有参数的值 + values: any[] + //字段错误消息 + errors: string[] + //字段告警消息 + warnings: string[] + //字段值,与values[0]是恒定相等 + value: any + //初始值 + initialValue: any + //校验规则,具体类型描述参考后面文档 + rules: ValidatePatternRules[] + //是否必填 + required: boolean + //是否挂载 + mounted: boolean + //是否卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + + +#### IVirtualFieldState + +> 虚拟Field核心状态 + +```typescript +interface IVirtualFieldState { + + /**只读状态**/ + + //状态名称,VirtualFieldState + displayName: string + //字段数据路径 + name: string + //字段节点路径 + path: string + //是否已经初始化 + initialized: boolean + + /**可写状态**/ + + //是否可见,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + visible: boolean + //是否展示,注意:该状态如果为false,UI不会显示,数据也不会提交(因为它是VirtualField) + display: boolean + //是否已挂载 + mounted: boolean + //是否已卸载 + unmounted: boolean + //字段扩展属性 + props: FieldProps +} +``` + + + +#### IField/IVirtualField + +> 通过registerField/registerVirtualField创建出来的实例API + +```typescript +interface IField/IVirtualField { + //批量更新容器 + batch: (callback?: () => void) => void + //获取状态 + getState: (callback?: (state: IFieldState) => any) => any + //设置状态 + setState: (callback?: (state: IFieldState | Draft) => void, silent?: boolean) => void + //获取源状态 + unsafe_getSourceState: (callback?: (state: IFieldState) => any) => any + //设置源状态 + unsafe_setSourceState: (callback?: (state: IFieldState) => void) => void + //获取状态变化情况 + hasChanged: (key?: string) => boolean + //获取状态脏 + isDirty: (key?: string) => boolean + //获取状态脏信息 + getDirtyInfo: () => StateDirtyMap +} +``` + + diff --git a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap index 09f00018cff..fa3c5b7f29b 100644 --- a/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/index.spec.ts.snap @@ -617,7 +617,7 @@ Object { undefined, ], "visible": true, - "visited": true, + "visited": false, "warnings": Array [], }, } diff --git a/packages/core/src/__tests__/index.spec.ts b/packages/core/src/__tests__/index.spec.ts index 379ac5fe3cd..37b83d86b7c 100644 --- a/packages/core/src/__tests__/index.spec.ts +++ b/packages/core/src/__tests__/index.spec.ts @@ -1,5 +1,13 @@ import { isEqual } from '@uform/shared' -import { createForm, LifeCycleTypes, FormLifeCycle, FormPath } from '../index' +import { + createForm, + LifeCycleTypes, + FormLifeCycle, + FormPath, + registerValidationFormats, + registerValidationRules, + registerValidationMTEngine +} from '../index' import { ValidateDescription, ValidatePatternRules } from '@uform/validator' // mock datasource @@ -595,14 +603,14 @@ describe('setFormState', () => { form.setFormState(state => { state.values = { a: '1234' } }) - expect(form.getFormState((state) => state.values)).toEqual({ a: '1234' }) + expect(form.getFormState(state => state.values)).toEqual({ a: '1234' }) expect(fieldChange).toBeCalledTimes(2) expect(formChange).toBeCalledTimes(2) - form.setFormState((state) => state.values = { a: '5678' }, true) - expect(form.getFormState((state) => state.values)).toEqual({ a: '5678' }) + form.setFormState(state => (state.values = { a: '5678' }), true) + expect(form.getFormState(state => state.values)).toEqual({ a: '5678' }) expect(formChange).toBeCalledTimes(2) - expect(fieldChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) }) }) @@ -650,18 +658,18 @@ describe('setFieldState', () => { const fieldChange = jest.fn() const form = createForm({ lifecycles: [ - new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange), + new FormLifeCycle(LifeCycleTypes.ON_FIELD_CHANGE, fieldChange) ] }) form.registerField({ path: 'a' }) - form.getFieldState('a') + form.getFieldState('a') form.setFieldState('a', state => (state.value = '1234')) expect(form.getFieldState('a', state => state.value)).toEqual('1234') expect(fieldChange).toBeCalledTimes(2) form.setFieldState('a', state => (state.value = '5678'), true) expect(form.getFieldState('a', state => state.value)).toEqual('5678') - expect(fieldChange).toBeCalledTimes(2) + expect(fieldChange).toBeCalledTimes(2) }) test('validating and loading', () => { @@ -1089,6 +1097,16 @@ describe('createMutators', () => { })) ).toEqual({ active: true, + visited: false + }) + mutators.blur() + expect( + form.getFieldState('a', state => ({ + active: state.active, + visited: state.visited + })) + ).toEqual({ + active: false, visited: true }) }) @@ -1300,7 +1318,10 @@ describe('major sences', () => { test('dynamic remove with intialValues', async () => { const form = createForm({ initialValues: { - aa: [{ aa: 123, bb: 321 }, { aa: 345, bb: 678 }] + aa: [ + { aa: 123, bb: 321 }, + { aa: 345, bb: 678 } + ] } }) const aa = form.registerField({ path: 'aa' }) @@ -1398,3 +1419,128 @@ describe('major sences', () => { expect(form.getFormGraph()).toMatchSnapshot() }) }) + +describe('validator', () => { + test('registerValidationFormats', async () => { + registerValidationFormats({ + number: /^[+-]?\d+(\.\d+)?$/ + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + format: 'number', + message: 'This field is not a number.' + } + ] + }) + + aa.setState(state => { + state.value = 'hello world' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field is not a number.'] + } + ]) + ) + }) + + test('registerValidationRules', async () => { + registerValidationRules({ + custom: value => { + return value === '123' ? 'This field can not be 123' : '' + } + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + custom: true + } + ] + }) + + aa.setState(state => { + state.value = '123' + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123'] + } + ]) + ) + }) + + test('registerValidationMTEngine', async () => { + registerValidationMTEngine((message, context) => { + return message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { + return FormPath.getIn(context, $0) + }) + }) + + const form = createForm({ + values: {}, + initialValues: {}, + onChange: values => { + // console.log(values) + } + }) + + const aa = form.registerField({ + path: 'aa', + rules: [ + { + validator(value) { + return value === 123 + ? 'This field can not be 123 {{scope.outerVariable}}' + : '' + }, + scope: { + outerVariable: 'addonAfter' + } + } + ] + }) + + aa.setState(state => { + state.value = 123 + }) + await form.validate() + + form.getFormState(state => + expect(state.errors).toEqual([ + { + path: 'aa', + messages: ['This field can not be 123 addonAfter'] + } + ]) + ) + }) +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 38bd8afd1fc..40a70d362b7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,7 +14,8 @@ import { import { FormValidator, setValidationLanguage, - setValidationLocale + setValidationLocale, + ValidateFieldOptions } from '@uform/validator' import { FormHeart } from './shared/lifecycle' import { FormGraph } from './shared/graph' @@ -545,12 +546,12 @@ export function createForm( focus() { field.setState((state: IFieldState) => { state.active = true - state.visited = true }) }, blur() { field.setState((state: IFieldState) => { state.active = false + state.visited = true }) }, push(value?: any) { @@ -752,7 +753,7 @@ export function createForm( async function validate( path?: FormPathPattern, - opts?: {} + opts?: ValidateFieldOptions ): Promise { if (!state.getState(state => state.validating)) { state.unsafe_setSourceState(state => { diff --git a/packages/core/src/shared/model.ts b/packages/core/src/shared/model.ts index 8fc79fe882e..0095cb7c4ba 100644 --- a/packages/core/src/shared/model.ts +++ b/packages/core/src/shared/model.ts @@ -9,13 +9,19 @@ import { FormPathPattern } from '@uform/shared' import produce, { Draft } from 'immer' -import { IStateModelFactory, StateDirtyMap, IModel, StateModel } from '../types' +import { + IStateModelProvider, + IStateModelFactory, + StateDirtyMap, + IModel, + StateModel +} from '../types' const hasProxy = !!globalThisPolyfill.Proxy export const createStateModel = ( Factory: IStateModelFactory -) => { - return class Model extends Subscribable +): IStateModelProvider => { + return class Model extends Subscribable implements IModel { public state: State & { displayName?: string } public props: Props & @@ -218,5 +224,5 @@ export const createStateModel = ( ) : !isEqual(this.prevState, this.state) } - } + } as any } diff --git a/packages/core/src/state/form.ts b/packages/core/src/state/form.ts index 9a2010a0d7a..6920e1c7e80 100644 --- a/packages/core/src/state/form.ts +++ b/packages/core/src/state/form.ts @@ -55,10 +55,12 @@ export const FormState = createStateModel( } else { draft.pristine = false } - if (draft.validating === true) { - draft.loading = true - } else if (draft.validating === false) { - draft.loading = false + if (draft.validating !== prevState.validating) { + if (draft.validating === true) { + draft.loading = true + } else if (draft.validating === false) { + draft.loading = false + } } if (draft.mounted === true && draft.mounted !== prevState.mounted) { draft.unmounted = false diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ceef3f9ce7b..223e8df8a58 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,9 @@ import { FormPath, FormPathPattern, isFn, Subscribable } from '@uform/shared' -import { ValidatePatternRules, ValidateNodeResult } from '@uform/validator' +import { + ValidatePatternRules, + ValidateNodeResult, + ValidateFieldOptions +} from '@uform/validator' import { FormLifeCycle } from './shared/lifecycle' import { Draft } from 'immer' @@ -105,6 +109,10 @@ export interface IStateModelFactory { displayName?: string } +export interface IStateModelProvider { + new (props: P): IModel +} + export interface IFieldState { displayName?: string name: string @@ -155,7 +163,6 @@ export interface IFieldStateProps { editable?: boolean useDirty?: boolean computeState?: (draft: IFieldState, prevState: IFieldState) => void - onChange?: (fieldState: IField) => void } export const isField = (target: any): target is IField => @@ -208,13 +215,12 @@ export interface IFormStateProps { initialValues?: {} values?: {} lifecycles?: FormLifeCycle[] + useDirty?: boolean editable?: boolean | ((name: string) => boolean) + validateFirst?: boolean } export interface IFormCreatorOptions extends IFormStateProps { - useDirty?: boolean - validateFirst?: boolean - editable?: boolean onChange?: (values: IFormState['values']) => void onSubmit?: (values: IFormState['values']) => any | Promise onReset?: () => void @@ -245,7 +251,6 @@ export interface IVirtualFieldStateProps { ) => void name?: string props?: FieldProps - onChange?: (fieldState: IVirtualField) => void } export type IFormValidateResult = ValidateNodeResult @@ -275,9 +280,9 @@ export interface IMutators { remove(index: number | string): any unshift(value: any): any[] shift(): any[] - move($from: number, $to: number): any - moveDown(index: number): any - moveUp(index: number): any + move($from: number, $to: number): any[] + moveDown(index: number): any[] + moveUp(index: number): any[] validate(): Promise exist(index?: number | string): boolean } @@ -297,7 +302,7 @@ export interface IModel extends Subscribable { setState: (callback?: (state: S | Draft) => void, silent?: boolean) => void unsafe_getSourceState: (callback?: (state: S) => any) => any unsafe_setSourceState: (callback?: (state: S) => void) => void - hasChanged: (key?: string) => boolean + hasChanged: (path?: FormPathPattern) => boolean isDirty: (key?: string) => boolean getDirtyInfo: () => StateDirtyMap } @@ -315,7 +320,10 @@ export interface IForm { clearErrors: (pattern?: FormPathPattern) => void hasChanged(target: any, path: FormPathPattern): boolean reset(options?: IFormResetOptions): Promise - validate(path?: FormPathPattern, options?: {}): Promise + validate( + path?: FormPathPattern, + options?: ValidateFieldOptions + ): Promise setFormState(callback?: (state: IFormState) => any, silent?: boolean): void getFormState(callback?: (state: IFormState) => any): any setFieldState( diff --git a/packages/validator/src/rules.ts b/packages/validator/src/rules.ts index dc28bf6786a..e28342d1465 100644 --- a/packages/validator/src/rules.ts +++ b/packages/validator/src/rules.ts @@ -26,6 +26,9 @@ const getLength = (value: any) => const getRuleMessage = (rule: any, type: string) => { if (isFn(rule.validator) || Object.keys(rule).length > 2) { + if (rule.format) { + return rule.message || getMessage(type) + } return getMessage(type) } else { return rule.message || getMessage(type) diff --git a/packages/validator/src/types.ts b/packages/validator/src/types.ts index 4d2b5673e1c..98319e8b9fb 100644 --- a/packages/validator/src/types.ts +++ b/packages/validator/src/types.ts @@ -32,6 +32,7 @@ export interface ValidateDescription { whitespace?: boolean enum?: any[] message?: string + [key: string]: any } export type ValidateRules = ValidateDescription[] @@ -73,7 +74,6 @@ export type ValidateRulesMap = { export interface ValidateFieldOptions { first?: boolean - key?: string } export type ValidateCalculator = ( diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 6ce515eb100..233f2177dac 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -38,7 +38,7 @@ const template = (message: SyncValidateResponse, context: any): string => { if (isFn(FormValidator.template)) { return FormValidator.template(message, context) } - return message.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, $0) => { + return message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => { return FormPath.getIn(context, $0) }) } else if (isObj(message)) { @@ -75,7 +75,7 @@ class FormValidator { } ] } else if (isArr(rules)) { - return rules.reduce((buf, rule) => { + return rules.reduce((buf: any, rule) => { return buf.concat(this.transformRules(rule)) }, []) } else if (isObj(rules)) { @@ -117,8 +117,7 @@ class FormValidator { const payload = await rule(value, ruleObj) const message = template(payload, { ...ruleObj, - value, - key: options.key + value }) if (isStr(payload)) { if (first) {