├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.en-us.md
├── README.md
├── build.json
├── build.plugin.js
├── commitlint.config.js
├── cypress.config.ts
├── cypress
└── support
│ ├── component-index.html
│ ├── component.ts
│ └── types.d.ts
├── demo
├── autounmout.md
├── basic.md
├── custom.md
├── dynamic.md
├── mix.md
├── normalize.md
├── onchange.md
├── redux.md
├── same-name.md
├── seterror.md
├── topath.md
├── useField.md
├── validator.md
├── validatorPromise.md
├── valuename.md
└── watch.md
├── ice.config.js
├── package-lock.json
├── package.json
├── src
├── index.ts
├── types.ts
└── utils.ts
├── test
├── index.spec.tsx
├── options.spec.tsx
├── rules.spec.tsx
├── tsconfig.json
└── utils.spec.tsx
├── tsconfig.build.json
├── tsconfig.json
├── tsdoc.json
└── vite.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | demo
2 | es
3 | lib
4 | build
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "parser": "@typescript-eslint/parser",
7 | "parserOptions": {
8 | "project": "./tsconfig.json"
9 | },
10 | "extends": [
11 | "@alifd/eslint-config-next",
12 | "plugin:@typescript-eslint/recommended",
13 | "prettier"
14 | ],
15 | "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc"],
16 | "overrides": [
17 | {
18 | "files": ["./test/**/*.ts", "./test/**/*.tsx"],
19 | "extends": ["plugin:cypress/recommended"],
20 | "plugins": ["cypress"]
21 | }
22 | ],
23 | "rules": {
24 | "tsdoc/syntax": "error",
25 | "valid-jsdoc": "off",
26 | "max-statements": "off",
27 | "max-len": "off",
28 | "import/prefer-default-export": "off",
29 | "no-unused-vars": "off",
30 | "@typescript-eslint/no-unused-vars": ["warn", {"ignoreRestSiblings": true}],
31 | "no-use-before-define": "off",
32 | "react/no-multi-comp": "off",
33 | "react/jsx-filename-extension": ["error", { "extensions": [".tsx", ".jsx"] }],
34 | "@typescript-eslint/no-use-before-define": "error",
35 | "@typescript-eslint/no-explicit-any": "warn",
36 | "@typescript-eslint/no-this-alias": "warn",
37 | "@typescript-eslint/consistent-type-exports": "warn",
38 | "@typescript-eslint/consistent-type-imports": "warn"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 |
11 | - uses: actions/setup-node@v3
12 | with:
13 | node-version: 14
14 | cache: 'npm'
15 |
16 | - run: npm ci
17 |
18 | - run: npm run test
19 |
20 | - name: coverage
21 | run: bash <(curl -s https://codecov.io/bash)
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # production
7 | build/
8 | dist/
9 | tmp/
10 | lib/
11 | es/
12 |
13 | # misc
14 | .idea/
15 | .happypack
16 | .DS_Store
17 | *.swp
18 | *.dia~
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | coverage/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | es
3 | lib
4 | build
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "printWidth": 120,
5 | "semi": true,
6 | "singleQuote": true,
7 | "overrides": [
8 | {
9 | "files": "package.json",
10 | "options": {
11 | "tabWidth": 2
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 |
3 | language: node_js
4 |
5 | node_js:
6 | - 8
7 |
8 | jobs:
9 | include:
10 | - stage: test
11 | install:
12 | - npm install
13 | - npm install -g codecov
14 | before_script:
15 | - npm i -g npm
16 | script:
17 | - npm run eslint
18 | - npm test
19 | - codecov
20 | after_success:
21 | - bash <(curl -s https://codecov.io/bash)
22 | - stage: release
23 | script: skip
24 | deploy:
25 | provider: script
26 | skip_cleanup: true
27 | script:
28 | - npx semantic-release
29 |
30 |
31 | stages:
32 | - name: test
33 | if: ((branch = master) AND (type = push)) OR (type = pull_request)
34 | - name: release
35 | if: (branch = master) AND (type = push)
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present Alibaba Group Holding Limited, https://www.alibabagroup.com/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.en-us.md:
--------------------------------------------------------------------------------
1 | # Field
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | - category: Components
14 | - family: DataEntry
15 | - chinese: 表单辅助工具
16 | - cols: 1
17 | - type: 表单
18 |
19 | ---
20 | ## Development Guide
21 |
22 | ### When to use
23 |
24 | Fields can be used to manage data when it comes to form data manipulation and validation. After being associated with a component, the form data can be automatically written back, read, and verified.
25 |
26 | ### Use caution
27 |
28 | - With Field `init` components, `value` `onChange` must be placed in init's third argument, otherwise it may be overridden by init.
29 | - `Form` has been deeply optimized with `Field` for `data acquisition` and `automatic verification prompt`. It is recommended to use `Field` in `Form`. Please check Form demo.
30 | - initValue The defaultValue of a similar component, which only takes effect when the component first renders (the ajax asynchronous invocation setting initValue may have missed the first render)
31 | - with `parseName=true` you could use `getValues` to get a struct value, but not work in `getValue` you still need pass complete key
32 |
33 | ### basic use
34 |
35 | ```
36 | Class Demo extends React.Component {
37 | Field = new Field(this); // instance creation
38 |
39 | onClick = ()=>{
40 | Console.log(this.field.getValue('name'));
41 | }
42 | Render() {
43 | Const init = this.field.init;
44 |
45 | // Note: initValue will only be assigned when the component is first initialized. If you are using an asynchronous assignment, use setValue
46 | Return
47 |
48 | Get Data
49 |
50 | }
51 | }
52 | ```
53 |
54 | ### update data
55 | #### Event Updates
56 |
57 | ```
58 | Class Demo extends React.Component {
59 | Field = new Field(this);
60 |
61 | onClick = ()=>{
62 | this.field.setValue('name', 'newvalue'); // Assignment will automatically trigger render
63 | }
64 | Render() {
65 | Const init = this.field.init;
66 |
67 | Return
68 |
69 | Settings
70 |
71 | }
72 | }
73 | ```
74 |
75 | #### props update
76 |
77 | ```
78 | Class Demo extends React.Component {
79 | Field = new Field(this);
80 |
81 | // Set the data before the component is mounted (this can be replaced with initValue)
82 | componentWillMount() {
83 | this.field.setValue('name', 'init Name')
84 | }
85 | // Receive data from props
86 | componentWillReceiveProps(nextProps) {
87 | this.field.setValue('name', nextProps.name)
88 | }
89 | Render() {
90 | Const init = this.field.init;
91 |
92 | Return
93 |
94 |
95 | }
96 | }
97 | ```
98 |
99 | #### ajax update
100 | ```
101 | Class Demo extends React.Component {
102 | Field = new Field(this);
103 |
104 | onClick = ()=>{
105 | Ajax({
106 | Url:'/demo.json',
107 | Success:(json)=>{
108 | // Update of assignment in callback event
109 | this.field.setValue('name',json.name);
110 | }
111 | });
112 | }
113 | Render() {
114 | Const init = this.field.init;
115 |
116 | Return
117 |
118 | Settings
119 |
120 | }
121 | }
122 | ```
123 | #### onChange update monitoring
124 | Two usages:
125 | 1. Unified management
126 |
127 | ```
128 | Class Demo extends React.Component {
129 | Field = new Field(this,{
130 | onChange:(name, value) => {
131 | Switch(name) {
132 | Case 'name1':
133 | this.field.setValue('name2','value set by name1');
134 | Break;
135 | Case 'name2':
136 | this.field.setValue('name1','value set by name2');
137 | Break;
138 | }
139 | }
140 | });
141 | Render() {
142 | Const init = this.field.init;
143 |
144 | Return
145 |
146 |
147 |
148 | }
149 | }
150 | ```
151 |
152 | 2. Individual management
153 |
154 | ```
155 | Class Demo extends React.Component {
156 | Render() {
157 | Const init = this.field.init;
158 |
159 | Return
160 | {
163 | this.field.setValue('name2','value set by name1');
164 | }
165 | }
166 | })} />
167 | {
170 | this.field.setValue('name1','value set by name2');
171 | }
172 | }
173 | })} />
174 |
175 | }
176 | }
177 | ```
178 |
179 | For details, please check Demo Demo `Associate Control`
180 |
181 | ## API
182 | ### initialization
183 | ```
184 | Let myfield = new Field(this [,options]);
185 | ```
186 |
187 | |Parameter | Description | Type | Optional |Default |
188 | |-----------|------------------------------------------|------------|-------|--------|
189 | | this | The incoming call to this class | React.Component | must be set |
190 | | options | Some event configuration, detailed parameters are as follows | Object | Not required | |
191 |
192 | `options` configuration item
193 |
194 | | Parameters | Description | Type |Default |
195 | |-----------|------------------------------------------|-----------|--------|
196 | | onChange | all component changes will arrive here [setValue won't trigger the function] | Function(name,value) | |
197 | |parseName | Whether to translate `name` in `init(name)` (getValues will convert a string with `.` to an object) | Boolean | false|
198 | |forceUpdate | Only the components of PureComponent are recommended to open this forced refresh function, which will cause performance problems (500 components as an example: the render will cost 700ms when it is turned on, and 400ms if it is turned off) | Boolean |false|
199 | | autoUnmount | Automatically delete the Unmout element, if you want to keep the data can be set to false | Boolean |true|
200 | | autoValidate | Automatically validate while value changed | Boolean |true|
201 | | values | initial value| Object ||
202 | | processErrorMessage | function to transform error objects on validation. Main usage is to add `key` prop to React component | Function(error) ||
203 | | afterValidateRerender | function to perform any operations after components have rerendered to show validation results. | Function({errorGroup, options, instance}) - see [afterValidateRerender](#afterValidateRerender) for more information ||
204 |
205 |
206 | #### API Interface
207 | The api interface provided by the object after `new` (eg `myfield.getValues()`) (The api function starting with `set` should not be manipulated in render, which may trigger an infinite loop)
208 |
209 | |Parameter | Description | Type | Optional |Default |
210 | |-----------|------------------------------------------|------------|-------|--------|
211 | |init | Initialize each component, [Detailed Parameters below] (#init))| Function(name:String, option:Object)| ||
212 | | getValues | Get the value of a group of input controls. If no parameters are passed, get the values of all components | Function([names: String[]]) |
213 | | getValue | get the value of a single input control | Function(name: String) |
214 | | setValues | Sets the value of a set of input controls (triggers render, follow the use of react time) | Function(obj: Object) |
215 | | setValue | Sets the value of a single input control (triggers render, follow the use of react time) | Function(name: String, value) |
216 | | validateCallback | Validate and retrieve the values of a set of input fields and Error. | Function([names: String[]], [options: Object], callback: Function(errors, values)) | | |
217 | | validatePromise | Validate and retrieve the values of a set of input fields and Error. Returns a promise| Function([names: String[]], [options: Object], (optional) callback: Function(errors, values)) | | |
218 | |getError | Get Error of a Single Input Control | Function(name: String) | | |
219 | |getErrors | Get Errors of a Group of Input Controls | Function([name: String]) | | |
220 | |setError | Set Error for a Single Input Control | Function(name: String, errors:String/Array[String]) | | |
221 | |setErrors | Set Errors of a Group of Input Controls | Function(obj: Object) | | |
222 | |reset | reset the value of a group of input controls, clear the checksum | Function([names: String[]])| ||
223 | | resetToDefault | Resets the value of a group of input controls to the default | Function([names: String[]])| ||
224 | |getState | Judge check status | Function(name: String)| 'error' 'success' 'loading' '' | '' |
225 | | getNames | Get the key of all components | Function()| ||
226 | |remove | Delete the data of a certain control or a group of controls. After deletion, the validate/value associated with it will be cleared. | Function(name: String/String[])|
227 | | spliceArray | delete data of name like name.{index} | Function(keyMatch: String, index: Number)| | |
228 | | watch | watch field value changes | Function(names: String[], value: unknown, oldValue: unknown, triggerType: 'init' | 'change' | 'setValue' | 'unmount' | 'reset') | | |
229 |
230 | #### init
231 | ```
232 | init(name, options, props)
233 | ```
234 |
235 | |Parameter | Description | Type | Optional |Default |
236 | |-----------|------------------------------------------|------------|-------|--------|
237 | | name | Required unique input control symbol | String |
238 | | options.valueName | attribute name of the component value, such as Checkbox is `checked`, Input is `value` | String | | 'value' |
239 | | options.initValue | The initial value of the component (the component will be read only when rendering for the first time, and later modifying this value is invalid), similar to defaultValue | any | | |
240 | |options.trigger | Name of the event that triggered the data change | String | | 'onChange' |
241 | | options.rules | Checksum Rules | Array/Object | | | |
242 | | options.getValueFormatter | custom way to get value from `onChange` event, Detailed usage see demo `custom data get` | Function(value, ...args) parameter order and components are exactly the same The | | | |
243 | | options.getValueFormatter | custom way to set value. Detailed usage see demo `custom data get` | Function(values) | | | |
244 | |props | Component-defined events can be written here | Object | | | |
245 | | autoValidate | Automatically validate while value changed | Boolean |true|
246 |
247 | return
248 |
249 | ```
250 | {id,value,onChange}
251 | ```
252 |
253 | #### rules
254 |
255 | ```
256 | {
257 | rules:[{ required: true }]
258 | }
259 | ```
260 |
261 | multiple rule
262 |
263 | ```
264 | {
265 | rules:[{required:true,trigger:'onBlur'},{pattern:/abcd/,message:'match abcd'},{validator:(rule, value, callback)=>{callback('got error')}}]
266 | }
267 | ```
268 |
269 | |Parameter | Description | Type | Optional |
270 | |-----------|------------------------------------------|------------|-------|--------|
271 | | required | cannot be empty| Boolean | true | `undefined/null/"/[]` will trigger this rule)
272 | |pattern | check regular expression | regular | | |
273 | |minLength | Minimum string length / Minimum number of arrays | Number | | String/Number/Array |
274 | |maxLength | Maximum length of string / Maximum number of arrays | Number | | String/Number/Array |
275 | |length | string exact length / exact number of arrays | |number | | String/Number/Array |
276 | |min | Min | Number | | String/Number |
277 | |max | maximum | Number | | String/Number |
278 | | format | sum of common patterns | String | url, email, tel, number | String |
279 | | validator | Custom validation, (don't forget to execute `callback()` when validation is successful, otherwise validation will not return) | Function(rule,value,callback) |
280 | | trigger | Name of the event that triggered the check | String/Array | onChange/onBlur/... | onChange |
281 | | message | error message | String | | |
282 |
283 | ## Custom Component Access to Field Standards
284 |
285 | - Supports controlled mode (value+onChange) `Must`
286 | - value control component data display
287 | - onChange callback function when the component changes (the first parameter can be given to value)
288 |
289 | - One complete operation throws an onChange event
290 | For example, there is a process that indicates the status of the progress, it is recommended to increase the API `onProcess`; if there is a Start indicates the startup state, it is recommended to increase the API `onStart`
291 |
292 | - Clear data when `value={undefined}`, field's reset function will send undefined data to all components
293 |
294 | ```
295 | componentWillReceiveProps(nextProps) {
296 | if ('value' in nextProps ) {
297 | this.setState({
298 | value: nextProps.value === undefined? []: nextProps.value // set value after clear
299 | })
300 | }
301 | }
302 | ```
303 |
304 | ## afterValidateRerender
305 |
306 | `afterValidateRerender` receives an object with the following properties
307 |
308 | |Parameter | Description | Type |
309 | |-----------|------------------------------------------|------------|-------|--------|
310 | | errorsGroup | Map of field elements with errors, indexed by name. Access the error by `errorsGroup[field_name].errors` | Object |
311 | | options | field's `options` property | Object |
312 | | instance | fields's `instance` property, which holds references to React components for the field items | Object |
313 |
314 |
315 | ## Known Issues
316 |
317 | - Why doesn't the callback function enter the `this.field.validate` manually? A: Is it safe to define the validator method to ensure that the `callback` can be executed on any branch?
318 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Field
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | - category: Components
14 | - family: DataEntry
15 | - chinese: 表单辅助工具
16 | - cols: 1
17 | - type: 表单
18 |
19 | ---
20 |
21 | ## 开发指南
22 |
23 | ### 何时使用
24 |
25 | 涉及到表单数据操作、校验的地方都可以用 Field 来管理数据。和组件关联后可以自动对表单数据进行回写、读取、校验。
26 |
27 | ### 使用注意
28 |
29 | - 使用 Field `init` 过的组件, `value` `onChange` 必须放在 init 的第三个参数, 否则有可能被 init 覆盖。
30 | - `Form`已经和`Field` 在`数据获取`和`自动校验提示`方面做了深度优化,建议在`Form`中使用`Field`, 请查看 Form demo。
31 | - initValue 类似组件的 defaultValue 只有在组件第一次 render 的时候才生效(ajax 异步调用设置 initValue 可能已经错过了第一次 render)
32 | - autoUnmount 默认打开的,如果需要保留会 `自动卸载的组件` 数据请关闭此项
33 | - `parseName=true` 可以通过 `getValues` 获取到结构化的数据, 但是 getValue 还是必须传完整 key 值
34 |
35 | ### 基本使用
36 |
37 | ```
38 | class Demo extends React.Component {
39 | field = new Field(this); // 实例创建
40 |
41 | onClick = ()=>{
42 | console.log(this.field.getValue('name'));
43 | }
44 | render() {
45 | const init = this.field.init;
46 |
47 | // 注意:initValue只会在组件第一次初始化的时候被赋值,如果你是异步赋值请用setValue
48 | return
49 |
50 | 获取数据
51 |
52 | }
53 | }
54 | ```
55 |
56 | ### 更新数据
57 |
58 | #### 事件更新
59 |
60 | ```
61 | class Demo extends React.Component {
62 | field = new Field(this);
63 |
64 | onClick = ()=>{
65 | this.field.setValue('name', 'newvalue'); // 赋值会自动触发render
66 | }
67 | render() {
68 | const init = this.field.init;
69 |
70 | return
71 |
72 | 设置数据
73 |
74 | }
75 | }
76 | ```
77 |
78 | #### props 更新
79 |
80 | ```
81 | class Demo extends React.Component {
82 | field = new Field(this);
83 |
84 | // 在组件挂载之前把数据设置进去(可以用initValue替代这种用法)
85 | componentWillMount() {
86 | this.field.setValue('name', 'init Name')
87 | }
88 | // 接收来自props的数据
89 | componentWillReceiveProps(nextProps) {
90 | this.field.setValue('name', nextProps.name)
91 | }
92 | render() {
93 | const init = this.field.init;
94 |
95 | return
96 |
97 |
98 | }
99 | }
100 | ```
101 |
102 | #### ajax 更新
103 |
104 | ```
105 | class Demo extends React.Component {
106 | field = new Field(this);
107 |
108 | onClick = ()=>{
109 | Ajax({
110 | url:'/demo.json',
111 | success:(json)=>{
112 | // 回调事件中赋值更新
113 | this.field.setValue('name',json.name);
114 | }
115 | });
116 | }
117 | render() {
118 | const init = this.field.init;
119 |
120 | return
121 |
122 | 设置数据
123 |
124 | }
125 | }
126 | ```
127 |
128 | #### onChange 更新监控
129 |
130 | 两种用法:
131 |
132 | 1. 统一管理
133 |
134 | ```
135 | class Demo extends React.Component {
136 | field = new Field(this,{
137 | onChange:(name, value) => {
138 | switch(name) {
139 | case 'name1':
140 | this.field.setValue('name2','value set by name1');
141 | break;
142 | case 'name2':
143 | this.field.setValue('name1','value set by name2');
144 | break;
145 | }
146 | }
147 | });
148 | render() {
149 | const init = this.field.init;
150 |
151 | return
152 |
153 |
154 |
155 | }
156 | }
157 | ```
158 |
159 | 2. 各自管理
160 |
161 | ```
162 | class Demo extends React.Component {
163 | render() {
164 | const init = this.field.init;
165 |
166 | return
167 | {
170 | this.field.setValue('name2','value set by name1');
171 | }
172 | }
173 | })} />
174 | {
177 | this.field.setValue('name1','value set by name2');
178 | }
179 | }
180 | })} />
181 |
182 | }
183 | }
184 | ```
185 |
186 | 详细请查看 demo 演示 `关联控制`
187 |
188 | ## API
189 |
190 | ### 初始化
191 |
192 | ```
193 | let myfield = new Field(this [,options]);
194 | ```
195 |
196 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
197 | | ------- | -------------------------- | --------------- | -------- | ------ |
198 | | this | 传入调用 class 的 this | React.Component | 必须设置 | |
199 | | options | 一些事件配置, 详细参数如下 | Object | 非必须 | |
200 |
201 | `options` 配置项
202 |
203 | | 参数 | 说明 | 类型 | 默认值 |
204 | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------ |
205 | | onChange | 所有组件的 change 都会到达这里[setValue 不会触发该函数] | Function(name,value) | |
206 | | parseName | 是否翻译`init(name)`中的`name`(getValues 会把带`.`的字符串转换成对象) | Boolean | false |
207 | | forceUpdate | 仅建议 PureComponent 的组件打开此强制刷新功能,会带来性能问题(500 个组件为例:打开的时候 render 花费 700ms, 关闭时候 render 花费 400ms) | Boolean | false |
208 | | autoUnmount | 自动删除 Unmout 元素,如果想保留数据可以设置为 false | Boolean | true |
209 | | autoValidate | 是否修改数据的时候就自动触发校验, 设为 false 后只能通过 validate() 来触发校验 | Boolean | true |
210 | | values | 初始化数据 | Object | |
211 |
212 | #### API 接口
213 |
214 | `new`之后的对象提供的 api 接口 (例:`myfield.getValues()`)(`set` 开头的 api 函数不要在 render 里面操作, 可能会触发死循环)
215 |
216 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
217 | | ---------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------ | ------ |
218 | | init | 初始化每个组件,[详细参数如下](#init)) | Function(name:String, option:Object) | | |
219 | | getValues | 获取一组输入控件的值,如不传入参数,则获取全部组件的值 | Function([names: String[]]) | | |
220 | | getValue | 获取单个输入控件的值 | Function(name: String) | | |
221 | | setValues | 设置一组输入控件的值(会触发 render,请遵循 react 时机使用) | Function(obj: Object) | | |
222 | | setValue | 设置单个输入控件的值 (会触发 render,请遵循 react 时机使用) | Function(name: String, value) | | |
223 | | validate | 校验并获取一组输入域的值与 Error | Function([names: String[]], [options: Object], callback: Function(errors, values)) | | |
224 | | getError | 获取单个输入控件的 Error | Function(name: String) | | |
225 | | getErrors | 获取一组输入控件的 Error | Function([name: String]) | | |
226 | | setError | 设置单个输入控件的 Error | Function(name: String, errors:String/Array[String]) | | |
227 | | setErrors | 设置一组输入控件的 Error | Function(obj: Object) | | |
228 | | reset | 重置一组输入控件的值、清空校验 | Function([names: String[]]) | | |
229 | | resetToDefault | 重置一组输入控件的值为默认值 | Function([names: String[]]) | | |
230 | | getState | 判断校验状态 | Function(name: String) | 'error' 'success' 'loading' '' | '' |
231 | | getNames | 获取所有组件的 key | Function() | | |
232 | | remove | 删除某一个或者一组控件的数据,删除后与之相关的 validate/value 都会被清空 | Function(name: String/String[]) | | |
233 | | addArrayValue | 添加 name 是数组格式的数据, 并且自动处理其他 name 的数组错位问题 | Function(key: String, index: Number, value1, value2, ...) | | |
234 | | deleteArrayValue | 删除 name 是数组格式的数据, 并且自动处理其他 name 的数组错位问题 | Function(key: String, index: Number, howmany) | | |
235 | | watch | 监听字段值变化 | Function(names: String[], value: unknown, oldValue: unknown, triggerType: 'init' | 'change' | 'setValue' | 'unmount' | 'reset') | | |
236 |
237 | #### init
238 |
239 | ```
240 | init(name, options, props)
241 | ```
242 |
243 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
244 | | ------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------ | ---------- |
245 | | id | 自定义的表单域 id,如不提供则使用 name 替代 | String | | |
246 | | name | 必填输入控件唯一标志 | String | | |
247 | | options.valueName | 组件值的属性名称,如 Checkbox 的是 `checked`,Input 是 `value` | String | | 'value' |
248 | | options.initValue | 组件初始值(组件第一次 render 的时候才会读取,后面再修改此值无效),类似 defaultValue | any | | |
249 | | options.trigger | 触发数据变化的事件名称 | String | | 'onChange' |
250 | | options.rules | 校验规则 | Array/Object | | | |
251 | | options.getValueFormatter | 自定义从组件获取 `value` 的方式,详细用法查看 demo `自定义数据获取` | Function(value,...args) 参数顺序和组件的 onChange 完全一致 | | | |
252 | | options.setValueFormatter | 自定义转换 `value` 为组件需要的数据 ,详细用法查看 demo `自定义数据获取` | Function(value) | | | |
253 | | props | 组件自定义的事件可以写在这里 | Object | | | |
254 | | autoValidate | 是否修改数据的时候自动触发校验单个组件的校验, 设为 false 后只能通过 validate() 来触发校验 | Boolean | true |
255 |
256 | 返回值
257 |
258 | ```
259 | {id,value,onChange}
260 | ```
261 |
262 | #### rules
263 |
264 | ```
265 | {
266 | rules:[{ required: true }]
267 | }
268 | ```
269 |
270 | 多个 rule
271 |
272 | ```
273 | {
274 | rules:[{required:true,trigger:'onBlur'},{pattern:/abcd/,message:'abcd不能缺'},{validator:(rule, value, callback)=>{callback('出错了')}}]
275 | }
276 | ```
277 |
278 | | 参数 | 说明 | 类型 | 可选值 | 使用类型 |
279 | | --------- | --------------------------------------------------------------------- | ----------------------------- | ----------------------- | ------------------------------------ |
280 | | required | 不能为空 | Boolean | true | `undefined/null/“”/[]` 会触发此规则) |
281 | | pattern | 校验正则表达式 | 正则 | | |
282 | | minLength | 字符串最小长度 / 数组最小个数 | Number | | String/Number/Array |
283 | | maxLength | 字符串最大长度 / 数组最大个数 | Number | | String/Number/Array |
284 | | length | 字符串精确长度 / 数组精确个数 | Number | | String/Number/Array |
285 | | min | 最小值 | Number | | String/Number |
286 | | max | 最大值 | Number | | String/Number |
287 | | format | 对常用 pattern 的总结 | String | url、email、tel、number | String |
288 | | validator | 自定义校验,(校验成功的时候不要忘记执行 `callback()`,否则会校验不返回) | Function(rule,value,callback) | | |
289 | | trigger | 触发校验的事件名称 | String/Array | onChange/onBlur/... | onChange |
290 | | message | 出错时候信息 | String | | |
291 |
292 | ## 自定义组件接入 Field 标准
293 |
294 | - 支持受控模式(value+onChange) `必须`
295 |
296 | - value 控制组件数据展现
297 | - onChange 组件发生变化时候的回调函数(第一个参数可以给到 value)
298 |
299 | - 一次完整操作抛一次 onChange 事件 `建议`
300 | 比如有 Process 表示进展中的状态,建议增加 API `onProcess`;如果有 Start 表示启动状态,建议增加 API `onStart`
301 |
302 | - `value={undefined}`的时候清空数据, field 的 reset 函数会给所有组件下发 undefined 数据 `建议`
303 |
304 | ```
305 | componentWillReceiveProps(nextProps) {
306 | if ('value' in nextProps ) {
307 | this.setState({
308 | value: nextProps.value === undefined? []: nextProps.value // 设置组件的被清空后的数值
309 | })
310 | }
311 | }
312 | ```
313 |
314 | ## 已知问题
315 |
316 | - 为何手动调用`this.field.validate`的时候进不了回调函数? 答: 是不是自定义了 validator 方法,确保`callback`在任何分支下都能被执行到。
317 |
--------------------------------------------------------------------------------
/build.json:
--------------------------------------------------------------------------------
1 | {
2 | "disableGenerateStyle": true,
3 | "plugins": [
4 | "./build.plugin.js",
5 | "build-plugin-component",
6 | "build-plugin-fusion",
7 | [
8 | "build-plugin-moment-locales",
9 | {
10 | "locales": ["zh-cn"]
11 | }
12 | ]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/build.plugin.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const modifyPkgHomePage = require('build-plugin-component/src/utils/modifyPkgHomePage');
3 |
4 | module.exports = ({ context, onHook }) => {
5 | const { rootDir, pkg } = context;
6 | onHook('after.build.compile', async () => {
7 | // 提前中断 babel compile 任务,用 tsc 方式编译
8 | await modifyPkgHomePage(pkg, rootDir);
9 | process.exit(0);
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "rules": {
3 | "header-max-length": [2, "always", 72],
4 | "scope-case": [2, "always", "pascal-case"],
5 | "subject-empty": [2, "never"],
6 | "subject-full-stop": [2, "never", "."],
7 | "type-empty": [2, "never"],
8 | "type-case": [2, "always", "lower-case"],
9 | "type-enum": [2, "always",
10 | [
11 | "typescript",
12 | "deprecated",
13 | "build",
14 | "chore",
15 | "ci",
16 | "docs",
17 | "feat",
18 | "fix",
19 | "perf",
20 | "refactor",
21 | "revert",
22 | "style",
23 | "test",
24 | "temp"
25 | ]
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | component: {
5 | devServer: {
6 | framework: 'react',
7 | bundler: 'vite',
8 | },
9 | specPattern: ['test/**/*.spec.{ts,tsx}'],
10 | viewportWidth: 1000,
11 | viewportHeight: 600,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components Test
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { mount } from 'cypress/react';
4 |
5 | declare global {
6 | // eslint-disable-next-line @typescript-eslint/no-namespace
7 | namespace Cypress {
8 | interface Chainable {
9 | mount: typeof mount;
10 | }
11 | }
12 | }
13 |
14 | Cypress.Commands.add('mount', mount);
15 |
--------------------------------------------------------------------------------
/cypress/support/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace Chai {
4 | export interface Assert {
5 | deepEqual(actual: unknown, expected: unknown, message?: string): void;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/demo/autounmout.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自动卸载 - auto unmount
3 | order: 6
4 | ---
5 |
6 | autoUnmount 默认为 true,当组件被 unmount 的时候会自动删除数据. autoUnmount 设置为 false 后,会一直保存数据.
7 |
8 | ---
9 |
10 | autoUnmount is true by default, and data will be deleted automatically. Field will keep data while autoUnmount is set to false.
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Input, Button } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class Demo extends React.Component {
19 | state = {
20 | show: true,
21 | show2: true,
22 | };
23 | field = new Field(this);
24 | field2 = new Field(this, { autoUnmount: false });
25 |
26 | render() {
27 | return (
28 |
29 | {this.state.show ? (
30 |
35 | ) : null}
36 | {
38 | console.log(
39 | 'value auto delete',
40 | this.field.getValues()
41 | );
42 | }}
43 | style={{ marginLeft: 4 }}
44 | >
45 | print
46 |
47 | this.setState({ show: false })}
49 | warning
50 | style={{ marginLeft: 4 }}
51 | >
52 | delete
53 |
54 |
55 |
56 | {this.state.show2 ? (
57 |
62 | ) : null}
63 | {
65 | console.log(
66 | 'value always exist',
67 | this.field2.getValues()
68 | );
69 | }}
70 | style={{ marginLeft: 4 }}
71 | >
72 | print
73 |
74 | this.setState({ show2: false })}
76 | warning
77 | style={{ marginLeft: 4 }}
78 | >
79 | delete
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | ReactDOM.render( , mountNode);
87 | ```
88 |
--------------------------------------------------------------------------------
/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 基本 - basic
3 | order: 0
4 | ---
5 |
6 | `getValue` `setValue` `reset` 的使用
7 |
8 | ---
9 |
10 | usage of `getValue` `setValue` `reset`
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Input, Button } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class App extends React.Component {
19 | field = new Field(this, { values: { input: 0 } });
20 |
21 | onGetValue() {
22 | console.log(this.field.getValue('input'));
23 | }
24 |
25 | render() {
26 | const { init, setValue, reset } = this.field;
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | getValue
35 |
36 | setValue('input', 'set me by click')}
39 | >
40 | setValue
41 |
42 | reset()}>reset
43 |
44 | );
45 | }
46 | }
47 |
48 | ReactDOM.render( , mountNode);
49 | ```
50 |
51 | ```css
52 | .demo .next-btn {
53 | margin-right: 5px;
54 | }
55 | ```
56 |
--------------------------------------------------------------------------------
/demo/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自定义组件 - custom
3 | order: 10
4 | ---
5 |
6 | 自己的组件如何接入Field。
7 |
8 | `最低标准`: 组件支持 `onChange` 读取组件数据。`达到效果`:Field 可以 getValue,但是 setValue 无效
9 |
10 | `完全支持`: 组件支持[受控](https://facebook.github.io/react/docs/forms.html#controlled-components), 也就是支持两个api:`value` `onChange`. value: 设置组件的数据; onChange: 在组件修改的时候在第一个数暴露数据
11 |
12 | ---
13 |
14 | `must`: has api of `onChange`, so you can use `getValue` but you can't `setValue`
15 | `perfect support`: has api of `value` `onChange`. value: set data for component; onChange: return first param for component
16 |
17 | ```jsx
18 | import ReactDOM from 'react-dom';
19 | import React from 'react';
20 | import { Button } from '@alifd/next';
21 | import Field from '@alifd/field';
22 |
23 | class Custom extends React.Component {
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = {
28 | value: typeof props.value === 'undefined' ? [] : props.value,
29 | };
30 | }
31 |
32 | // update value
33 | componentWillReceiveProps(nextProps) {
34 | if ('value' in nextProps) {
35 | this.setState({
36 | value:
37 | typeof nextProps.value === 'undefined'
38 | ? []
39 | : nextProps.value,
40 | });
41 | }
42 | }
43 |
44 | onAdd = () => {
45 | const value = this.state.value.concat([]);
46 | value.push('new');
47 |
48 | this.setState({
49 | value,
50 | });
51 | this.props.onChange(value);
52 | };
53 |
54 | render() {
55 | return (
56 |
57 | {this.state.value.map((v, i) => {
58 | return {v} ;
59 | })}
60 |
61 | Add +{' '}
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 | class App extends React.Component {
69 | field = new Field(this);
70 |
71 | onGetValue() {
72 | console.log(this.field.getValue('custom'));
73 | }
74 |
75 | render() {
76 | const { init, setValue, reset } = this.field;
77 |
78 | return (
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | getValue
87 |
88 | setValue('custom', ['test', 'setValue'])}
91 | >
92 | setValue
93 |
94 | reset()}>reset
95 |
96 | );
97 | }
98 | }
99 | ReactDOM.render( , mountNode);
100 | ```
101 |
102 | ```css
103 | .demo .next-btn {
104 | margin-right: 5px;
105 | }
106 | .custom {
107 | border: 1px dashed;
108 | padding: 4px;
109 | display: inline-block;
110 | }
111 | .custom span {
112 | border: 1px solid green;
113 | padding: 0px 5px;
114 | height: 24px;
115 | display: inline-block;
116 | margin-right: 2px;
117 | }
118 | ```
119 |
--------------------------------------------------------------------------------
/demo/dynamic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 动态表格 - dynamic
3 | order: 7
4 | ---
5 |
6 | 通过 `deleteArrayValue/addArrayValue` 可以往数组格式的数据里面 删除/添加 数据, 并且自动订正其他 name 的 偏移问题
7 |
8 | ---
9 |
10 | by use `deleteArrayValue/addArrayValue` could delete and add array , and fix keys offset problem
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Button, Input, Table } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | const CustomInput = (props) => {
19 | return ;
20 | };
21 |
22 | class Demo extends React.Component {
23 | constructor(props) {
24 | super(props);
25 |
26 | this.idx = 3;
27 |
28 | this.field = new Field(this, {
29 | parseName: true,
30 | values: {
31 | name: [0, 1, 2, 3].map((i) => {
32 | return { id: i, input: i, custom: i };
33 | }),
34 | },
35 | });
36 | }
37 |
38 | getValues = () => {
39 | const values = this.field.getValues();
40 | console.log(values);
41 | };
42 |
43 | addItem(index) {
44 | ++this.idx;
45 | this.field.addArrayValue('name', index, {
46 | id: this.idx,
47 | input: this.idx,
48 | custom: this.idx,
49 | });
50 | console.log(this.field.getNames());
51 | }
52 |
53 | removeItem(index) {
54 | this.field.deleteArrayValue('name', index);
55 | console.log(this.field.getNames());
56 | }
57 |
58 | input = (value, index) => (
59 |
60 | );
61 | customInput = (value, index) => (
62 |
63 | );
64 | op = (value, index) => {
65 | return (
66 |
67 |
71 | add
72 |
73 |
78 | delete
79 |
80 |
81 | );
82 | };
83 |
84 | render() {
85 | const dataSource = this.field.getValue('name');
86 | return (
87 |
88 |
89 |
90 |
95 |
100 |
105 |
106 |
107 | {JSON.stringify(dataSource, null, 2)}
108 |
109 |
110 | );
111 | }
112 | }
113 |
114 | ReactDOM.render( , mountNode);
115 | ```
116 |
--------------------------------------------------------------------------------
/demo/mix.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 组合使用 - mix usage
3 | order: 8
4 | ---
5 |
6 | 多组件混合使用
7 |
8 | ---
9 |
10 | multi type of component
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import {
16 | Button,
17 | Checkbox,
18 | Input,
19 | Radio,
20 | Select,
21 | Range,
22 | DatePicker,
23 | TimePicker,
24 | } from '@alifd/next';
25 | import Field from '@alifd/field';
26 |
27 | const CheckboxGroup = Checkbox.Group;
28 | const RadioGroup = Radio.Group;
29 |
30 | const list = [
31 | {
32 | value: 'apple',
33 | label: 'apple',
34 | },
35 | {
36 | value: 'pear',
37 | label: 'pear',
38 | },
39 | {
40 | value: 'orange',
41 | label: 'orange',
42 | },
43 | ];
44 | const layout = {
45 | marginBottom: 10,
46 | width: 400,
47 | };
48 |
49 | class App extends React.Component {
50 | field = new Field(this);
51 |
52 | render() {
53 | const { init, getValue } = this.field;
54 |
55 | return (
56 |
57 |
58 |
59 | A
60 | B
61 | C
62 | D
63 |
64 |
65 |
66 | {getValue('radiogroup') !== 'd' ? (
67 |
71 | jack
72 | lucy
73 |
74 | disabled
75 |
76 | hugohua
77 |
78 | ) : (
79 |
80 | )}
81 |
82 |
83 |
90 |
91 |
92 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
{
109 | console.log(this.field.getValues());
110 | }}
111 | >
112 | getValues
113 |
114 |
{
116 | this.field.setValues({
117 | name: 'hugohua',
118 | range: [30, 50],
119 | checkboxgroup: ['orange'],
120 | radiogroup: 'd',
121 | });
122 | }}
123 | >
124 | setValues
125 |
126 |
{
128 | this.field.reset();
129 | }}
130 | >
131 | reset
132 |
133 |
134 | );
135 | }
136 | }
137 |
138 | ReactDOM.render( , mountNode);
139 | ```
140 |
141 | ```css
142 | .demo .next-btn {
143 | margin-right: 5px;
144 | }
145 | ```
146 |
--------------------------------------------------------------------------------
/demo/normalize.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自定义返回值 - custom value
3 | order: 2
4 | ---
5 |
6 | 当组件返回的数据和最终期望提交的格式不一致的时候,可以使用 `getValueFormatter` 和 `setValueFormatter` 两个函数做转换。
7 |
8 | 比如 switch 组件期望上报 0/1, date-picker 组件期望上报 YYYY-MM-DD 这种字符串格式
9 |
10 | ---
11 |
12 | custom get `value` by api `getValueFormatter`
13 | custom set `value` by api `setValueFormatter`
14 |
15 | ```jsx
16 | import ReactDOM from 'react-dom';
17 | import React from 'react';
18 | import { Button, DatePicker, Switch } from '@alifd/next';
19 | import Field from '@alifd/field';
20 | import moment from 'moment';
21 |
22 | class App extends React.Component {
23 | field = new Field(this);
24 |
25 | render() {
26 | const init = this.field.init;
27 |
28 | return (
29 |
30 | {
33 | return value === true ? 1 : 0;
34 | },
35 | setValueFormatter: (value, inputValues) => {
36 | return value === 1 ? true : false;
37 | },
38 | })}
39 | />
40 |
41 |
42 | {
45 | return value.format('YYYY-MM-DD');
46 | },
47 | setValueFormatter: (value, inputValues) => {
48 | return moment(value, 'YYYY-MM-DD');
49 | },
50 | })}
51 | />
52 |
53 |
54 | {
57 | console.log(this.field.getValues());
58 | }}
59 | >
60 | getValues
61 |
62 |
63 | );
64 | }
65 | }
66 |
67 | ReactDOM.render( , mountNode);
68 | ```
69 |
--------------------------------------------------------------------------------
/demo/onchange.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 关联控制 - controlled
3 | order: 1
4 | ---
5 |
6 | 组件之间的关联控制. `onChange` 统一管理。
7 |
8 | ---
9 |
10 | manage value by `onChange`
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Input, Select, Range } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class App extends React.Component {
19 | field = new Field(this, {
20 | onChange: (name, value) => {
21 | console.log(this.field.getValues());
22 |
23 | switch (name) {
24 | case 'input':
25 | this.field.setValue('sync', `change to: ${value}`);
26 | break;
27 | case 'select':
28 | this.field.setValue('sync', `${value} is coming`);
29 | break;
30 | case 'range':
31 | this.field.setValue('sync', ` (${value.join(',')}) ready`);
32 | break;
33 | }
34 | },
35 | });
36 |
37 | render() {
38 | const { init, getValue } = this.field;
39 | const layout = {
40 | marginBottom: 10,
41 | width: 400,
42 | };
43 |
44 | return (
45 |
46 |
51 |
52 |
57 |
58 |
59 |
63 | jack
64 | lucy
65 |
66 | disabled
67 |
68 | hugo
69 |
70 |
71 |
72 | {getValue('select') !== 'hugo' ? (
73 |
83 | ) : null}
84 |
85 |
86 |
87 |
92 |
93 |
94 | );
95 | }
96 | }
97 |
98 | ReactDOM.render( , mountNode);
99 | ```
100 |
--------------------------------------------------------------------------------
/demo/redux.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: redux 中使用 - with redux
3 | order: 5
4 | ---
5 |
6 | 在 redux 中使用, 在 `componentWillReceiveProps` 更新
7 |
8 | ---
9 |
10 | set value in `componentWillReceiveProps`
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Input, Button } from '@alifd/next';
16 | import Field from '@alifd/field';
17 | import { combineReducers, createStore } from 'redux';
18 | import { Provider, connect } from 'react-redux';
19 |
20 | function formReducer(state = { email: 'frankqian@qq.com' }, action) {
21 | switch (action.type) {
22 | case 'save_fields':
23 | return {
24 | ...state,
25 | ...action.payload,
26 | };
27 | default:
28 | return state;
29 | }
30 | }
31 |
32 | class Demo extends React.Component {
33 | componentWillReceiveProps(nextProps) {
34 | this.field.setValues({
35 | email: nextProps.email,
36 | newlen: nextProps.email.length,
37 | });
38 | }
39 |
40 | field = new Field(this, {
41 | onChange: (name, value) => {
42 | console.log('onChange', name, value);
43 | this.field.setValue('newlen', value.length);
44 | this.props.dispatch({
45 | type: 'save_fields',
46 | payload: {
47 | [name]: value,
48 | },
49 | });
50 | },
51 | });
52 |
53 | setEmail() {
54 | this.props.dispatch({
55 | type: 'save_fields',
56 | payload: {
57 | email: 'qq@gmail.com',
58 | },
59 | });
60 | }
61 |
62 | render() {
63 | const init = this.field.init;
64 |
65 | const newLen = init('newlen', { initValue: this.props.email.length });
66 |
67 | return (
68 |
69 |
84 | now length is:{newLen.value}
85 |
email: {this.props.email}
86 |
set
87 |
88 | );
89 | }
90 | }
91 |
92 | const ReduxDemo = connect((state) => {
93 | return {
94 | email: state.formReducer.email,
95 | };
96 | })(Demo);
97 |
98 | const store = createStore(
99 | combineReducers({
100 | formReducer,
101 | })
102 | );
103 |
104 | ReactDOM.render(
105 |
106 |
107 |
108 |
109 | ,
110 | mountNode
111 | );
112 | ```
113 |
--------------------------------------------------------------------------------
/demo/same-name.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自动卸载同名组件 - same name
3 | order: 6
4 | debug: true
5 | ---
6 |
7 | 2 个组件相同 name,删除其中一个的时候数据要保留.
8 |
9 | ---
10 |
11 | 2 Component with same name, while delete one should keep the data.
12 |
13 | ```jsx
14 | import ReactDOM from 'react-dom';
15 | import React from 'react';
16 | import { Input, Button } from '@alifd/next';
17 | import Field from '@alifd/field';
18 |
19 | class Demo extends React.Component {
20 | state = {
21 | show: true,
22 | show2: true,
23 | };
24 | field = new Field(this);
25 |
26 | render() {
27 | return (
28 |
29 | {this.state.show ? (
30 |
33 | ) : null}
34 | this.setState({ show: !this.state.show })}
36 | warning
37 | style={{ marginLeft: 4 }}
38 | >
39 | delete
40 |
41 |
42 |
43 | {this.state.show2 ? (
44 |
47 | ) : null}
48 | this.setState({ show2: !this.state.show2 })}
50 | warning
51 | style={{ marginLeft: 4 }}
52 | >
53 | delete
54 |
55 |
56 |
57 |
58 | {
60 | console.log(
61 | 'value always exist',
62 | this.field.getValues()
63 | );
64 | }}
65 | >
66 | print
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | ReactDOM.render( , mountNode);
74 | ```
75 |
--------------------------------------------------------------------------------
/demo/seterror.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自定义错误 - custom errors
3 | order: 3
4 | ---
5 |
6 | 自己控制组件的errors
7 |
8 | ---
9 |
10 | set errors of component by yourself
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Input, Button } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class App extends React.Component {
19 | field = new Field(this);
20 |
21 | validate = () => {
22 | console.log(this.field.getErrors());
23 | this.field.validateCallback((error, values) => {
24 | alert(JSON.stringify(error));
25 | });
26 | };
27 |
28 | render() {
29 | const { init, getError, setError, setErrors } = this.field;
30 | return (
31 |
32 |
43 |
44 | {getError('input')}
45 |
46 |
47 | {
49 | setError('input', 'set error 1');
50 | }}
51 | >
52 | setError
53 |
54 |
55 | {
57 | setErrors({ input: 'set error 2' });
58 | }}
59 | >
60 | setErrors
61 |
62 |
63 | {
65 | setErrors({ input: '' });
66 | }}
67 | >
68 | clear
69 |
70 |
71 |
72 |
73 |
74 |
75 | {getError('input2')}
76 |
77 |
78 | {
80 | setError(
81 | 'input2',
82 | 'errors will be removed by onChange and shown on validate'
83 | );
84 | }}
85 | >
86 | setError
87 |
88 |
89 | validate
90 |
91 | );
92 | }
93 | }
94 |
95 | ReactDOM.render( , mountNode);
96 | ```
97 |
98 | ```css
99 | .demo .next-btn {
100 | margin-right: 5px;
101 | }
102 | ```
103 |
--------------------------------------------------------------------------------
/demo/topath.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 结构化解析 - Parse Array or Object
3 | order: 11
4 | ---
5 |
6 | 把 `init('obj.b')` 的数据转换成 `obj={obj:{b:'value'}}`;
7 |
8 | 把 `init('arr.0')` 的数据转换成 `obj={arr:['']}`;
9 |
10 | ---
11 |
12 | from `init('obj.b')` to `obj={obj:{b:'value'}}`;
13 |
14 | from `init('arr.0')` to `obj={arr:['']}`;
15 |
16 | ```jsx
17 | import ReactDOM from 'react-dom';
18 | import React from 'react';
19 | import { Input, Button } from '@alifd/next';
20 | import Field from '@alifd/field';
21 |
22 | class App extends React.Component {
23 | field = new Field(this, {
24 | parseName: true,
25 | values: {
26 | objWithDefaults: {
27 | a: 1,
28 | b: 2,
29 | },
30 | },
31 | });
32 |
33 | onGetValue() {
34 | console.log(this.field.getValues());
35 | }
36 |
37 | onSetValue() {
38 | this.field.setValues({
39 | obj: {
40 | b: 'b',
41 | c: 'c',
42 | },
43 | arr: ['first', 'second'],
44 | objWithDefaults: {
45 | a: 100,
46 | b: 200,
47 | },
48 | });
49 | }
50 |
51 | render() {
52 | const { init, reset, resetToDefault } = this.field;
53 |
54 | return (
55 |
56 |
Object transfer
57 | obj.b:
58 | obj.c:
59 |
60 |
Array transfer
61 | arr.0:
62 | arr.1:
63 |
64 |
65 |
Object with Defaults
66 | objWithDefaults.a:
{' '}
67 | objWithDefaults.b:{' '}
68 |
69 |
70 |
71 | result:
72 |
{JSON.stringify(this.field.getValues(), null, 2)}
73 |
74 |
75 |
76 | getValues
77 |
78 |
setValues
79 |
reset()}>reset
80 |
resetToDefault()}>resetToDefault
81 |
82 | );
83 | }
84 | }
85 |
86 | ReactDOM.render( , mountNode);
87 | ```
88 |
89 | ```css
90 | .demo .next-btn {
91 | margin-right: 5px;
92 | }
93 | ```
94 |
--------------------------------------------------------------------------------
/demo/useField.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Hooks
3 | order: 12
4 | ---
5 |
6 | `getUseField` requires `useState` and `useMemo` implementation from React or Rax. Extend the `Field` Component and add a `useField` static method.
7 |
8 | `static useField(...args) {
9 | return this.getUseField(useState)(...args);
10 | }`
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React, { useState, useMemo } from 'react';
15 | import { Input, Button } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class myField extends Field {
19 | static useField(...args) {
20 | return this.getUseField({ useState, useMemo })(...args);
21 | }
22 | }
23 |
24 | function NewApp() {
25 | const field = myField.useField();
26 |
27 | const { init, setValue, reset } = field;
28 |
29 | function onGetValue() {
30 | console.log(field.getValue('input'));
31 | }
32 |
33 | function onSetValue() {
34 | field.setValue('input', 'xyz');
35 | }
36 |
37 | return (
38 |
39 |
40 | setValue
41 | getValue
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | ReactDOM.render( , mountNode);
49 | ```
50 |
--------------------------------------------------------------------------------
/demo/validator.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 校验 - validate
3 | order: 4
4 | ---
5 |
6 | 校验的错误信息需要用`getError`获取
7 |
8 | `注意`:Form 和 Field 做了深度结合,在 Form 中使用Field,错误信息不需`getError`获取会自动展现。
9 |
10 | ---
11 |
12 | you can easily use validate in `Form`, or you can use `getError` to set errors where you want to put
13 |
14 | ```jsx
15 | import ReactDOM from 'react-dom';
16 | import React from 'react';
17 | import { Input, Button, Checkbox } from '@alifd/next';
18 | import Field from '@alifd/field';
19 |
20 | const CheckboxGroup = Checkbox.Group;
21 |
22 | const list = [
23 | {
24 | value: 'apple',
25 | label: 'apple',
26 | },
27 | {
28 | value: 'pear',
29 | label: 'pear',
30 | },
31 | {
32 | value: 'orange',
33 | label: 'orange',
34 | },
35 | ];
36 |
37 | class App extends React.Component {
38 | state = {
39 | checkboxStatus: true,
40 | };
41 | field = new Field(this);
42 |
43 | isChecked(rule, value, callback) {
44 | if (!value) {
45 | return callback('consent agreement not checked ');
46 | } else {
47 | return callback();
48 | }
49 | }
50 |
51 | userName(rule, value, callback) {
52 | if (value === 'frank') {
53 | setTimeout(() => callback('name existed'), 200);
54 | } else {
55 | setTimeout(() => callback(), 200);
56 | }
57 | }
58 |
59 | render() {
60 | const init = this.field.init;
61 |
62 | return (
63 |
64 |
70 | {this.field.getError('input') ? (
71 |
72 | {this.field.getError('input').join(',')}
73 |
74 | ) : (
75 | ''
76 | )}
77 |
78 |
79 |
91 | {this.field.getError('input1') ? (
92 |
93 | {this.field.getError('input1').join(',')}
94 |
95 | ) : (
96 | ''
97 | )}
98 |
99 |
100 |
112 | {this.field.getState('username') === 'loading'
113 | ? 'validating...'
114 | : ''}
115 | {this.field.getError('username') ? (
116 |
117 | {this.field.getError('username').join(',')}
118 |
119 | ) : (
120 | ''
121 | )}
122 |
123 |
124 | agreement:
125 |
131 | {this.field.getError('checkbox') ? (
132 |
133 | {this.field.getError('checkbox').join(',')}
134 |
135 | ) : (
136 | ''
137 | )}
138 |
139 |
140 |
152 | {this.field.getError('textarea') ? (
153 |
154 | {this.field.getError('textarea').join(',')}
155 |
156 | ) : (
157 | ''
158 | )}
159 |
160 |
161 | {this.state.checkboxStatus ? (
162 |
163 | Array validate:
164 |
177 | {this.field.getError('checkboxgroup') ? (
178 |
179 | {this.field.getError('checkboxgroup').join(',')}
180 |
181 | ) : (
182 | ''
183 | )}
184 |
185 | ) : null}
186 |
187 |
188 |
{
191 | this.field.validateCallback((errors, values) => {
192 | console.log(errors, values);
193 | });
194 | }}
195 | >
196 | validate
197 |
198 |
{
200 | this.field.reset();
201 | }}
202 | >
203 | reset
204 |
205 |
{
207 | if (this.state.checkboxStatus) {
208 | this.setState({ checkboxStatus: false });
209 | this.field.remove('checkboxgroup');
210 | } else {
211 | this.setState({ checkboxStatus: true });
212 | }
213 | }}
214 | >
215 | {this.state.checkboxStatus ? 'delete' : 'restore'}
216 |
217 |
218 | );
219 | }
220 | }
221 |
222 | ReactDOM.render( , mountNode);
223 | ```
224 |
225 | ```css
226 | .demo .next-btn {
227 | margin-right: 5px;
228 | }
229 | ```
230 |
--------------------------------------------------------------------------------
/demo/validatorPromise.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 校验 - validatePromise
3 | order: 4
4 | ---
5 |
6 | 校验的错误信息需要用`getError`获取
7 |
8 | `注意`:Form 和 Field 做了深度结合,在 Form 中使用Field,错误信息不需`getError`获取会自动展现。
9 |
10 | ---
11 |
12 | you can easily use validate in `Form`, or you can use `getError` to set errors where you want to put
13 |
14 | ```jsx
15 | import ReactDOM from 'react-dom';
16 | import React from 'react';
17 | import { Input, Button, Checkbox } from '@alifd/next';
18 | import Field from '@alifd/field';
19 |
20 | const CheckboxGroup = Checkbox.Group;
21 |
22 | const list = [
23 | {
24 | value: 'apple',
25 | label: 'apple',
26 | },
27 | {
28 | value: 'pear',
29 | label: 'pear',
30 | },
31 | {
32 | value: 'orange',
33 | label: 'orange',
34 | },
35 | ];
36 |
37 | class App extends React.Component {
38 | state = {
39 | checkboxStatus: true,
40 | };
41 | field = new Field(this);
42 |
43 | isChecked(rule, value) {
44 | if (!value) {
45 | return Promise.reject('consent agreement not checked ');
46 | } else {
47 | return Promise.resolve(null);
48 | }
49 | }
50 |
51 | userName(rule, value) {
52 | if (value === 'frank') {
53 | return new Promise((resolve, reject) => {
54 | setTimeout(() => reject('name existed'), 200);
55 | });
56 | } else {
57 | return new Promise((resolve, reject) => {
58 | setTimeout(() => reject(), 200);
59 | });
60 | }
61 | }
62 |
63 | render() {
64 | const init = this.field.init;
65 |
66 | return (
67 |
68 |
74 | {this.field.getError('input') ? (
75 |
76 | {this.field.getError('input').join(',')}
77 |
78 | ) : (
79 | ''
80 | )}
81 |
82 |
83 |
95 | {this.field.getError('input1') ? (
96 |
97 | {this.field.getError('input1').join(',')}
98 |
99 | ) : (
100 | ''
101 | )}
102 |
103 |
104 |
116 | {this.field.getState('username') === 'loading'
117 | ? 'validating...'
118 | : ''}
119 | {this.field.getError('username') ? (
120 |
121 | {this.field.getError('username').join(',')}
122 |
123 | ) : (
124 | ''
125 | )}
126 |
127 |
128 | agreement:
129 |
135 | {this.field.getError('checkbox') ? (
136 |
137 | {this.field.getError('checkbox').join(',')}
138 |
139 | ) : (
140 | ''
141 | )}
142 |
143 |
144 |
156 | {this.field.getError('textarea') ? (
157 |
158 | {this.field.getError('textarea').join(',')}
159 |
160 | ) : (
161 | ''
162 | )}
163 |
164 |
165 | {this.state.checkboxStatus ? (
166 |
167 | Array validate:
168 |
181 | {this.field.getError('checkboxgroup') ? (
182 |
183 | {this.field.getError('checkboxgroup').join(',')}
184 |
185 | ) : (
186 | ''
187 | )}
188 |
189 | ) : null}
190 |
191 |
192 |
{
195 | this.field
196 | .validatePromise()
197 | .then(({ errors, values }) => {
198 | console.log(errors, values);
199 | });
200 | }}
201 | >
202 | validate
203 |
204 |
{
206 | this.field.reset();
207 | }}
208 | >
209 | reset
210 |
211 |
{
213 | if (this.state.checkboxStatus) {
214 | this.setState({ checkboxStatus: false });
215 | this.field.remove('checkboxgroup');
216 | } else {
217 | this.setState({ checkboxStatus: true });
218 | }
219 | }}
220 | >
221 | {this.state.checkboxStatus ? 'delete' : 'restore'}
222 |
223 |
224 | );
225 | }
226 | }
227 |
228 | ReactDOM.render( , mountNode);
229 | ```
230 |
231 | ```css
232 | .demo .next-btn {
233 | margin-right: 5px;
234 | }
235 | ```
236 |
--------------------------------------------------------------------------------
/demo/valuename.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 自定义受控字段 - custom valueName
3 | order: 9
4 | ---
5 |
6 | valueName 的默认值为 value,如果为其他需要用 valueName 指定
7 |
8 | ---
9 |
10 | default valueName is `value`
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Button, Checkbox, Radio, Switch } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class App extends React.Component {
19 | field = new Field(this);
20 |
21 | render() {
22 | const init = this.field.init;
23 |
24 | return (
25 |
26 |
32 | {' '}
33 | checked
34 |
35 |
36 |
42 | defaultChecked
43 |
44 |
45 |
52 |
53 |
54 | {
57 | console.log(this.field.getValues());
58 | }}
59 | >
60 | getValues
61 |
62 | {
64 | this.field.setValues({
65 | radio: true,
66 | switch: true,
67 | checkbox: false,
68 | });
69 | }}
70 | >
71 | {' '}
72 | setValues{' '}
73 |
74 | {
76 | this.field.reset();
77 | }}
78 | >
79 | reset
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | ReactDOM.render( , mountNode);
87 | ```
88 |
89 | ```css
90 | .demo .next-btn {
91 | margin-right: 5px;
92 | }
93 | ```
94 |
--------------------------------------------------------------------------------
/demo/watch.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 监听字段值变化 - watch value change
3 | order: 13
4 | ---
5 |
6 | 使用 `field.watch` 方法来监听字段值的变化
7 |
8 | ---
9 |
10 | Use `field.watch` to detect changes of the field value
11 |
12 | ```jsx
13 | import ReactDOM from 'react-dom';
14 | import React from 'react';
15 | import { Button, Input, Switch } from '@alifd/next';
16 | import Field from '@alifd/field';
17 |
18 | class App extends React.Component {
19 | constructor(props) {
20 | super(props);
21 | this.field = new Field(this);
22 | this.state = {
23 | showInput: true,
24 | };
25 | this.field.watch(
26 | ['radio', 'input', 'switch'],
27 | (name, value, oldValue, triggerType) => {
28 | // console.log('[Detect change]', name, value, oldValue, triggerType);
29 | console.group('[Detect Change]');
30 | console.log('name:', name);
31 | console.log('value:', oldValue, ' -> ', value);
32 | console.log('triggerType:', triggerType);
33 | console.groupEnd('[Detect Change]');
34 |
35 | // 监听switch变化,联动控制input显隐
36 | if (name === 'switch') {
37 | this.setState({
38 | showInput: value,
39 | });
40 | }
41 | }
42 | );
43 | }
44 |
45 | render() {
46 | const init = this.field.init;
47 | const { showInput } = this.state;
48 |
49 | return (
50 |
51 |
58 |
59 | {showInput && (
60 |
61 | )}
62 |
63 |
64 |
65 | {
68 | console.log(this.field.getValues());
69 | }}
70 | >
71 | getValues
72 |
73 | {
75 | this.field.setValues({
76 | switch: true,
77 | input: '123',
78 | });
79 | }}
80 | >
81 | setValues
82 |
83 | {
85 | this.field.reset();
86 | }}
87 | >
88 | reset
89 |
90 |
91 | );
92 | }
93 | }
94 |
95 | ReactDOM.render( , mountNode);
96 | ```
97 |
98 | ```css
99 | .demo .next-btn {
100 | margin-right: 5px;
101 | }
102 | ```
103 |
--------------------------------------------------------------------------------
/ice.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | injectBabel: 'runtime',
3 | publicPath: './',
4 | plugins: [
5 | ['ice-plugin-fusion', {}],
6 | 'ice-plugin-component',
7 | ],
8 | chainWebpack: (config, { command }) => {
9 | // 内置 jsx 和 tsx 规则均会使用到 babel 配置
10 | ['jsx', 'tsx'].forEach((rule) => {
11 | config.module
12 | .rule(rule)
13 | .use('babel-loader')
14 | .tap((options) => {
15 | // 添加一条 babel plugin,同理可添加 presets
16 | options.plugins.push(require.resolve('babel-plugin-transform-jsx-list'));
17 | options.plugins.push(require.resolve('babel-plugin-transform-react-es6-displayname'));
18 | options.plugins.push(require.resolve('babel-plugin-transform-object-assign'));
19 | options.plugins.push(require.resolve('babel-plugin-transform-proto-to-assign'));
20 |
21 | return options;
22 | });
23 | });
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alifd/field",
3 | "version": "2.0.4",
4 | "description": "Fields can be used to manage data when it comes to form data manipulation and validation. After being associated with a component, the form data can be automatically written back, read, and verified.",
5 | "files": [
6 | "demo/",
7 | "es/",
8 | "lib/",
9 | "build/"
10 | ],
11 | "main": "lib/index.js",
12 | "module": "es/index.js",
13 | "stylePath": "style.js",
14 | "scripts": {
15 | "start": "build-scripts dev",
16 | "build": "npm run build:demo && npm run build:es && npm run build:lib",
17 | "build:demo": "build-scripts build",
18 | "build:es": "rm -rf es && tsc -p ./tsconfig.build.json --outDir es --module esnext",
19 | "build:lib": "rm -rf lib && tsc -p ./tsconfig.build.json --outDir lib --module commonjs",
20 | "prepublishOnly": "npm run build",
21 | "test": "cypress run --component -b chrome",
22 | "test:head": "cypress open --component -b chrome",
23 | "precommit": "lint-staged"
24 | },
25 | "lint-staged": {
26 | "@(src|test)/**/*.@(ts|tsx)": [
27 | "eslint"
28 | ],
29 | "**/*.@(js|ts|tsx|json)": [
30 | "prettier --write",
31 | "git add"
32 | ]
33 | },
34 | "husky": {
35 | "hooks": {
36 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
37 | }
38 | },
39 | "license": "MIT",
40 | "keywords": [
41 | "react",
42 | "component"
43 | ],
44 | "dependencies": {
45 | "@alifd/validate": "^2.0.2",
46 | "tslib": "^2.6.2"
47 | },
48 | "devDependencies": {
49 | "@alib/build-scripts": "^0.1.3",
50 | "@alifd/eslint-config-next": "^2.0.0",
51 | "@alifd/next": "^1.15.12",
52 | "@commitlint/cli": "^8.1.0",
53 | "@types/chai": "^4.3.11",
54 | "@types/react": "^16.14.56",
55 | "@types/react-dom": "^16.9.24",
56 | "@typescript-eslint/eslint-plugin": "^6.13.2",
57 | "@typescript-eslint/parser": "^6.13.2",
58 | "@vitejs/plugin-react": "^4.2.1",
59 | "build-plugin-component": "^1.0.0",
60 | "build-plugin-fusion": "^0.1.0",
61 | "build-plugin-moment-locales": "^0.1.0",
62 | "chai": "^4.4.1",
63 | "cypress": "^13.6.4",
64 | "enzyme": "^3.10.0",
65 | "enzyme-adapter-react-16": "^1.14.0",
66 | "es6-promise-polyfill": "^1.2.0",
67 | "eslint": "^8.56.0",
68 | "eslint-config-prettier": "^9.1.0",
69 | "eslint-plugin-cypress": "^2.15.1",
70 | "eslint-plugin-import": "^2.29.1",
71 | "eslint-plugin-markdown": "^3.0.1",
72 | "eslint-plugin-react": "^7.14.2",
73 | "eslint-plugin-tsdoc": "^0.2.17",
74 | "husky": "^3.0.0",
75 | "lint-staged": "^9.2.0",
76 | "mocha": "^6.1.4",
77 | "moment": "^2.24.0",
78 | "prettier": "^3.2.4",
79 | "prop-types": "^15.8.1",
80 | "react": "^16.3.0",
81 | "react-dom": "^16.8.6",
82 | "react-redux": "^7.1.0",
83 | "redux": "^4.0.4",
84 | "semantic-release": "^17.2.3",
85 | "sinon": "^16.1.3",
86 | "typescript": "^4.9.5",
87 | "vite": "^4.5.2"
88 | },
89 | "componentConfig": {
90 | "name": "field",
91 | "title": "Field",
92 | "categories": [
93 | "表单"
94 | ]
95 | },
96 | "homepage": "https://unpkg.com/@alifd/field@2.0.4/build/index.html",
97 | "bugs": "https://github.com/alibaba-fusion/field/issues",
98 | "publishConfig": {
99 | "access": "public",
100 | "registry": "https://registry.npmjs.org"
101 | },
102 | "repository": {
103 | "type": "git",
104 | "url": "https://github.com/alibaba-fusion/field.git"
105 | },
106 | "release": {
107 | "plugins": [
108 | [
109 | "@semantic-release/commit-analyzer",
110 | {
111 | "releaseRules": [
112 | {
113 | "type": "typescript",
114 | "release": "patch"
115 | },
116 | {
117 | "type": "revert",
118 | "release": "patch"
119 | }
120 | ],
121 | "parserOpts": {
122 | "noteKeywords": [
123 | "BREAKING CHANGE",
124 | "BREAKING CHANGES"
125 | ]
126 | }
127 | }
128 | ],
129 | "@semantic-release/release-notes-generator",
130 | "@semantic-release/npm",
131 | "@semantic-release/github"
132 | ]
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Component, MutableRefObject } from 'react';
2 | import Validate, { type NormalizedValidateError } from '@alifd/validate';
3 | import {
4 | getValueFromEvent,
5 | getErrorStrs,
6 | getParams,
7 | hasIn,
8 | setIn,
9 | getIn,
10 | deleteIn,
11 | mapValidateRules,
12 | warning,
13 | cloneToRuleArr,
14 | isOverwritten,
15 | } from './utils';
16 | import type {
17 | FieldOption,
18 | ComponentInstance,
19 | GetUseFieldOption,
20 | FieldMeta,
21 | RerenderFunction,
22 | WatchCallback,
23 | NormalizedFieldOption,
24 | InitOption,
25 | FieldValues,
26 | NormalizedFieldMeta,
27 | Rule,
28 | FieldState,
29 | ValidateCallback,
30 | ValidateResultFormatter,
31 | ValidateErrorGroup,
32 | RerenderType,
33 | WatchTriggerType,
34 | SetState,
35 | ValidatePromiseResults,
36 | InitResult,
37 | } from './types';
38 |
39 | const initMeta = {
40 | state: '' as const,
41 | valueName: 'value',
42 | trigger: 'onChange',
43 | inputValues: [],
44 | };
45 |
46 | class Field {
47 | static create(com: ComponentInstance, options: FieldOption = {}) {
48 | return new this(com, options);
49 | }
50 |
51 | static getUseField({ useState, useMemo }: GetUseFieldOption) {
52 | return (options: FieldOption = {}) => {
53 | const [, setState] = useState();
54 |
55 | const field = useMemo(() => this.create({ setState }, options), [setState]);
56 |
57 | return field;
58 | };
59 | }
60 |
61 | fieldsMeta: Record;
62 | cachedBind: Record>;
63 | instance: Record;
64 | instanceCount: Record;
65 | reRenders: Record;
66 | listeners: Record>;
67 | values: FieldValues;
68 | processErrorMessage?: FieldOption['processErrorMessage'];
69 | afterValidateRerender?: FieldOption['afterValidateRerender'];
70 | com: ComponentInstance;
71 | options: NormalizedFieldOption;
72 |
73 | constructor(com: ComponentInstance, options: FieldOption = {}) {
74 | if (!com) {
75 | warning('`this` is missing in `Field`, you should use like `new Field(this)`');
76 | }
77 |
78 | this.com = com;
79 | this.fieldsMeta = {};
80 | this.cachedBind = {};
81 | this.instance = {};
82 | this.instanceCount = {};
83 | this.reRenders = {};
84 | this.listeners = {};
85 | // holds constructor values. Used for setting field defaults on init if no other value or initValue is passed.
86 | // Also used caching values when using `parseName: true` before a field is initialized
87 | this.values = Object.assign({}, options.values);
88 |
89 | this.processErrorMessage = options.processErrorMessage;
90 | this.afterValidateRerender = options.afterValidateRerender;
91 |
92 | this.options = Object.assign(
93 | {
94 | parseName: false,
95 | forceUpdate: false,
96 | first: false,
97 | onChange: () => {},
98 | autoUnmount: true,
99 | autoValidate: true,
100 | },
101 | options
102 | );
103 |
104 | (
105 | [
106 | 'init',
107 | 'getValue',
108 | 'getValues',
109 | 'setValue',
110 | 'setValues',
111 | 'getError',
112 | 'getErrors',
113 | 'setError',
114 | 'setErrors',
115 | 'validateCallback',
116 | 'validatePromise',
117 | 'getState',
118 | 'reset',
119 | 'resetToDefault',
120 | 'remove',
121 | 'spliceArray',
122 | 'addArrayValue',
123 | 'deleteArrayValue',
124 | 'getNames',
125 | ] as const
126 | ).forEach((m) => {
127 | this[m] = this[m].bind(this);
128 | });
129 | }
130 |
131 | /**
132 | * 设置配置信息
133 | * @param options - 配置
134 | */
135 | setOptions(options: Partial) {
136 | Object.assign(this.options, options);
137 | }
138 |
139 | /**
140 | * 初始化一个字段项
141 | * @param name - 字段 key
142 | * @param option - 字段配置
143 | * @param rprops - 其它参数
144 | */
145 | init<
146 | ValueType = any,
147 | ValueName extends string = 'value',
148 | Trigger extends string = 'onChange',
149 | OtherProps extends object = object,
150 | >(name: string, option: InitOption = {}, rprops?: OtherProps) {
151 | const {
152 | id,
153 | initValue,
154 | valueName = 'value',
155 | trigger = 'onChange',
156 | rules = [],
157 | props = {},
158 | getValueFromEvent = null,
159 | getValueFormatter = getValueFromEvent,
160 | setValueFormatter,
161 | autoValidate = true,
162 | reRender,
163 | } = option;
164 | const { parseName } = this.options;
165 |
166 | if (getValueFromEvent) {
167 | warning('`getValueFromEvent` has been deprecated in `Field`, use `getValueFormatter` instead of it');
168 | }
169 |
170 | const originalProps = Object.assign({}, props, rprops) as Record;
171 | const defaultValueName = `default${valueName[0].toUpperCase()}${valueName.slice(1)}`;
172 | let defaultValue;
173 | if (typeof initValue !== 'undefined') {
174 | defaultValue = initValue;
175 | } else if (typeof originalProps[defaultValueName] !== 'undefined') {
176 | // here use typeof, in case of defaultValue={0}
177 | defaultValue = originalProps[defaultValueName];
178 | }
179 |
180 | // get field from this.fieldsMeta or new one
181 | const field = this._getInitMeta(name) as NormalizedFieldMeta;
182 | Object.assign(field, {
183 | valueName,
184 | initValue: defaultValue,
185 | disabled: 'disabled' in originalProps ? originalProps.disabled : false,
186 | getValueFormatter,
187 | setValueFormatter,
188 | rules: cloneToRuleArr(rules),
189 | ref: originalProps.ref,
190 | });
191 |
192 | let oldValue = field.value;
193 |
194 | // Controlled Component, should always equal props.value
195 | if (valueName in originalProps) {
196 | const originalValue = originalProps[valueName];
197 |
198 | // When rerendering set the values from props.value
199 | if (parseName) {
200 | // when parseName is true, field should not store value locally. To prevent sync issues
201 | if (!('value' in field)) {
202 | this._proxyFieldValue(field);
203 | }
204 | } else {
205 | this.values[name] = originalValue;
206 | }
207 | field.value = originalValue;
208 | }
209 |
210 | /**
211 | * first init field (value not in field)
212 | * should get field.value from this.values or defaultValue
213 | */
214 | if (!('value' in field)) {
215 | if (parseName) {
216 | const cachedValue = getIn(this.values, name);
217 | if (typeof cachedValue !== 'undefined') {
218 | oldValue = cachedValue;
219 | }
220 | const initValue = typeof cachedValue !== 'undefined' ? cachedValue : defaultValue;
221 | // when parseName is true, field should not store value locally. To prevent sync issues
222 | this._proxyFieldValue(field);
223 | field.value = initValue;
224 | } else {
225 | const cachedValue = this.values[name];
226 | if (typeof cachedValue !== 'undefined') {
227 | field.value = cachedValue;
228 | oldValue = cachedValue;
229 | } else if (typeof defaultValue !== 'undefined') {
230 | // should be same with parseName, but compatible with old versions
231 | field.value = defaultValue;
232 | this.values[name] = field.value;
233 | }
234 | }
235 | }
236 |
237 | // field value init end
238 | const newValue = field.value;
239 | this._triggerFieldChange(name, newValue, oldValue, 'init');
240 |
241 | // Component props
242 | const inputProps = {
243 | 'data-meta': 'Field',
244 | id: id || name,
245 | ref: this._getCacheBind(name, `${name}__ref`, this._saveRef),
246 | [valueName]: setValueFormatter
247 | ? setValueFormatter(field.value as ValueType, field.inputValues)
248 | : field.value,
249 | };
250 |
251 | let rulesMap: Record = {};
252 |
253 | if (this.options.autoValidate && autoValidate !== false) {
254 | // trigger map in rules,
255 | rulesMap = mapValidateRules(field.rules, trigger);
256 |
257 | // step1 : validate hooks
258 | for (const action in rulesMap) {
259 | // skip default trigger, which will trigger in step2
260 | if (action === trigger) {
261 | continue;
262 | }
263 |
264 | const actionRule = rulesMap[action];
265 | inputProps[action] = (...args: unknown[]) => {
266 | this._callNativePropsEvent(action, originalProps, ...args);
267 | this._validate(name, actionRule, action);
268 | };
269 | }
270 | }
271 |
272 | // step2: onChange(trigger=onChange by default) hack
273 | inputProps[trigger] = (...args: unknown[]) => {
274 | const oldValue = this.getValue(name);
275 | this._updateFieldValue(name, ...args);
276 | const newValue = this.getValue(name);
277 | this._triggerFieldChange(name, newValue, oldValue, 'change');
278 |
279 | // clear validate error
280 | this._resetError(name);
281 |
282 | this._callNativePropsEvent(trigger, originalProps, ...args);
283 | // call global onChange
284 | this.options.onChange(name, field.value);
285 |
286 | // validate while onChange
287 | const rule = rulesMap[trigger];
288 | rule && this._validate(name, rule, trigger);
289 |
290 | this._reRender(name, trigger);
291 | };
292 |
293 | // step3: save reRender function
294 | if (reRender && typeof reRender === 'function') {
295 | this.reRenders[name] = reRender;
296 | }
297 |
298 | delete originalProps[defaultValueName];
299 |
300 | return Object.assign({}, originalProps, inputProps) as OtherProps & InitResult;
301 | }
302 |
303 | /**
304 | * 获取单个输入控件的值
305 | * @param name - 字段名
306 | * @returns 字段值
307 | */
308 | getValue(name: string): T | undefined {
309 | if (this.options.parseName) {
310 | return getIn(this.values, name);
311 | }
312 | return this.values[name] as T;
313 | }
314 |
315 | /**
316 | * 获取一组输入控件的值
317 | * @param names - 字段名数组
318 | * @returns 不传入`names`参数,则获取全部字段的值
319 | */
320 | getValues>(names?: string[]): T {
321 | const allValues: Record = {};
322 |
323 | if (names && names.length) {
324 | names.forEach((name) => {
325 | allValues[name] = this.getValue(name);
326 | });
327 | } else {
328 | Object.assign(allValues, this.values);
329 | }
330 |
331 | return allValues as T;
332 | }
333 |
334 | /**
335 | * 设置单个输入控件的值(默认会触发 render,请遵循 react 时机使用)
336 | * @param name - 字段名
337 | * @param value - 字段值
338 | * @param reRender - 设置完成后是否重新渲染,默认为 true
339 | * @param triggerChange - 是否触发 watch change,默认为 true
340 | */
341 | setValue(name: string, value: T, reRender = true, triggerChange = true) {
342 | const oldValue = this.getValue(name);
343 | if (name in this.fieldsMeta) {
344 | this.fieldsMeta[name].value = value;
345 | }
346 | if (this.options.parseName) {
347 | this.values = setIn(this.values, name, value);
348 | } else {
349 | this.values[name] = value;
350 | }
351 | const newValue = this.getValue(name);
352 | if (triggerChange) {
353 | this._triggerFieldChange(name, newValue, oldValue, 'setValue');
354 | }
355 | reRender && this._reRender(name, 'setValue');
356 | }
357 |
358 | /**
359 | * 设置一组输入控件的值(默认会触发 render,请遵循 react 时机使用)
360 | * @param fieldsValue - 一组输入控件值对象
361 | * @param reRender - 设置完成后是否重新渲染,默认为 true
362 | */
363 | setValues(fieldsValue: Record = {}, reRender = true) {
364 | if (!this.options.parseName) {
365 | Object.keys(fieldsValue).forEach((name) => {
366 | this.setValue(name, fieldsValue[name], false, true);
367 | });
368 | } else {
369 | // NOTE: this is a shallow merge
370 | // Ex. we have two values a.b.c=1 ; a.b.d=2, and use setValues({a:{b:{c:3}}}) , then because of shallow merge a.b.d will be lost, we will get only {a:{b:{c:3}}}
371 | // fieldsMeta[name].value is proxy from this.values[name] when parseName is true, so there is no need to assign value to fieldMeta
372 | // shallow merge
373 | let newValues = Object.assign({}, this.values, fieldsValue);
374 | const fields = this.getNames();
375 | const allOldFieldValues = this.getValues(fields);
376 | // record all old field values, exclude items overwritten by fieldsValue
377 | const oldFieldValues = fields
378 | .filter((name) => !isOverwritten(fieldsValue, name))
379 | .map((name) => ({ name, value: this.fieldsMeta[name].value }));
380 | // assign lost field value to newValues
381 | oldFieldValues.forEach(({ name, value }) => {
382 | if (!hasIn(newValues, name)) {
383 | newValues = setIn(newValues, name, value);
384 | }
385 | });
386 | // store the new values
387 | this.values = newValues;
388 |
389 | // trigger changes after update
390 | for (const name of fields) {
391 | this._triggerFieldChange(name, this.getValue(name), allOldFieldValues[name], 'setValue');
392 | }
393 | }
394 | reRender && this._reRender();
395 | }
396 |
397 | /**
398 | * 获取单个输入控件的 Error
399 | * @param name - 字段名
400 | * @returns 该字段的 Error
401 | */
402 | getError(name: string) {
403 | const field = this._get(name);
404 | if (field && field.errors && field.errors.length) {
405 | return field.errors;
406 | }
407 |
408 | return null;
409 | }
410 |
411 | /**
412 | * 获取一组输入控件的 Error
413 | * @param names - 字段名列表
414 | * @returns 不传入`names`参数,则获取全部字段的 Error
415 | */
416 | getErrors(names?: K[]) {
417 | const fields = names || this.getNames();
418 | const allErrors = {} as Record;
419 | fields.forEach((f: K) => {
420 | allErrors[f] = this.getError(f);
421 | });
422 | return allErrors;
423 | }
424 |
425 | /**
426 | * 设置单个输入控件的 Error
427 | * @param name - 字段名
428 | * @param errors - 错误信息
429 | */
430 | setError(name: string, errors?: unknown) {
431 | const err: unknown[] = Array.isArray(errors) ? errors : errors ? [errors] : [];
432 | if (name in this.fieldsMeta) {
433 | this.fieldsMeta[name].errors = err;
434 | } else {
435 | this.fieldsMeta[name] = {
436 | errors: err,
437 | name,
438 | };
439 | }
440 |
441 | if (this.fieldsMeta[name].errors && this.fieldsMeta[name].errors!.length > 0) {
442 | this.fieldsMeta[name].state = 'error';
443 | } else {
444 | this.fieldsMeta[name].state = '';
445 | }
446 |
447 | this._reRender(name, 'setError');
448 | }
449 |
450 | /**
451 | * 设置一组输入控件的 Error
452 | */
453 | setErrors(fieldsErrors: Record = {}) {
454 | Object.keys(fieldsErrors).forEach((name) => {
455 | this.setError(name, fieldsErrors[name]);
456 | });
457 | }
458 |
459 | /**
460 | * 获取单个字段的校验状态
461 | * @param name - 字段名
462 | */
463 | getState(name: string): FieldState {
464 | const field = this._get(name);
465 |
466 | if (field && field.state) {
467 | return field.state;
468 | }
469 |
470 | return '';
471 | }
472 |
473 | /**
474 | * 校验全部字段
475 | * @param callback - 校验结果的回调函数
476 | */
477 | validateCallback(callback?: ValidateCallback): void;
478 | /**
479 | * 校验指定字段
480 | * @param names - 字段名或字段名列表
481 | * @param callback - 校验结果回调函数
482 | */
483 | validateCallback(names?: string | string[], callback?: ValidateCallback): void;
484 | /**
485 | * 校验 - Callback version
486 | */
487 | validateCallback(ns?: string | string[] | ValidateCallback, cb?: ValidateCallback) {
488 | const { names, callback } = getParams(ns, cb);
489 | const fieldNames = names || this.getNames();
490 |
491 | const descriptor: Record = {};
492 | const values: FieldValues = {};
493 |
494 | let hasRule = false;
495 | for (let i = 0; i < fieldNames.length; i++) {
496 | const name = fieldNames[i];
497 | const field = this._get(name) as NormalizedFieldMeta;
498 |
499 | if (!field) {
500 | continue;
501 | }
502 |
503 | if (field.rules && field.rules.length) {
504 | descriptor[name] = field.rules;
505 | values[name] = this.getValue(name);
506 | hasRule = true;
507 |
508 | // clear error
509 | field.errors = [];
510 | field.state = '';
511 | }
512 | }
513 |
514 | if (!hasRule) {
515 | const errors = this.formatGetErrors(fieldNames);
516 | callback && callback(errors, this.getValues(names ? fieldNames : []));
517 | return;
518 | }
519 |
520 | const validate = new Validate(descriptor, {
521 | first: this.options.first,
522 | messages: this.options.messages,
523 | });
524 |
525 | validate.validate(values, (errors) => {
526 | let errorsGroup: ValidateErrorGroup | null = null;
527 | if (errors && errors.length) {
528 | errorsGroup = {};
529 | errors.forEach((e) => {
530 | const fieldName = e.field;
531 | if (!errorsGroup![fieldName]) {
532 | errorsGroup![fieldName] = {
533 | errors: [],
534 | };
535 | }
536 | const fieldErrors = errorsGroup![fieldName].errors;
537 | fieldErrors.push(e.message);
538 | });
539 | }
540 | if (errorsGroup) {
541 | // update error in every Field
542 | Object.keys(errorsGroup).forEach((i) => {
543 | const field = this._get(i);
544 | if (field) {
545 | field.errors = getErrorStrs(errorsGroup![i].errors, this.processErrorMessage);
546 | field.state = 'error';
547 | }
548 | });
549 | }
550 |
551 | const formattedGetErrors = this.formatGetErrors(fieldNames);
552 |
553 | if (formattedGetErrors) {
554 | errorsGroup = Object.assign({}, formattedGetErrors, errorsGroup);
555 | }
556 |
557 | // update to success which has no error
558 | for (let i = 0; i < fieldNames.length; i++) {
559 | const name = fieldNames[i];
560 | const field = this._get(name);
561 | if (field && field.rules && !(errorsGroup && name in errorsGroup)) {
562 | field.state = 'success';
563 | }
564 | }
565 |
566 | callback && callback(errorsGroup, this.getValues(names ? fieldNames : []));
567 | this._reRender(names, 'validate');
568 |
569 | this._triggerAfterValidateRerender(errorsGroup);
570 | });
571 | }
572 |
573 | /**
574 | * Promise 方式校验全部字段
575 | */
576 | async validatePromise(): Promise;
577 | /**
578 | * Promise 方式校验指定字段
579 | * @param names - 字段名或字段名列表
580 | */
581 | async validatePromise(names?: string | string[]): Promise;
582 | /**
583 | * Promise 方式校验所有字段,并使用一个函数处理校验结果
584 | * @param formatter - 校验结果处理函数
585 | */
586 | async validatePromise(
587 | formatter?: (results: ValidatePromiseResults) => FormatterResults | Promise
588 | ): Promise;
589 | /**
590 | * Promise 方式校验指定字段,并使用一个函数处理校验结果
591 | * @param names - 字段名或字段名列表
592 | * @param formatter - 校验结果处理函数
593 | */
594 | async validatePromise(
595 | names?: string | string[],
596 | formatter?: (results: ValidatePromiseResults) => FormatterResults | Promise
597 | ): Promise;
598 | /**
599 | * 校验 - Promise version
600 | */
601 | async validatePromise(
602 | ns?: string | string[] | ValidateResultFormatter,
603 | formatter?: ValidateResultFormatter
604 | ) {
605 | const { names, callback } = getParams(ns, formatter);
606 | const fieldNames = names || this.getNames();
607 |
608 | const descriptor: Record = {};
609 | const values: FieldValues = {};
610 |
611 | let hasRule = false;
612 | for (let i = 0; i < fieldNames.length; i++) {
613 | const name = fieldNames[i];
614 | const field = this._get(name) as NormalizedFieldMeta;
615 |
616 | if (!field) {
617 | continue;
618 | }
619 |
620 | if (field.rules && field.rules.length) {
621 | descriptor[name] = field.rules;
622 | values[name] = this.getValue(name);
623 | hasRule = true;
624 |
625 | // clear error
626 | field.errors = [];
627 | field.state = '';
628 | }
629 | }
630 |
631 | if (!hasRule) {
632 | const errors = this.formatGetErrors(fieldNames);
633 | if (callback) {
634 | return callback({
635 | errors,
636 | values: this.getValues(names ? fieldNames : []),
637 | });
638 | } else {
639 | return {
640 | errors,
641 | values: this.getValues(names ? fieldNames : []),
642 | };
643 | }
644 | }
645 |
646 | const validate = new Validate(descriptor, {
647 | first: this.options.first,
648 | messages: this.options.messages,
649 | });
650 |
651 | const results = await validate.validatePromise(values);
652 | const errors = (results && results.errors) || [];
653 |
654 | const errorsGroup = this._getErrorsGroup({ errors, fieldNames });
655 |
656 | let callbackResults: ValidatePromiseResults | FormatterResults = {
657 | errors: errorsGroup,
658 | values: this.getValues(names ? fieldNames : []),
659 | };
660 | try {
661 | if (callback) {
662 | callbackResults = await callback(callbackResults);
663 | }
664 | } catch (error) {
665 | return error;
666 | }
667 | this._reRender(names, 'validate');
668 | // afterValidateRerender 作为通用属性,在 callback 和 promise 两个版本的 validate 中保持相同行为
669 | this._triggerAfterValidateRerender(errorsGroup);
670 | return callbackResults;
671 | }
672 |
673 | /**
674 | * 重置一组输入控件的值,并清空校验信息
675 | * @param names - 要重置的字段名,不传递则重置全部字段
676 | */
677 | reset(ns?: string | string[]) {
678 | this._reset(ns, false);
679 | }
680 |
681 | /**
682 | * 重置一组输入控件的值为默认值,并清空校验信息
683 | * @param names - 要重置的字段名,不传递则重置全部字段
684 | */
685 | resetToDefault(ns?: string | string[]) {
686 | this._reset(ns, true);
687 | }
688 |
689 | /**
690 | * 获取所有字段名列表
691 | */
692 | getNames() {
693 | const fieldsMeta = this.fieldsMeta;
694 | return Object.keys(fieldsMeta).filter(() => {
695 | return true;
696 | });
697 | }
698 |
699 | /**
700 | * 删除某一个或者一组控件的数据,删除后与之相关的 validate/value 都会被清空
701 | * @param name - 要删除的字段名,不传递则删除全部字段
702 | */
703 | remove(ns?: string | string[]) {
704 | if (typeof ns === 'string') {
705 | ns = [ns];
706 | }
707 | if (!ns) {
708 | this.values = {};
709 | }
710 |
711 | const names = ns || Object.keys(this.fieldsMeta);
712 | names.forEach((name) => {
713 | if (name in this.fieldsMeta) {
714 | delete this.fieldsMeta[name];
715 | }
716 | if (this.options.parseName) {
717 | this.values = deleteIn(this.values, name);
718 | } else {
719 | delete this.values[name];
720 | }
721 | });
722 | }
723 |
724 | /**
725 | * 向指定数组字段内添加数据
726 | * @param name - 字段名
727 | * @param index - 开始添加的索引
728 | * @param argv - 新增的数据
729 | */
730 | addArrayValue(name: string, index: number, ...argv: unknown[]) {
731 | return this._spliceArrayValue(name, index, 0, ...argv);
732 | }
733 |
734 | /**
735 | * 删除指定字段数组内的数据
736 | * @param name - 变量名
737 | * @param index - 开始删除的索引
738 | * @param howmany - 删除几个数据,默认为 1
739 | */
740 | deleteArrayValue(name: string, index: number, howmany = 1) {
741 | return this._spliceArrayValue(name, index, howmany);
742 | }
743 |
744 | /**
745 | * splice in a Array [deprecated]
746 | * @deprecated Use `addArrayValue` or `deleteArrayValue` instead
747 | * @param keyMatch - like name.\{index\}
748 | * @param startIndex - index
749 | */
750 | spliceArray(keyMatch: string, startIndex: number) {
751 | // @ts-expect-error FIXME 无效的 if 逻辑,恒定为 false
752 | if (keyMatch.match(/{index}$/) === -1) {
753 | warning('key should match /{index}$/');
754 | return;
755 | }
756 |
757 | // regex to match field names in the same target array
758 | const reg = keyMatch.replace('{index}', '(\\d+)');
759 | const keyReg = new RegExp(`^${reg}`);
760 |
761 | const listMap: Record> = {};
762 | /**
763 | * keyMatch='key.\{index\}'
764 | * case 1: names=['key.0', 'key.1'], should delete 'key.1'
765 | * case 2: names=['key.0.name', 'key.0.email', 'key.1.name', 'key.1.email'], should delete 'key.1.name', 'key.1.email'
766 | */
767 | const names = this.getNames();
768 | const willChangeNames: string[] = [];
769 | names.forEach((n) => {
770 | // is name in the target array?
771 | const ret = keyReg.exec(n);
772 | if (ret) {
773 | const index = parseInt(ret[1]);
774 |
775 | if (index > startIndex) {
776 | const l = listMap[index];
777 | const item = {
778 | from: n,
779 | to: `${keyMatch.replace('{index}', (index - 1).toString())}${n.replace(ret[0], '')}`,
780 | };
781 | willChangeNames.push(item.from);
782 | if (names.includes(item.to)) {
783 | willChangeNames.push(item.to);
784 | }
785 | if (!l) {
786 | listMap[index] = [item];
787 | } else {
788 | l.push(item);
789 | }
790 | }
791 | }
792 | });
793 | const oldValues = this.getValues(willChangeNames);
794 |
795 | const idxList = Object.keys(listMap)
796 | .map((i) => {
797 | return {
798 | index: Number(i),
799 | list: listMap[i],
800 | };
801 | })
802 | // @ts-expect-error FIXME 返回 boolean 值并不能正确排序
803 | .sort((a, b) => a.index < b.index);
804 |
805 | // should be continuous array
806 | if (idxList.length > 0 && idxList[0].index === startIndex + 1) {
807 | idxList.forEach((l) => {
808 | const list = l.list;
809 | list.forEach((i) => {
810 | const v = this.getValue(i.from); // get index value
811 | this.setValue(i.to, v, false, false); // set value to index - 1
812 | });
813 | });
814 |
815 | const lastIdxList = idxList[idxList.length - 1];
816 | lastIdxList.list.forEach((i) => {
817 | this.remove(i.from);
818 | });
819 |
820 | let parentName = keyMatch.replace('.{index}', '');
821 | parentName = parentName.replace('[{index}]', '');
822 | const parent = this.getValue(parentName) as unknown[];
823 |
824 | if (parent) {
825 | // if parseName=true then parent is an Array object but does not know an element was removed
826 | // this manually decrements the array length
827 | parent.length--;
828 | }
829 | }
830 | for (const name of willChangeNames) {
831 | this._triggerFieldChange(name, this.getValue(name), oldValues[name], 'setValue');
832 | }
833 | }
834 |
835 | /**
836 | * 获取全部字段信息
837 | * @param name - 传递 falsy 值
838 | */
839 | get(name?: undefined | ''): Record;
840 | /**
841 | * 获取指定字段信息
842 | * @param name - 字段名
843 | */
844 | get(name: string): NormalizedFieldMeta | null;
845 | get(name?: string) {
846 | if (name) {
847 | return this._get(name);
848 | } else {
849 | return this.fieldsMeta;
850 | }
851 | }
852 |
853 | /**
854 | * 监听字段值变化
855 | * @param names - 监听的 name 列表
856 | * @param callback - 变化回调
857 | * @returns 解除监听回调
858 | */
859 | watch(names: string[], callback: WatchCallback) {
860 | for (const name of names) {
861 | if (!this.listeners[name]) {
862 | this.listeners[name] = new Set();
863 | }
864 | const set = this.listeners[name];
865 | set.add(callback);
866 | }
867 | return () => {
868 | for (const name of names) {
869 | if (this.listeners[name]) {
870 | this.listeners[name].delete(callback);
871 | }
872 | }
873 | };
874 | }
875 |
876 | _get(name: string) {
877 | return name in this.fieldsMeta ? this.fieldsMeta[name] : null;
878 | }
879 |
880 | _getInitMeta(name: string) {
881 | if (!(name in this.fieldsMeta)) {
882 | this.fieldsMeta[name] = Object.assign({ name }, initMeta);
883 | }
884 |
885 | return this.fieldsMeta[name];
886 | }
887 |
888 | _getErrorsGroup({ errors, fieldNames }: { errors: NormalizedValidateError[]; fieldNames: string[] }) {
889 | let errorsGroup: ValidateErrorGroup | null = null;
890 | if (errors && errors.length) {
891 | errorsGroup = {};
892 | errors.forEach((e) => {
893 | const fieldName = e.field;
894 | if (!errorsGroup![fieldName]) {
895 | errorsGroup![fieldName] = {
896 | errors: [],
897 | };
898 | }
899 | const fieldErrors = errorsGroup![fieldName].errors;
900 | fieldErrors.push(e.message);
901 | });
902 | }
903 | if (errorsGroup) {
904 | // update error in every Field
905 | Object.keys(errorsGroup).forEach((i) => {
906 | const field = this._get(i);
907 | if (field) {
908 | field.errors = getErrorStrs(errorsGroup![i].errors, this.processErrorMessage);
909 | field.state = 'error';
910 | }
911 | });
912 | }
913 |
914 | const formattedGetErrors = this.formatGetErrors(fieldNames);
915 |
916 | if (formattedGetErrors) {
917 | errorsGroup = Object.assign({}, formattedGetErrors, errorsGroup);
918 | }
919 |
920 | // update to success which has no error
921 | for (let i = 0; i < fieldNames.length; i++) {
922 | const name = fieldNames[i];
923 | const field = this._get(name);
924 | if (field && field.rules && !(errorsGroup && name in errorsGroup)) {
925 | field.state = 'success';
926 | }
927 | }
928 |
929 | return errorsGroup;
930 | }
931 |
932 | _reset(ns?: string | string[], backToDefault?: boolean) {
933 | if (typeof ns === 'string') {
934 | ns = [ns];
935 | }
936 | let changed = false;
937 |
938 | const names = ns || Object.keys(this.fieldsMeta);
939 |
940 | const oldValues = this.getValues(names);
941 | if (!ns) {
942 | this.values = {};
943 | }
944 | names.forEach((name) => {
945 | const field = this._get(name);
946 | if (field) {
947 | changed = true;
948 |
949 | field.value = backToDefault ? field.initValue : undefined;
950 | field.state = '';
951 |
952 | delete field.errors;
953 | delete field.rules;
954 | delete field.rulesMap;
955 |
956 | if (this.options.parseName) {
957 | this.values = setIn(this.values, name, field.value);
958 | } else {
959 | this.values[name] = field.value;
960 | }
961 | }
962 | this._triggerFieldChange(name, this.getValue(name), oldValues[name], 'reset');
963 | });
964 |
965 | if (changed) {
966 | this._reRender(names, 'reset');
967 | }
968 | }
969 |
970 | _resetError(name: string) {
971 | const field = this._get(name);
972 | if (field) {
973 | delete field.errors; //清空错误
974 | field.state = '';
975 | }
976 | }
977 |
978 | _reRender(name?: string | string[], action?: RerenderType | string) {
979 | // 指定了字段列表且字段存在对应的自定义渲染函数
980 | if (name) {
981 | const names = Array.isArray(name) ? name : [name];
982 | if (names.length && names.every((n) => this.reRenders[n])) {
983 | names.forEach((n) => {
984 | const reRender = this.reRenders[n];
985 | reRender(action);
986 | });
987 | return;
988 | }
989 | }
990 |
991 | if (this.com) {
992 | if (!this.options.forceUpdate && (this.com as { setState: SetState }).setState) {
993 | (this.com as { setState: SetState }).setState({});
994 | } else if ((this.com as Component).forceUpdate) {
995 | (this.com as Component).forceUpdate(); //forceUpdate 对性能有较大的影响,成指数上升
996 | }
997 | }
998 | }
999 |
1000 | /**
1001 | * Get errors using `getErrors` and format to match the structure of errors returned in field.validate
1002 | */
1003 | formatGetErrors(names?: string[]) {
1004 | const errors = this.getErrors(names);
1005 | let formattedErrors: ValidateErrorGroup | null = null;
1006 | for (const field in errors) {
1007 | if (errors.hasOwnProperty(field) && errors[field]) {
1008 | const errorsObj = errors[field]!;
1009 | if (!formattedErrors) {
1010 | formattedErrors = {};
1011 | }
1012 | formattedErrors[field] = { errors: errorsObj };
1013 | }
1014 | }
1015 | return formattedErrors;
1016 | }
1017 |
1018 | /**
1019 | * call native event from props.onXx
1020 | * eg: props.onChange props.onBlur props.onFocus
1021 | */
1022 | _callNativePropsEvent(action: string, props: Record, ...args: unknown[]) {
1023 | action in props &&
1024 | typeof props[action] === 'function' &&
1025 | (props[action] as (...args: unknown[]) => unknown)(...args);
1026 | }
1027 |
1028 | _proxyFieldValue(field: NormalizedFieldMeta) {
1029 | const _this = this;
1030 | Object.defineProperty(field, 'value', {
1031 | configurable: true,
1032 | enumerable: true,
1033 | get() {
1034 | return getIn(_this.values, this.name);
1035 | },
1036 | set(v) {
1037 | // 此处 this 解释同上
1038 | _this.values = setIn(_this.values, this.name, v);
1039 | return true;
1040 | },
1041 | });
1042 | }
1043 |
1044 | /**
1045 | * update field.value and validate
1046 | */
1047 | _updateFieldValue(name: string, ...others: unknown[]) {
1048 | const e = others[0];
1049 | const field = this._get(name);
1050 |
1051 | if (!field) {
1052 | return;
1053 | }
1054 | field.value = field.getValueFormatter ? field.getValueFormatter.apply(this, others) : getValueFromEvent(e);
1055 | field.inputValues = others;
1056 |
1057 | if (this.options.parseName) {
1058 | this.values = setIn(this.values, name, field.value);
1059 | } else {
1060 | this.values[name] = field.value;
1061 | }
1062 | }
1063 |
1064 | /**
1065 | * ref must always be the same function, or if not it will be triggerd every time.
1066 | */
1067 | _getCacheBind(
1068 | name: string,
1069 | action: string,
1070 | fn: (name: string, ...args: Args) => Result
1071 | ): (...args: Args) => Result {
1072 | const cache = (this.cachedBind[name] = this.cachedBind[name] || {});
1073 | if (!cache[action]) {
1074 | cache[action] = fn.bind(this, name);
1075 | }
1076 | return cache[action] as (...args: Args) => Result;
1077 | }
1078 |
1079 | _setCache(name: string, action: string, hander: unknown) {
1080 | const cache = (this.cachedBind[name] = this.cachedBind[name] || {});
1081 | cache[action] = hander;
1082 | }
1083 |
1084 | _getCache(name: string, action: string): R | undefined {
1085 | const cache = this.cachedBind[name] || {};
1086 | return cache[action] as R | undefined;
1087 | }
1088 |
1089 | _saveRef(name: string, component: unknown) {
1090 | const key = `${name}_field`;
1091 | const autoUnmount = this.options.autoUnmount;
1092 |
1093 | if (!component && autoUnmount) {
1094 | // more than one component, do nothing
1095 | this.instanceCount[name] && this.instanceCount[name]--;
1096 | if (this.instanceCount[name] > 0) {
1097 | return;
1098 | }
1099 |
1100 | // component with same name (eg: type ? : )
1101 | // while type changed, B will render before A unmount. so we should cached value for B
1102 | // step: render -> B mount -> 1. _saveRef(A, null) -> 2. _saveRef(B, ref) -> render
1103 | // 1. _saveRef(A, null)
1104 | const cache = this.fieldsMeta[name];
1105 | if (cache) {
1106 | if (this.options.parseName) {
1107 | // 若 parseName 模式下,因为 value 为 getter、setter,所以将当前值记录到_value 内
1108 | cache._value = cache.value;
1109 | }
1110 | this._setCache(name, key, cache);
1111 | }
1112 |
1113 | // after destroy, delete data
1114 | delete this.instance[name];
1115 | delete this.reRenders[name];
1116 | const oldValue = this.getValue(name);
1117 | this.remove(name);
1118 | const newValue = this.getValue(name);
1119 | this._triggerFieldChange(name, newValue, oldValue, 'unmount');
1120 | return;
1121 | }
1122 |
1123 | // 2. _saveRef(B, ref) (eg: same name but different compoent may be here)
1124 | if (autoUnmount && !this.fieldsMeta[name] && this._getCache(name, key)) {
1125 | const cache = this._getCache(name, key)!;
1126 | this.fieldsMeta[name] = cache;
1127 | // 若 parseName 模式,则使用_value 作为值设置到 values 内
1128 | this.setValue(name, this.options.parseName ? cache._value : cache.value, false, false);
1129 | this.options.parseName && '_value' in cache && delete cache._value;
1130 | }
1131 |
1132 | // only one time here
1133 | const field = this._get(name);
1134 |
1135 | if (field) {
1136 | //When the autoUnmount is false, the component uninstallation needs to clear the verification information to avoid blocking the validation.
1137 | if (!component && !autoUnmount) {
1138 | field.state = '';
1139 | delete field.errors;
1140 | delete (field as FieldMeta).rules;
1141 | delete field.rulesMap;
1142 | }
1143 | const ref = field.ref;
1144 | if (ref) {
1145 | if (typeof ref === 'string') {
1146 | throw new Error(`can not set string ref for ${name}`);
1147 | } else if (typeof ref === 'function') {
1148 | ref(component);
1149 | } else if (typeof ref === 'object' && 'current' in ref) {
1150 | // while ref = React.createRef() ref={ current: null}
1151 | (ref as MutableRefObject).current = component;
1152 | }
1153 | }
1154 |
1155 | // mount
1156 | if (autoUnmount && component) {
1157 | let cnt = this.instanceCount[name];
1158 | if (!cnt) {
1159 | cnt = 0;
1160 | }
1161 |
1162 | this.instanceCount[name] = cnt + 1;
1163 | }
1164 |
1165 | this.instance[name] = component;
1166 | }
1167 | }
1168 |
1169 | _validate(name: string, rule: Rule[], trigger: string) {
1170 | const field = this._get(name);
1171 | if (!field) {
1172 | return;
1173 | }
1174 |
1175 | const value = field.value;
1176 |
1177 | field.state = 'loading';
1178 | let validate = this._getCache(name, trigger);
1179 |
1180 | if (validate && typeof validate.abort === 'function') {
1181 | validate.abort();
1182 | }
1183 | validate = new Validate({ [name]: rule }, { messages: this.options.messages });
1184 |
1185 | this._setCache(name, trigger, validate);
1186 |
1187 | validate.validate(
1188 | {
1189 | [name]: value,
1190 | },
1191 | (errors) => {
1192 | let newErrors: string[], newState: FieldState;
1193 | if (errors && errors.length) {
1194 | newErrors = getErrorStrs(errors, this.processErrorMessage);
1195 | newState = 'error';
1196 | } else {
1197 | newErrors = [];
1198 | newState = 'success';
1199 | }
1200 |
1201 | let reRender = false;
1202 | // only status or errors changed, Rerender
1203 | if (
1204 | newState !== field.state ||
1205 | !field.errors ||
1206 | newErrors.length !== field.errors.length ||
1207 | newErrors.find((e, idx) => e !== field.errors![idx])
1208 | ) {
1209 | reRender = true;
1210 | }
1211 |
1212 | field.errors = newErrors;
1213 | field.state = newState;
1214 |
1215 | reRender && this._reRender(name, 'validate');
1216 | }
1217 | );
1218 | }
1219 |
1220 | /**
1221 | * splice array
1222 | */
1223 | _spliceArrayValue(key: string, index: number, howmany: number, ...argv: unknown[]) {
1224 | const argc = argv.length;
1225 | const offset = howmany - argc; // how the reset fieldMeta move
1226 | const startIndex = index + howmany; // 计算起点
1227 |
1228 | /**
1229 | * eg: call _spliceArrayValue('key', 1) to delete 'key.1':
1230 | * case 1: names=['key.0', 'key.1']; delete 'key.1';
1231 | * case 2: names=['key.0', 'key.1', 'key.2']; key.1= key.2; delete key.2;
1232 | * case 3: names=['key.0.name', 'key.0.email', 'key.1.name', 'key.1.email'], should delete 'key.1.name', 'key.1.email'
1233 | * eg: call _spliceArrayValue('key', 1, item) to add 'key.1':
1234 | * case 1: names=['key.0']; add 'key.1' = item;
1235 | * case 2: names=['key.0', 'key.1']; key.2= key.1; delete key.1; add key.1 = item;
1236 | */
1237 | const listMap: Record> = {}; // eg: {1:[{from: 'key.2.name', to: 'key.1.name'}, {from: 'key.2.email', to: 'key.1.email'}]}
1238 | const replacedReg = /\$/g;
1239 | // 替换特殊字符$
1240 | const replacedKey = key.replace(replacedReg, '\\$&');
1241 | const keyReg = new RegExp(`^(${replacedKey}.)(\\d+)`);
1242 | const replaceArgv: string[] = [];
1243 | const names = this.getNames();
1244 | const willChangeNames: string[] = [];
1245 |
1246 | // logic of offset fix begin
1247 | names.forEach((n) => {
1248 | const ret = keyReg.exec(n);
1249 | if (ret) {
1250 | const idx = parseInt(ret[2]); // get index of 'key.0.name'
1251 |
1252 | if (idx >= startIndex) {
1253 | const l = listMap[idx];
1254 | const item = {
1255 | from: n,
1256 | to: n.replace(keyReg, (match, p1) => `${p1}${idx - offset}`),
1257 | };
1258 | willChangeNames.push(item.from);
1259 | if (names.includes(item.to)) {
1260 | willChangeNames.push(item.to);
1261 | }
1262 | if (!l) {
1263 | listMap[idx] = [item];
1264 | } else {
1265 | l.push(item);
1266 | }
1267 | }
1268 |
1269 | // in case of offsetList.length = 0, eg: delete last element
1270 | if (offset > 0 && idx >= index && idx < index + howmany) {
1271 | replaceArgv.push(n);
1272 | }
1273 | }
1274 | });
1275 |
1276 | const oldValues = this.getValues(willChangeNames);
1277 |
1278 | // sort with index eg: [{index:1, list: [{from: 'key.2.name', to: 'key.1.name'}]}, {index:2, list: [...]}]
1279 | const offsetList = Object.keys(listMap)
1280 | .map((i) => {
1281 | return {
1282 | index: Number(i),
1283 | list: listMap[i],
1284 | };
1285 | })
1286 | .sort((a, b) => (offset > 0 ? a.index - b.index : b.index - a.index));
1287 |
1288 | offsetList.forEach((l) => {
1289 | const list = l.list;
1290 | list.forEach((i) => {
1291 | this.fieldsMeta[i.to] = this.fieldsMeta[i.from];
1292 | // 移位后,同步调整 name
1293 | this.fieldsMeta[i.to].name = i.to;
1294 | });
1295 | });
1296 |
1297 | // delete copy data
1298 | if (offsetList.length > 0) {
1299 | const removeList = offsetList.slice(offsetList.length - (offset < 0 ? -offset : offset), offsetList.length);
1300 | removeList.forEach((item) => {
1301 | item.list.forEach((i) => {
1302 | delete this.fieldsMeta[i.from];
1303 | });
1304 | });
1305 | } else {
1306 | // will get from this.values while rerender
1307 | replaceArgv.forEach((i) => {
1308 | delete this.fieldsMeta[i];
1309 | });
1310 | }
1311 |
1312 | const p = this.getValue(key) as unknown[];
1313 | if (p) {
1314 | p.splice(index, howmany, ...argv);
1315 | }
1316 |
1317 | for (const name of willChangeNames) {
1318 | this._triggerFieldChange(name, this.getValue(name), oldValues[name], 'setValue');
1319 | }
1320 |
1321 | this._reRender();
1322 | }
1323 |
1324 | _triggerFieldChange(name: string, value: unknown, oldValue: unknown, triggerType: WatchTriggerType) {
1325 | // same value should not trigger change
1326 | if (Object.is(value, oldValue)) {
1327 | return;
1328 | }
1329 | const listenerSet = this.listeners[name];
1330 | if (!listenerSet?.size) {
1331 | return;
1332 | }
1333 | for (const callback of listenerSet) {
1334 | callback(name, value, oldValue, triggerType);
1335 | }
1336 | }
1337 |
1338 | _triggerAfterValidateRerender(errorsGroup: ValidateErrorGroup | null) {
1339 | if (typeof this.afterValidateRerender === 'function') {
1340 | this.afterValidateRerender({
1341 | errorsGroup,
1342 | options: this.options,
1343 | instance: this.instance,
1344 | });
1345 | }
1346 | }
1347 | }
1348 |
1349 | export * from './types';
1350 |
1351 | export default Field;
1352 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { MessagesConfig, PresetFormatter, Validator } from '@alifd/validate';
2 | import type { Component, useState, useMemo, Dispatch, SetStateAction, Ref, RefCallback } from 'react';
3 |
4 | export type { Validator };
5 |
6 | export type FieldValues = Record;
7 |
8 | export type ValidateErrorGroup = Record;
9 |
10 | export interface FieldOption {
11 | /**
12 | * 所有组件的 change 都会到达这里 [setValue 不会触发该函数]
13 | */
14 | onChange?: (name: string, value: any) => void;
15 |
16 | /**
17 | * 是否翻译 init(name) 中的 name(getValues 会把带。的字符串转换成对象)
18 | * @defaultValue false
19 | */
20 | parseName?: boolean;
21 |
22 | /**
23 | * 仅建议 PureComponent 的组件打开此强制刷新功能,会带来性能问题 (500 个组件为例:打开的时候 render 花费 700ms, 关闭时候 render 花费 400ms)
24 | * @defaultValue false
25 | */
26 | forceUpdate?: boolean;
27 |
28 | /**
29 | * 自动删除 (remove) Unmout 元素,如果想保留数据可以设置为 false
30 | * @defaultValue true
31 | */
32 | autoUnmount?: boolean;
33 |
34 | /**
35 | * 是否修改数据的时候就自动触发校验,设为 false 后只能通过 validate() 来触发校验
36 | * @defaultValue true
37 | */
38 | autoValidate?: boolean;
39 |
40 | /**
41 | * 初始化数据
42 | */
43 | values?: FieldValues;
44 |
45 | /**
46 | * 校验时发现第一个错误就返回
47 | * @defaultValue false
48 | */
49 | first?: boolean;
50 |
51 | /**
52 | * 定制默认错误校验信息模板
53 | */
54 | messages?: MessagesConfig;
55 |
56 | /**
57 | * 处理错误信息
58 | * @param message - 错误信息字符串
59 | * @returns - 处理后的错误信息
60 | */
61 | processErrorMessage?: (message: string) => string;
62 |
63 | /**
64 | * 校验结束并 rerender 之后的回调
65 | * @param results - 校验结果和一些元信息
66 | */
67 | afterValidateRerender?: (results: {
68 | errorsGroup: ValidateErrorGroup | null;
69 | options: FieldOption;
70 | instance: Record;
71 | }) => void;
72 | }
73 |
74 | export type RequiredSome = Omit & Required>;
75 |
76 | export type NormalizedFieldOption = RequiredSome<
77 | FieldOption,
78 | 'parseName' | 'forceUpdate' | 'first' | 'onChange' | 'autoUnmount' | 'autoValidate'
79 | >;
80 |
81 | export type Rule = {
82 | /**
83 | * 是否必填 (不能和 pattern 同时使用)
84 | * @defaultValue true
85 | */
86 | required?: boolean;
87 |
88 | /**
89 | * 出错时候信息
90 | */
91 | message?: string;
92 |
93 | /**
94 | * 校验正则表达式
95 | */
96 | pattern?: string | RegExp;
97 | /**
98 | * 字符串最小长度 / 数组最小个数
99 | */
100 | minLength?: number;
101 | /**
102 | * 字符串最大长度 / 数组最大个数
103 | */
104 | maxLength?: number;
105 |
106 | /**
107 | * 字符串精确长度 / 数组精确个数
108 | */
109 | length?: number;
110 |
111 | /**
112 | * 最小值
113 | */
114 | min?: number;
115 |
116 | /**
117 | * 最大值
118 | */
119 | max?: number;
120 | /**
121 | * 对常用 pattern 的总结
122 | */
123 | format?: PresetFormatter;
124 |
125 | /**
126 | * 自定义校验,(校验成功的时候不要忘记执行 callback(),否则会校验不返回)
127 | */
128 | validator?: Validator;
129 |
130 | /**
131 | * 触发校验的事件名称
132 | */
133 | trigger?: string | string[];
134 | };
135 |
136 | export type InitOption<
137 | ValueType = any,
138 | ValueName extends string = 'value',
139 | Trigger extends string = 'onChange',
140 | Props = object,
141 | > = {
142 | /**
143 | * 唯一标识
144 | */
145 | id?: string;
146 | /**
147 | * 组件值的属性名称,如 Checkbox 的是 checked,Input 是 value
148 | * @defaultValue 'value'
149 | */
150 | valueName?: ValueName;
151 |
152 | /**
153 | * 组件初始值 (组件第一次 render 的时候才会读取,后面再修改此值无效),类似 defaultValue
154 | */
155 | initValue?: ValueType;
156 |
157 | /**
158 | * 触发数据变化的事件名称
159 | * @defaultValue 'onChange'
160 | */
161 | trigger?: Trigger;
162 |
163 | /**
164 | * 校验规则
165 | */
166 | rules?: Rule[] | Rule;
167 |
168 | /**
169 | * 自动校验
170 | * @defaultValue true
171 | */
172 | autoValidate?: boolean;
173 |
174 | /**
175 | * 组件自定义的事件可以写在这里,其他会透传 (小包版本^0.3.0 支持,大包^0.7.0 支持)
176 | */
177 | props?: Props;
178 |
179 | /**
180 | * 自定义从组件获取 `value` 的方式,参数顺序和组件的 onChange 完全一致的
181 | */
182 | getValueFormatter?: (eventArgs: any) => ValueType;
183 | /**
184 | * @deprecated Use `getValueFormatter` instead
185 | */
186 | getValueFromEvent?: (eventArgs: any) => ValueType;
187 | /**
188 | * 自定义转换 `value` 为组件需要的数据
189 | */
190 | setValueFormatter?: (value: ValueType, ...restArgs: unknown[]) => any;
191 |
192 | /**
193 | * 自定义重新渲染函数
194 | */
195 | reRender?: RerenderFunction;
196 | };
197 |
198 | export type WatchTriggerType = 'init' | 'change' | 'setValue' | 'unmount' | 'reset';
199 |
200 | export interface WatchCallback {
201 | (name: string, value: unknown, oldValue: unknown, triggerType: WatchTriggerType): void;
202 | }
203 |
204 | export interface GetUseFieldOption {
205 | useState: typeof useState;
206 | useMemo: typeof useMemo;
207 | }
208 |
209 | export type SetState = Dispatch>;
210 |
211 | export type ComponentInstance = Component | { setState: SetState } | unknown;
212 |
213 | /**
214 | * 字段校验状态
215 | * @example
216 | * - '': 初始状态
217 | * - 'loading': 正在校验
218 | * - 'success': 校验成功
219 | * - 'error': 校验错误
220 | */
221 | export type FieldState = '' | 'loading' | 'success' | 'error';
222 |
223 | export interface FieldMeta extends Pick {
224 | name?: string;
225 | value?: unknown;
226 | _value?: unknown;
227 | initValue?: unknown;
228 | disabled?: boolean;
229 | state?: FieldState;
230 | valueName?: string;
231 | trigger?: string;
232 | inputValues?: unknown[];
233 | ref?: Ref;
234 | errors?: unknown[];
235 | rulesMap?: never;
236 | }
237 |
238 | export interface NormalizedFieldMeta
239 | extends Omit, 'rules'> {
240 | rules: Rule[];
241 | }
242 |
243 | export type UnknownFunction = (...args: unknown[]) => unknown;
244 |
245 | export type RerenderType = 'validate' | 'setValue' | 'setError' | 'reset';
246 |
247 | export interface RerenderFunction {
248 | (type?: RerenderType): unknown;
249 | (type?: string): unknown;
250 | }
251 |
252 | export type ValidateCallback = (errors: ValidateErrorGroup | null, values: FieldValues) => unknown;
253 |
254 | export type ValidatePromiseResults = {
255 | errors: ValidateErrorGroup | null;
256 | values: FieldValues;
257 | };
258 |
259 | // 沿用旧的名称
260 | export type ValidateResults = ValidatePromiseResults;
261 |
262 | export type ValidateResultFormatter = (results: ValidatePromiseResults) => R | Promise;
263 |
264 | export type DynamicKV = {
265 | [k in K]: V;
266 | };
267 |
268 | export interface TriggerFunction {
269 | /**
270 | * @param eventOrValue - 事件对象或具体数据
271 | * @param rests - 其它参数
272 | */
273 | (eventOrValue: any, ...rests: any[]): any;
274 | }
275 |
276 | export type InitResult = {
277 | 'data-meta': 'Field';
278 | id: string;
279 | ref: RefCallback;
280 | } & {
281 | [k in Exclude]: ValueType;
282 | } & {
283 | [k in Exclude]: TriggerFunction;
284 | };
285 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ChangeEvent } from 'react';
2 | import type { UnknownFunction } from './types';
3 |
4 | export function splitNameToPath(name: ''): '';
5 | export function splitNameToPath(name: string): string[];
6 | export function splitNameToPath(name: unknown): '';
7 | export function splitNameToPath(name: unknown) {
8 | return typeof name === 'string' && name ? name.replace(/\[/, '.').replace(/\]/, '').split('.') : '';
9 | }
10 |
11 | export function hasIn(state: unknown, name: string | undefined | null): boolean {
12 | if (!state) {
13 | return false;
14 | }
15 |
16 | const path = splitNameToPath(name);
17 | const length = path.length;
18 | if (!length) {
19 | return false;
20 | }
21 |
22 | let result: unknown = state;
23 | for (let i = 0; i < length; ++i) {
24 | // parent is not object
25 | if (typeof result !== 'object' || result === null) {
26 | return false;
27 | }
28 | // has no property
29 | const thisName = path[i];
30 | if (!(thisName in result)) {
31 | return false;
32 | }
33 | // pass on
34 | result = (result as Record)[thisName];
35 | }
36 |
37 | return true;
38 | }
39 |
40 | export function getIn(state: unknown, name: string): V {
41 | if (!state) {
42 | return state as V;
43 | }
44 |
45 | const path = splitNameToPath(name);
46 | const length = path.length;
47 | if (!length) {
48 | return undefined as V;
49 | }
50 |
51 | let result: unknown = state;
52 | for (let i = 0; i < length; ++i) {
53 | // parent is not object
54 | if (typeof result !== 'object' || result === null) {
55 | return undefined as V;
56 | }
57 | result = (result as Record)[path[i]];
58 | }
59 |
60 | return result as V;
61 | }
62 |
63 | function setInWithPath(
64 | state: unknown,
65 | value: unknown,
66 | path: string | Array,
67 | pathIndex: number
68 | ): R {
69 | if (pathIndex >= path.length) {
70 | return value as R;
71 | }
72 |
73 | const first = path[pathIndex];
74 | const next = setInWithPath(state && (state as Record)[first], value, path, pathIndex + 1);
75 |
76 | if (!state) {
77 | const initialized: any = isNaN(first as number) ? {} : [];
78 | initialized[first] = next;
79 | return initialized as R;
80 | }
81 |
82 | if (Array.isArray(state)) {
83 | const copy = [...state];
84 | copy[first as number] = next;
85 | return copy as R;
86 | }
87 |
88 | return Object.assign({}, state, {
89 | [first]: next,
90 | }) as R;
91 | }
92 |
93 | export function setIn(state: unknown, name: string, value: unknown): R {
94 | return setInWithPath(
95 | state,
96 | value,
97 | typeof name === 'string' ? name.replace(/\[/, '.').replace(/\]/, '').split('.') : '',
98 | 0
99 | );
100 | }
101 |
102 | export function deleteIn(state: undefined | null, name: string): undefined;
103 | export function deleteIn | unknown[]>(state: S, name: string): S;
104 | export function deleteIn | unknown[] | undefined | null>(state: S, name: string) {
105 | if (!state) {
106 | return;
107 | }
108 |
109 | const path = typeof name === 'string' ? name.replace(/\[/, '.').replace(/\]/, '').split('.') : '';
110 | const length = path.length;
111 | if (!length) {
112 | return state;
113 | }
114 |
115 | let result: any = state;
116 | for (let i = 0; i < length && !!result; ++i) {
117 | if (i === length - 1) {
118 | delete result[path[i]];
119 | } else {
120 | result = result[path[i]];
121 | }
122 | }
123 |
124 | return state;
125 | }
126 | export function getErrorStrs(): undefined;
127 | export function getErrorStrs(
128 | errors: Errors,
129 | processErrorMessage?: (message: unknown) => unknown
130 | ): Errors;
131 | export function getErrorStrs(
132 | errors: { message: MessageType }[],
133 | processErrorMessage?: ((message: MessageType) => R) | undefined
134 | ): typeof processErrorMessage extends undefined ? MessageType[] : R[];
135 | export function getErrorStrs(
136 | errors: Error[],
137 | processErrorMessage?: ((message: Error) => R) | undefined
138 | ): typeof processErrorMessage extends undefined ? Error[] : R[];
139 | export function getErrorStrs, Process extends (message: unknown) => unknown>(
140 | errors?: Error[] | undefined | null,
141 | processErrorMessage?: Process
142 | ) {
143 | if (errors) {
144 | return errors.map((e) => {
145 | const message = typeof e.message !== 'undefined' ? e.message : e;
146 |
147 | if (typeof processErrorMessage === 'function') {
148 | return processErrorMessage(message);
149 | }
150 |
151 | return message;
152 | });
153 | }
154 | return errors;
155 | }
156 |
157 | export function getParams unknown>(
158 | ns: Cb,
159 | cb?: undefined
160 | ): { names: undefined; callback: Cb };
161 | export function getParams(
162 | ns: Ns | Cb,
163 | cb: Cb
164 | ): { names: Ns extends undefined ? undefined : string[]; callback: Cb };
165 | export function getParams unknown>(ns?: string | string[] | Cb, cb?: Cb) {
166 | let names: string[] | Cb | undefined = typeof ns === 'string' ? [ns] : ns;
167 | let callback = cb;
168 | if (cb === undefined && typeof names === 'function') {
169 | callback = names;
170 | names = undefined;
171 | }
172 | return {
173 | names,
174 | callback,
175 | };
176 | }
177 |
178 | export function isOverwritten(values: unknown, name: unknown): typeof values extends object ? boolean : false;
179 | export function isOverwritten(values: unknown, name: unknown): typeof name extends string ? boolean : false;
180 | export function isOverwritten(values: unknown, name: unknown): boolean;
181 | /**
182 | * name 是否被覆写
183 | * e.g. \{ a: \{ b: 1 \} \} and 'a.b', should return true
184 | * e.g. \{ a: \{ b: 1 \} \} and 'a.b.c', should return true
185 | * e.g. \{ a: \{ b: 1 \} \} and 'a.b2', should return false
186 | * e.g. \{ a: \{ b: 1 \} \} and 'a2', should return false
187 | * e.g. \{ a: \{ b: [0, 1] \} \} and 'a.b[0]' return true
188 | * e.g. \{ a: \{ b: [0, 1] \} \} and 'a.b[5]' return true (miss index means overwritten in array)
189 | * @param values - 写入对象
190 | * @param name - 字段 key
191 | */
192 | export function isOverwritten(values: unknown, name: unknown) {
193 | if (!values || typeof values !== 'object' || !name || typeof name !== 'string') {
194 | return false;
195 | }
196 | const paths = splitNameToPath(name);
197 | let obj = values as Record;
198 | for (const path of paths) {
199 | if (path in obj) {
200 | const pathValue = obj[path];
201 | // 任意一层 path 值不是对象了,则代表被覆盖
202 | if (!pathValue || typeof pathValue !== 'object') {
203 | return true;
204 | } else {
205 | obj = pathValue as Record;
206 | }
207 | } else {
208 | // 数组的 index 已经移除,则代表被覆写
209 | if (Array.isArray(obj)) {
210 | return true;
211 | }
212 | return false;
213 | }
214 | }
215 | // 代表 name in values,则返回 true
216 | return true;
217 | }
218 |
219 | export function getValueFromEvent>(e: E): string | boolean;
220 | export function getValueFromEvent(e: E): E;
221 | /**
222 | * 从组件事件中获取数据
223 | * @param e - Event 或者 value
224 | * @returns 数据值
225 | */
226 | export function getValueFromEvent(e: unknown) {
227 | // support custom element
228 | if (!e || !(e as ChangeEvent).target || !(e as ChangeEvent).preventDefault) {
229 | return e;
230 | }
231 |
232 | const { target } = e as ChangeEvent;
233 |
234 | if (target.type === 'checkbox') {
235 | return target.checked;
236 | } else if (target.type === 'radio') {
237 | //兼容原生 radioGroup
238 | if (target.value) {
239 | return target.value;
240 | } else {
241 | return target.checked;
242 | }
243 | }
244 | return target.value;
245 | }
246 |
247 | function validateMap(
248 | rulesMap: Record,
249 | rule: Rule,
250 | defaultTrigger: string
251 | ) {
252 | const nrule = Object.assign({}, rule);
253 |
254 | if (!nrule.trigger) {
255 | nrule.trigger = [defaultTrigger];
256 | }
257 |
258 | if (typeof nrule.trigger === 'string') {
259 | nrule.trigger = [nrule.trigger];
260 | }
261 |
262 | for (let i = 0; i < nrule.trigger.length; i++) {
263 | const trigger: string = nrule.trigger[i];
264 |
265 | if (trigger in rulesMap) {
266 | rulesMap[trigger].push(nrule);
267 | } else {
268 | rulesMap[trigger] = [nrule];
269 | }
270 | }
271 |
272 | delete nrule.trigger;
273 | }
274 |
275 | /**
276 | * 提取rule里面的trigger并且做映射
277 | * @param rules - 规则
278 | * @param defaultTrigger - 默认触发器
279 | */
280 | export function mapValidateRules(
281 | rules: Rule[],
282 | defaultTrigger: string
283 | ) {
284 | const rulesMap: Record> = {};
285 |
286 | rules.forEach((rule) => {
287 | validateMap(rulesMap, rule, defaultTrigger);
288 | });
289 |
290 | return rulesMap;
291 | }
292 |
293 | let warn: (...args: unknown[]) => void = () => {};
294 |
295 | if (
296 | typeof process !== 'undefined' &&
297 | process.env &&
298 | process.env.NODE_ENV !== 'production' &&
299 | typeof window !== 'undefined' &&
300 | typeof document !== 'undefined'
301 | ) {
302 | warn = (...args) => {
303 | /* eslint-disable no-console */
304 | if (typeof console !== 'undefined' && console.error) {
305 | console.error(...args);
306 | }
307 | /* eslint-enable no-console */
308 | };
309 | }
310 |
311 | export const warning = warn;
312 |
313 | export function cloneToRuleArr(rules?: undefined | null): [];
314 | export function cloneToRuleArr(rules: Rule): Rule extends unknown[] ? Rule : Rule[];
315 | export function cloneToRuleArr(rules?: unknown | unknown[] | null) {
316 | if (!rules) {
317 | return [];
318 | }
319 | const rulesArr = Array.isArray(rules) ? rules : [rules];
320 | // 后续会修改 rule 对象,这里做浅复制以避免对传入对象的修改
321 | return rulesArr.map((rule) => ({ ...rule }));
322 | }
323 |
--------------------------------------------------------------------------------
/test/options.spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import { Input } from '@alifd/next';
3 | import Field from '../src';
4 |
5 | describe('options', () => {
6 | it('should support autoUnmount', () => {
7 | class Demo extends React.Component {
8 | state = {
9 | show: true,
10 | };
11 | field = new Field(this);
12 |
13 | render() {
14 | const init = this.field.init;
15 | return (
16 |
17 |
18 | {this.state.show ? : null}
19 |
20 | );
21 | }
22 | }
23 | const ref = createRef();
24 | cy.mount( ).then(() => {
25 | cy.wrap(ref.current).should('be.ok');
26 | ref.current!.setState({ show: false });
27 | cy.wrap(ref.current!.field.getValues()).should('not.have.property', 'input2');
28 | });
29 | });
30 |
31 | it('should support autoUnmount with same name', () => {
32 | class Demo extends React.Component {
33 | state = {
34 | show: true,
35 | };
36 | field = new Field(this);
37 |
38 | render() {
39 | const init = this.field.init;
40 | return (
41 |
42 | {this.state.show ? (
43 |
44 | ) : (
45 |
46 | )}
47 |
48 | );
49 | }
50 | }
51 | const ref = createRef();
52 | cy.mount( ).then(() => {
53 | cy.wrap(ref.current).should('be.ok');
54 | ref.current!.setState({ show: false });
55 | cy.wrap(ref.current!.field.getValues()).should('have.property', 'input').and('eq', '1');
56 | });
57 | });
58 |
59 | it('should support more than 1 Component with same name, delete one , can still getValue', () => {
60 | class Demo extends React.Component {
61 | state = {
62 | show: true,
63 | };
64 | field = new Field(this);
65 |
66 | render() {
67 | const init = this.field.init;
68 | return (
69 |
70 | {this.state.show && }
71 |
72 |
73 | );
74 | }
75 | }
76 | const ref = createRef();
77 | cy.mount( ).then(() => {
78 | cy.wrap(ref.current).should('be.ok');
79 | ref.current!.setState({ show: false });
80 | cy.wrap(ref.current!.field.getValues()).should('have.property', 'input').and('eq', '1');
81 | });
82 | });
83 |
84 | it('same name field should cache value when use parseName=true and autoUnmount=true', () => {
85 | class Demo extends Component {
86 | state = {
87 | show: true,
88 | };
89 | field = new Field(this, {
90 | autoUnmount: true,
91 | parseName: true,
92 | values: {
93 | name: 'aa',
94 | },
95 | });
96 | render() {
97 | const { show } = this.state;
98 | const { init } = this.field;
99 | return (
100 |
104 | );
105 | }
106 | }
107 |
108 | const ref = createRef();
109 | cy.mount( ).then(() => {
110 | cy.wrap(ref.current).should('be.ok');
111 | const ins = ref.current!;
112 | // 首先判断 name 值是否符合预期
113 | cy.wrap(ins.field.getValue('name')).should('eq', 'aa');
114 | // 调整 show,使两个 input 同时触发卸载和挂载
115 | ins.setState({ show: false });
116 | // 判断 name 值是否保留
117 | cy.wrap(ins.field.getValue('name')).should('eq', 'aa');
118 | // 复原 visible,使两个 input 同时触发挂载和卸载
119 | ins.setState({ show: true });
120 | // 判断 name 值是否保留
121 | cy.wrap(ins.field.getValue('name')).should('eq', 'aa');
122 | });
123 | });
124 |
125 | it('should support autoUnmount=false', () => {
126 | class Demo extends React.Component {
127 | state = {
128 | show: true,
129 | };
130 | field = new Field(this, { autoUnmount: false });
131 |
132 | render() {
133 | const { init } = this.field;
134 | return (
135 |
136 |
137 | {this.state.show && }
138 |
139 | );
140 | }
141 | }
142 | const ref = createRef();
143 | cy.mount( ).then(() => {
144 | cy.wrap(ref.current).should('be.ok');
145 | const ins = ref.current!;
146 | ins.setState({ show: false });
147 | cy.wrap(ins.field.getValue('input2')).should('eq', 'test2');
148 | });
149 | });
150 |
151 | describe('defaultValue', () => {
152 | it('should support `defaultValue`', () => {
153 | const inputValue = 'my value';
154 | const field = new Field({});
155 | field.init('input', { props: { defaultValue: inputValue } });
156 | assert.equal(field.getValue('input'), inputValue);
157 | });
158 |
159 | it('should support `defaultValue` with different value name and make camel case', () => {
160 | const inputValue = 'my value';
161 | const field = new Field({});
162 | field.init('input', {
163 | valueName: 'myValue',
164 | props: { defaultMyValue: inputValue },
165 | });
166 | assert.equal(field.getValue('input'), inputValue);
167 | });
168 |
169 | it('should support `defaultValue` with falsy value', () => {
170 | const inputValue = 0;
171 | const field = new Field({});
172 | field.init('input', { props: { defaultValue: inputValue } });
173 | assert.equal(field.getValue('input'), inputValue);
174 | });
175 | });
176 |
177 | describe('values', () => {
178 | it('should set default field input values when given `values` in constructor', () => {
179 | const inputValue = 'my value';
180 | const field = new Field(
181 | {},
182 | {
183 | values: {
184 | input: inputValue,
185 | },
186 | }
187 | );
188 | assert.equal(field.getValue('input'), inputValue);
189 | });
190 |
191 | it('should set default field input values when given falsy `values` in constructor', () => {
192 | const inputValue = 0;
193 | const field = new Field(
194 | {},
195 | {
196 | values: {
197 | input: inputValue,
198 | },
199 | }
200 | );
201 | field.init('input');
202 | assert.equal(field.getValue('input'), inputValue);
203 | });
204 |
205 | it('should set default field input values when given `values` and `parseName` = true in constructor', () => {
206 | const inputValue = 'my value';
207 | const field = new Field(
208 | {},
209 | {
210 | parseName: true,
211 | values: {
212 | input: {
213 | child: inputValue,
214 | },
215 | },
216 | }
217 | );
218 | field.init('input.child');
219 | assert.equal(field.getValue('input.child'), inputValue);
220 | });
221 |
222 | it('should allow access with `getValue` before init when given `values` in constructor', () => {
223 | const inputValue = 'my value';
224 | const field = new Field(
225 | {},
226 | {
227 | values: {
228 | input: inputValue,
229 | },
230 | }
231 | );
232 | assert.equal(field.getValue('input'), inputValue);
233 | });
234 |
235 | it('should allow access to with `getValues` before init when given `values` in constructor', () => {
236 | const inputValue = 'my value';
237 | const field = new Field(
238 | {},
239 | {
240 | values: {
241 | input: inputValue,
242 | },
243 | }
244 | );
245 | assert.equal(field.getValues().input, inputValue);
246 | });
247 |
248 | it('should use setValues instead of constructor values on field that has not been initialized', () => {
249 | const inputValue = 'my value';
250 | const field = new Field(
251 | {},
252 | {
253 | values: {
254 | input: inputValue,
255 | },
256 | }
257 | );
258 | field.setValue('input', 1);
259 | assert.equal(field.getValue('input'), 1);
260 | });
261 |
262 | it('should reset `input` to undefined when given `values` in constructor and call `reset`', () => {
263 | const fieldDefault = 'field default value';
264 | const field = new Field(
265 | {},
266 | {
267 | values: {
268 | input: fieldDefault,
269 | },
270 | }
271 | );
272 | field.init('input');
273 | field.reset();
274 | assert.equal(field.getValue('input'), undefined);
275 | });
276 |
277 | it('should reset `input` to constructor `undefined` after calling `resetToDefault`', () => {
278 | const fieldDefault = 'field default value';
279 | const field = new Field(
280 | {},
281 | {
282 | values: {
283 | input: fieldDefault,
284 | },
285 | }
286 | );
287 | field.init('input');
288 | field.resetToDefault('input');
289 | assert.equal(field.getValue('input'), undefined);
290 | });
291 |
292 | it('should reset `input` to undefined when given `values` in constructor and call `reset`', () => {
293 | const fieldDefault = 'field default value';
294 | const field = new Field(
295 | {},
296 | {
297 | values: {
298 | input: fieldDefault,
299 | },
300 | }
301 | );
302 | field.init('input');
303 | field.reset();
304 | assert.equal(field.getValue('input'), undefined);
305 | });
306 |
307 | it('should return `{}` for `getValues after all fields are removed', () => {
308 | const fieldDefault = 'field default value';
309 | const field = new Field(
310 | {},
311 | {
312 | values: {
313 | input: fieldDefault,
314 | },
315 | }
316 | );
317 | field.init('input');
318 | field.remove('input');
319 | assert.equal(Object.keys(field.getValues()).length, 0);
320 | });
321 |
322 | it('should return `undefined` after `remove` then re-`init`', () => {
323 | const field = new Field({}, { values: { input: 4 } });
324 | field.init('input');
325 | field.remove('input');
326 | field.init('input');
327 |
328 | assert(field.getValue('input') === undefined);
329 | });
330 |
331 | it('should set the value to constructor value even with initValue from init', () => {
332 | const inputValue = 0;
333 | const field = new Field(
334 | {},
335 | {
336 | values: {
337 | input: inputValue,
338 | },
339 | }
340 | );
341 | field.init('input', { initValue: 1 });
342 | assert.equal(field.getValue('input'), inputValue);
343 | });
344 | });
345 |
346 | describe('should support parseName', () => {
347 | it('getValues', () => {
348 | const field = new Field({}, { parseName: true });
349 | field.init('user.name', { initValue: 'frankqian' });
350 | field.init('user.pwd', { initValue: 12345 });
351 | field.init('option[0]', { initValue: 'option1' });
352 | field.init('option[1]', { initValue: 'option2' });
353 | const values = field.getValues<{
354 | user: { name: string; pwd: number };
355 | option: string[];
356 | }>();
357 |
358 | assert(Object.keys(values).length === 2);
359 | assert(values.user.name === 'frankqian');
360 | assert(values.user.pwd === 12345);
361 | assert(values.option[0] === 'option1');
362 |
363 | assert(field.getValue('option[1]') === 'option2');
364 | });
365 | it('should get constructor value of `name` if `getValue` called before init', () => {
366 | const field = new Field(
367 | {},
368 | {
369 | parseName: true,
370 | values: { a: { b: 1 } },
371 | }
372 | );
373 | assert(field.getValue('a.b') === 1);
374 | });
375 |
376 | it('should return constructor value for `names` if `getValues` called before init', () => {
377 | const field = new Field(
378 | {},
379 | {
380 | parseName: true,
381 | values: { a: 1, b: 2, c: 3 },
382 | }
383 | );
384 | const { a, b } = field.getValues(['a', 'b']);
385 | assert(a === 1);
386 | assert(b === 2);
387 | });
388 | it('should return all of constructor value if `getValues` called with no names before init', () => {
389 | const field = new Field(
390 | {},
391 | {
392 | parseName: true,
393 | values: { a: 1, b: 2, c: 3 },
394 | }
395 | );
396 | const { a, b, c } = field.getValues();
397 | assert(a === 1);
398 | assert(b === 2);
399 | assert(c === 3);
400 | });
401 | it('setValues', () => {
402 | const field = new Field({}, { parseName: true });
403 | field.init('user.name', { initValue: 'frankqian' });
404 | field.init('user.pwd', { initValue: 12345 });
405 | field.init('option[0]', { initValue: 'option1' });
406 | field.init('option[1]', { initValue: 'option2' });
407 |
408 | let values = field.getValues<{
409 | user: { name: string; pwd: number | string };
410 | option: string[];
411 | }>();
412 | assert(values.user.name === 'frankqian');
413 | assert(values.user.pwd === 12345);
414 | assert(values.option[0] === 'option1');
415 | assert(values.option[1] === 'option2');
416 |
417 | field.setValues({
418 | user: {
419 | pwd: 'helloworld',
420 | },
421 | option: ['test1', 'test2'],
422 | });
423 |
424 | values = field.getValues();
425 |
426 | assert(Object.keys(values).length === 2);
427 |
428 | assert(values.user.name === 'frankqian');
429 | assert(values.user.pwd === 'helloworld');
430 | assert(values.option[0] === 'test1');
431 | });
432 |
433 | it('should allow access with `getValue` before init when given `values` in constructor', () => {
434 | const fieldDefault = 'field default value';
435 | const field = new Field(
436 | {},
437 | {
438 | parseName: true,
439 | values: {
440 | input: {
441 | myValue: fieldDefault,
442 | },
443 | },
444 | }
445 | );
446 | assert.equal(field.getValue('input.myValue'), fieldDefault);
447 | });
448 |
449 | it('should allow access to with `getValues` before init when given `values` in constructor', () => {
450 | const fieldDefault = 'field default value';
451 | const field = new Field(
452 | {},
453 | {
454 | parseName: true,
455 | values: {
456 | input: {
457 | myValue: fieldDefault,
458 | },
459 | },
460 | }
461 | );
462 | assert.equal(field.getValues<{ input: { myValue: string } }>().input.myValue, fieldDefault);
463 | });
464 |
465 | it('should use setValue instead of constructor values on field that has not been initialized', () => {
466 | const fieldDefault = 'field default value';
467 | const field = new Field(
468 | {},
469 | {
470 | parseName: true,
471 | values: {
472 | input: {
473 | myValue: fieldDefault,
474 | },
475 | },
476 | }
477 | );
478 | field.setValue('input.myValue', 1);
479 | assert.equal(field.getValue('input.myValue'), 1);
480 | });
481 |
482 | it('should remove top level field after removed', () => {
483 | const fieldDefault = 'field default value';
484 | const field = new Field(
485 | {},
486 | {
487 | parseName: true,
488 | values: {
489 | input: {
490 | myValue: fieldDefault,
491 | },
492 | },
493 | }
494 | );
495 | field.init('input.myValue');
496 | field.remove('input.myValue');
497 | assert.deepEqual(field.getValues(), { input: {} });
498 | });
499 |
500 | it('should return `{}` for `getValues after `remove()`', () => {
501 | const fieldDefault = 'field default value';
502 | const field = new Field(
503 | {},
504 | {
505 | parseName: true,
506 | values: {
507 | input: {
508 | myValue: fieldDefault,
509 | },
510 | },
511 | }
512 | );
513 | field.init('input.myValue');
514 | field.setValue('input.value2', fieldDefault);
515 | field.remove();
516 | assert.equal(Object.keys(field.getValues()).length, 0);
517 | });
518 |
519 | it('should return `undefined` after `remove` then re-`init`', () => {
520 | const fieldDefault = 'field default value';
521 | const field = new Field(
522 | {},
523 | {
524 | parseName: true,
525 | values: {
526 | input: {
527 | myValue: fieldDefault,
528 | },
529 | },
530 | }
531 | );
532 | field.init('input.myValue');
533 | field.remove('input.myValue');
534 | field.init('input.myValue');
535 |
536 | assert(field.getValue('input.myValue') === undefined);
537 | });
538 |
539 | it('should return all setValues', () => {
540 | const fieldDefault = 'field default value';
541 | const field = new Field(
542 | {},
543 | {
544 | parseName: true,
545 | }
546 | );
547 | field.setValues({
548 | input: {
549 | myValue: fieldDefault,
550 | },
551 | });
552 |
553 | assert.deepEqual(field.getValues(), {
554 | input: { myValue: fieldDefault },
555 | });
556 | });
557 |
558 | it('should return all setValues and initValues', () => {
559 | const fieldDefault = 'field default value';
560 | const otherDefault = 'other default value';
561 | const field = new Field(
562 | {},
563 | {
564 | parseName: true,
565 | }
566 | );
567 | field.setValues({
568 | input: {
569 | myValue: fieldDefault,
570 | },
571 | });
572 |
573 | field.init('input.otherValue', { initValue: otherDefault });
574 |
575 | assert.deepEqual(field.getValues(), {
576 | input: {
577 | myValue: fieldDefault,
578 | otherValue: otherDefault,
579 | },
580 | });
581 | });
582 | describe('reset', () => {
583 | it('should reset all to undefined when call `reset`', () => {
584 | const fieldDefault = 'field default value';
585 | const field = new Field(
586 | {},
587 | {
588 | parseName: true,
589 | }
590 | );
591 | field.setValue('input.myValue', fieldDefault);
592 | field.setValue('input.otherValue', fieldDefault);
593 | field.reset();
594 | assert.equal(field.getValue('input.myValue'), undefined);
595 | assert.equal(field.getValue('input.otherValue'), undefined);
596 | });
597 |
598 | it('should reset all to undefined when given `values` in constructor and call `reset`', () => {
599 | const fieldDefault = 'field default value';
600 | const field = new Field(
601 | {},
602 | {
603 | parseName: true,
604 | values: {
605 | input: {
606 | myValue: fieldDefault,
607 | otherValue: fieldDefault,
608 | },
609 | },
610 | }
611 | );
612 | field.init('input.myValue');
613 | field.init('input.otherValue');
614 | field.reset();
615 | assert.equal(field.getValue('input.myValue'), undefined);
616 | assert.equal(field.getValue('input.otherValue'), undefined);
617 | });
618 |
619 | it('should reset only `input.myValue` to undefined when given `values` in constructor and pass `input.myValue` to `reset`', () => {
620 | const fieldDefault = 'field default value';
621 | const field = new Field(
622 | {},
623 | {
624 | parseName: true,
625 | values: {
626 | input: {
627 | myValue: fieldDefault,
628 | otherValue: fieldDefault,
629 | },
630 | },
631 | }
632 | );
633 | field.init('input.myValue');
634 | field.reset('input.myValue');
635 | assert.equal(field.getValue('input.myValue'), undefined);
636 | assert.equal(field.getValue('input.otherValue'), fieldDefault);
637 | });
638 |
639 | it('should reset all to undefined when call `resetToDefault` with no defaults', () => {
640 | const fieldDefault = 'field default value';
641 | const field = new Field(
642 | {},
643 | {
644 | parseName: true,
645 | }
646 | );
647 | field.setValue('input.myValue', fieldDefault);
648 | field.setValue('input.otherValue', fieldDefault);
649 | field.resetToDefault();
650 | assert.equal(field.getValue('input.myValue'), undefined);
651 | assert.equal(field.getValue('input.otherValue'), undefined);
652 | });
653 |
654 | it('should reset all to undefined when given `values` in constructor and call `resetToDefault`', () => {
655 | const fieldDefault = 'field default value';
656 | const secondValue = 'second';
657 | const field = new Field(
658 | {},
659 | {
660 | parseName: true,
661 | values: {
662 | input: {
663 | myValue: fieldDefault,
664 | otherValue: fieldDefault,
665 | },
666 | },
667 | }
668 | );
669 | field.init('input.myValue');
670 | field.init('input.otherValue');
671 | field.setValue('input.myValue', secondValue);
672 | field.setValue('input.otherValue', secondValue);
673 |
674 | // simulation rerender
675 | field.init('input.myValue');
676 | field.init('input.otherValue');
677 |
678 | field.resetToDefault();
679 | assert.equal(field.getValue('input.myValue'), undefined);
680 | assert.equal(field.getValue('input.otherValue'), undefined);
681 | });
682 |
683 | it('should reset `input.myValue` which inited to undefined when given `values` in constructor and call `resetToDefault`', () => {
684 | const fieldDefault = 'field default value';
685 | const secondValue = 'second';
686 | const field = new Field(
687 | {},
688 | {
689 | parseName: true,
690 | values: {
691 | input: {
692 | myValue: fieldDefault,
693 | otherValue: fieldDefault,
694 | },
695 | },
696 | }
697 | );
698 | field.init('input.myValue');
699 | field.setValue('input.myValue', secondValue);
700 | field.setValue('input.otherValue', secondValue);
701 |
702 | field.init('input.myValue');
703 |
704 | field.resetToDefault('input.myValue');
705 | assert.equal(field.getValue('input.myValue'), undefined);
706 | assert.equal(field.getValue('input.otherValue'), secondValue);
707 | });
708 | });
709 |
710 | it('should set the value to constructor value even with initValue from init', () => {
711 | const fieldDefault = 0;
712 | const initValue = 'other default value';
713 | const field = new Field(
714 | {},
715 | {
716 | parseName: true,
717 | values: {
718 | input: {
719 | myValue: fieldDefault,
720 | },
721 | },
722 | }
723 | );
724 |
725 | field.init('input.myValue', { initValue });
726 |
727 | assert.deepEqual(field.getValues(), {
728 | input: {
729 | myValue: fieldDefault,
730 | },
731 | });
732 | });
733 |
734 | // Fix https://github.com/alibaba-fusion/next/issues/4525
735 | it('overwrite values by setValues', () => {
736 | const field = new Field(
737 | {},
738 | {
739 | parseName: true,
740 | values: {
741 | one: [
742 | [
743 | {
744 | b: { name: 'zhangsan', age: 17 },
745 | },
746 | ],
747 | ],
748 | two: { code: '555' },
749 | },
750 | }
751 | );
752 | const name = field.init('one.0.0.b.name');
753 | const age = field.init('one.0.0.b.age');
754 | const code = field.init('two.code');
755 | assert.equal(name.value, 'zhangsan');
756 | assert.equal(age.value, 17);
757 | assert.equal(code.value, '555');
758 |
759 | field.setValues({
760 | one: [
761 | [
762 | {
763 | b: null,
764 | },
765 | ],
766 | ],
767 | two: '',
768 | });
769 | assert.equal(field.init('one.0.0.b.name').value, undefined);
770 | assert.equal(field.init('one.0.0.b.age').value, undefined);
771 | assert.equal(field.init('two.code').value, undefined);
772 | });
773 | });
774 |
775 | describe('should support autoValidate=false', () => {
776 | it('options.autoValidate=true', () => {
777 | const field = new Field({}, { autoValidate: true });
778 | const { value, ...inited } = field.init('input', {
779 | rules: [{ minLength: 10 }],
780 | });
781 |
782 | cy.mount( );
783 | cy.get('input').type('test');
784 | cy.then(() => {
785 | cy.wrap(field.getError('input')).should('not.be.null');
786 | });
787 | });
788 | it('options.autoValidate=false', () => {
789 | const field = new Field({}, { autoValidate: false });
790 | const { value, ...inited } = field.init('input', {
791 | rules: [{ minLength: 10 }],
792 | });
793 |
794 | cy.mount( );
795 | cy.get('input').type('test');
796 | cy.then(() => {
797 | cy.wrap(field.getError('input')).should('be.null');
798 | field.validateCallback('input');
799 | cy.wrap(field.getError('input')).should('not.be.null');
800 | });
801 | });
802 | it('props.autoValidate=false', () => {
803 | const field = new Field({});
804 | const { value, ...inited } = field.init('input', {
805 | autoValidate: false,
806 | rules: [{ minLength: 10 }],
807 | });
808 |
809 | cy.mount( );
810 | cy.get('input').type('test');
811 | cy.then(() => {
812 | cy.wrap(field.getError('input')).should('be.null');
813 | field.validateCallback('input');
814 | cy.wrap(field.getError('input')).should('not.be.null');
815 | });
816 | });
817 | });
818 |
819 | describe('processErrorMessage', () => {
820 | it('should pass error messages to `processErrorMessage` on validate', () => {
821 | const mySpy = cy.spy();
822 | const field = new Field({}, { processErrorMessage: mySpy });
823 | const inited = field.init('input', {
824 | initValue: 'test',
825 | rules: [{ minLength: 10, message: 'my error message' }],
826 | });
827 |
828 | cy.mount( ).then(() => {
829 | field.validateCallback();
830 | cy.wrap(mySpy).should('be.calledOnce');
831 | cy.wrap(mySpy.firstCall.args[0]).should('eq', 'my error message');
832 | });
833 | });
834 | });
835 |
836 | describe('afterValidateRerender', () => {
837 | it('should pass error messages to `afterValidateRerender` on validate', () => {
838 | const mySpy = cy.spy();
839 | const field = new Field({}, { afterValidateRerender: mySpy });
840 | const inited = field.init('input', {
841 | initValue: 'test',
842 | rules: [{ minLength: 10, message: 'my error message' }],
843 | });
844 |
845 | cy.mount( ).then(() => {
846 | field.validateCallback();
847 | cy.wrap(mySpy).should('be.calledOnce');
848 | cy.wrap(mySpy.firstCall.args[0].errorsGroup).should('deep.equal', {
849 | input: { errors: ['my error message'] },
850 | });
851 | cy.wrap(mySpy.firstCall.args[0].options).should('deep.equal', field.options);
852 | cy.wrap(mySpy.firstCall.args[0].instance).should(
853 | 'deep.equal',
854 | // @ts-expect-error read internal property "instance" for test
855 | field.instance
856 | );
857 | });
858 | });
859 | });
860 |
861 | describe('messages', () => {
862 | it('should support custom messages', () => {
863 | const mySpy = cy.spy();
864 | const field = new Field(
865 | {},
866 | {
867 | afterValidateRerender: mySpy,
868 | messages: {
869 | string: {
870 | minLength: 'custom error message',
871 | },
872 | },
873 | }
874 | );
875 | const inited = field.init('input', {
876 | initValue: 'test',
877 | rules: [{ minLength: 10 }],
878 | });
879 |
880 | cy.mount( ).then(() => {
881 | field.validateCallback();
882 | cy.wrap(mySpy).should('be.calledOnce');
883 | cy.wrap(mySpy.firstCall.args[0].errorsGroup).should('deep.equal', {
884 | input: { errors: ['custom error message'] },
885 | });
886 | });
887 | });
888 |
889 | it('should prefer user passed messages', () => {
890 | const mySpy = cy.spy();
891 | const field = new Field(
892 | {},
893 | {
894 | afterValidateRerender: mySpy,
895 | messages: {
896 | string: {
897 | minLength: 'custom error message',
898 | },
899 | },
900 | }
901 | );
902 | const inited = field.init('input', {
903 | initValue: 'test',
904 | rules: [{ minLength: 10, message: 'my error message' }],
905 | });
906 |
907 | cy.mount( ).then(() => {
908 | field.validateCallback();
909 | cy.wrap(mySpy).should('be.calledOnce');
910 | cy.wrap(mySpy.firstCall.args[0].errorsGroup).should('deep.equal', {
911 | input: { errors: ['my error message'] },
912 | });
913 | });
914 | });
915 | });
916 | });
917 |
--------------------------------------------------------------------------------
/test/rules.spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { createRef, forwardRef, useImperativeHandle } from 'react';
2 | import { Input } from '@alifd/next';
3 | import Field from '../src';
4 | import { Validator } from '../src/types';
5 |
6 | describe('rules', () => {
7 | it('required - validate', () => {
8 | const field = new Field({});
9 | const { value, ...inited } = field.init('input', {
10 | rules: [
11 | {
12 | required: true,
13 | message: 'cant be null',
14 | },
15 | ],
16 | });
17 |
18 | cy.mount( );
19 | cy.get('input').type('1');
20 | cy.get('input').clear();
21 | cy.then(() => {
22 | cy.wrap(field.getError('input')).should('deep.equal', ['cant be null']);
23 | });
24 |
25 | const { value: _v, ...inited2 } = field.init('input', { rules: [] });
26 | cy.mount( );
27 | const callback = cy.spy();
28 | field.validateCallback(callback);
29 |
30 | cy.wrap(callback).should('be.calledOnce');
31 | cy.wrap(callback).should('be.calledWithMatch', null);
32 | });
33 |
34 | it('required - validatePromise - callback', () => {
35 | const field = new Field({});
36 | const { value: _v, ...inited } = field.init('input', {
37 | rules: [
38 | {
39 | required: true,
40 | message: 'cant be null',
41 | },
42 | ],
43 | });
44 |
45 | cy.mount( );
46 | cy.get('input').type('1');
47 | cy.get('input').clear();
48 | cy.then(() => {
49 | cy.wrap(field.getError('input')).should('deep.equal', ['cant be null']);
50 | });
51 |
52 | const { value: _v2, ...inited2 } = field.init('input', { rules: [] });
53 | cy.mount( );
54 |
55 | const callback = cy.spy();
56 | field.validatePromise(callback);
57 |
58 | cy.wrap(callback).should('be.calledOnce');
59 | });
60 |
61 | it('required - validatePromise', () => {
62 | const field = new Field({});
63 | const { value, ...inited } = field.init('input', {
64 | rules: [
65 | {
66 | required: true,
67 | message: 'cant be null',
68 | },
69 | ],
70 | });
71 |
72 | cy.mount( );
73 | cy.get('input').type('a');
74 | cy.get('input').clear();
75 | cy.then(() => {
76 | cy.wrap(field.getError('input')).should('deep.equal', ['cant be null']);
77 | cy.wrap(field.validatePromise()).should('deep.equal', {
78 | errors: { input: { errors: ['cant be null'] } },
79 | values: { input: '' },
80 | });
81 | });
82 | });
83 |
84 | it('triger', () => {
85 | const field = new Field({});
86 | const { value, ...inited } = field.init('input', {
87 | rules: [
88 | {
89 | required: true,
90 | trigger: 'onBlur',
91 | message: 'cant be null',
92 | },
93 | ],
94 | });
95 |
96 | cy.mount( );
97 | cy.get('input').trigger('blur');
98 | cy.then(() => {
99 | cy.wrap(field.getError('input')).should('deep.equal', ['cant be null']);
100 | });
101 | });
102 | it('validator', () => {
103 | const field = new Field({});
104 | const { value, ...inited } = field.init('input', {
105 | rules: [
106 | {
107 | validator: (rule, value, callback) => {
108 | if (!value) {
109 | callback('不能为空!');
110 | } else {
111 | callback();
112 | }
113 | },
114 | },
115 | ],
116 | });
117 |
118 | cy.mount( );
119 | cy.get('input').type('a');
120 | cy.get('input').clear();
121 | cy.then(() => {
122 | cy.wrap(field.getError('input')).should('deep.equal', ['不能为空!']);
123 | });
124 | });
125 |
126 | it('should reRender while validator callback after 200ms, fix #51', () => {
127 | class Demo extends React.Component {
128 | field = new Field(this);
129 | userExists: Validator = (_rule, value) => {
130 | return new Promise((resolve, reject) => {
131 | if (!value) {
132 | resolve();
133 | } else {
134 | setTimeout(() => {
135 | if (value === 'frank') {
136 | reject([new Error('Sorry name existed')]);
137 | } else {
138 | resolve();
139 | }
140 | }, 100);
141 | }
142 | });
143 | };
144 |
145 | render() {
146 | const { getError, init } = this.field;
147 |
148 | return (
149 |
150 |
157 | {getError('userName')}
158 |
159 | );
160 | }
161 | }
162 |
163 | cy.mount( );
164 | cy.clock();
165 | cy.get('input').type('frank');
166 | cy.tick(200);
167 | cy.get('label').should('have.text', 'Sorry name existed');
168 | });
169 |
170 | it('should rulesProps immutable', () => {
171 | const field = new Field({});
172 | const initRules = {
173 | required: true,
174 | message: 'cant be null',
175 | };
176 | const { value, ...inited } = field.init('input', {
177 | rules: initRules,
178 | });
179 |
180 | cy.mount( );
181 |
182 | const callback = cy.spy();
183 | field.validateCallback(callback);
184 | cy.wrap(initRules).should('not.have.property', 'validator');
185 | });
186 |
187 | it('Should not block validation when component is unmounted while autoUnmount=false.', () => {
188 | const useField = Field.getUseField({
189 | useState: React.useState,
190 | useMemo: React.useMemo,
191 | });
192 | type Ref = {
193 | field: Field;
194 | setVisible: React.Dispatch>;
195 | };
196 | const Demo = forwardRef[((_props, ref) => {
197 | const [visible, setVisible] = React.useState(true);
198 | const field = useField({ autoUnmount: false });
199 | useImperativeHandle(ref, () => ({ setVisible, field }));
200 | if (!visible) {
201 | return null;
202 | }
203 | return (
204 | ]
209 | );
210 | });
211 |
212 | const ref = createRef[();
213 | cy.mount(] ).then(async () => {
214 | cy.wrap(ref.current).should('be.ok');
215 | const { errors } = await ref.current!.field.validatePromise();
216 | cy.wrap(errors).should('be.ok');
217 | ref.current!.setVisible(false);
218 | const { errors: errors2 } = await ref.current!.field.validatePromise();
219 | cy.wrap(errors2).should('not.be.ok');
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": ["../cypress", "./**/*"],
4 | }
5 |
--------------------------------------------------------------------------------
/test/utils.spec.tsx:
--------------------------------------------------------------------------------
1 | import React, { isValidElement, cloneElement, ReactElement } from 'react';
2 | import { assert } from 'chai';
3 | import {
4 | getErrorStrs,
5 | hasIn,
6 | getIn,
7 | setIn,
8 | deleteIn,
9 | cloneToRuleArr,
10 | splitNameToPath,
11 | isOverwritten,
12 | } from '../src/utils';
13 |
14 | function processErrorMessage(element: ReactElement) {
15 | if (element && isValidElement(element)) {
16 | return cloneElement(element, { key: 'error' });
17 | }
18 | return element;
19 | }
20 |
21 | describe('Field Utils', () => {
22 | describe('getErrorStrs', () => {
23 | it('should return `undefined` when given no errors', () => {
24 | assert(getErrorStrs() === undefined);
25 | });
26 |
27 | it('should return array of strings when given error objects', () => {
28 | const errors = [{ message: 'error 1' }, { message: 'error 2' }];
29 | assert.deepEqual(getErrorStrs(errors), ['error 1', 'error 2']);
30 | });
31 |
32 | it('should return array of strings when given string errors', () => {
33 | const errors = ['error 1', 'error 2'];
34 | assert.deepEqual(getErrorStrs(errors), ['error 1', 'error 2']);
35 | });
36 |
37 | it('should return array of React elements with `error` key when no key set using custom `processErrorMessage` function', () => {
38 | const errors = [{ message: message 1 }];
39 | const result = getErrorStrs(errors, processErrorMessage);
40 | assert.equal(result[0].key, 'error');
41 | });
42 |
43 | it('should accept React Elements in `message` key using custom `processErrorMessage` function', () => {
44 | const errors = [{ message: message 1 }, { message: message 2 }];
45 | const result = getErrorStrs(errors, processErrorMessage);
46 | assert.deepEqual(result[0].props.children, 'message 1');
47 | assert.deepEqual(result[1].props.children, 'message 2');
48 | });
49 |
50 | it('should accept React Elements using custom `processErrorMessage` function', () => {
51 | const errors = [message 1 , message 2 ];
52 | const result = getErrorStrs(errors, processErrorMessage);
53 | assert.deepEqual(result[0].props.children, 'message 1');
54 | assert.deepEqual(result[1].props.children, 'message 2');
55 | });
56 | });
57 |
58 | describe('splitNameToPath', () => {
59 | it('basic usage', () => {
60 | assert.deepEqual(splitNameToPath('a'), ['a']);
61 | assert.deepEqual(splitNameToPath('a.b'), ['a', 'b']);
62 | });
63 | it('with array', () => {
64 | assert.deepEqual(splitNameToPath('a[0]'), ['a', '0']);
65 | assert.deepEqual(splitNameToPath('a[0].b'), ['a', '0', 'b']);
66 | assert.deepEqual(splitNameToPath('a.b[0]'), ['a', 'b', '0']);
67 | });
68 | it('should return blank string when is not valid string', () => {
69 | assert(splitNameToPath(undefined) === '');
70 | assert(splitNameToPath(null) === '');
71 | assert(splitNameToPath('') === '');
72 | assert(splitNameToPath({}) === '');
73 | assert(splitNameToPath([]) === '');
74 | });
75 | });
76 |
77 | describe('hasIn', () => {
78 | it('should return false when state is nil', () => {
79 | assert(!hasIn(undefined, 'a.b'));
80 | assert(!hasIn(null, 'a.b'));
81 | });
82 | it('should return false when name is blank or nil', () => {
83 | assert(!hasIn({ a: 1 }, ''));
84 | assert(!hasIn({ a: 1 }, undefined));
85 | assert(!hasIn({ a: 1 }, null));
86 | });
87 | it('should return true when has property', () => {
88 | assert(hasIn({ a: { b: 1 } }, 'a.b'));
89 | });
90 | it('should return false when has no property', () => {
91 | assert(!hasIn({ a: { b: 1 } }, 'a.b.c'));
92 | assert(!hasIn({ a: { b: 1 } }, 'a.c'));
93 | assert(!hasIn({ a: { b: 1 } }, 'a[0]'));
94 | });
95 | });
96 |
97 | describe('getIn', () => {
98 | it('should return state when state is falsy', () => {
99 | assert(getIn(undefined, 'a') === undefined);
100 | });
101 |
102 | it('should return undefind when no name', () => {
103 | assert(getIn({ a: 1 }, '') === undefined);
104 | });
105 |
106 | it('should return undefind when value for name', () => {
107 | assert(getIn({ a: 1 }, 'b') === undefined);
108 | });
109 |
110 | it('should get top level element', () => {
111 | assert(getIn({ a: 1 }, 'a') === 1);
112 | });
113 |
114 | it('should get deep level element', () => {
115 | assert(getIn({ a: { b: { c: 1 } } }, 'a.b.c') === 1);
116 | });
117 |
118 | it('should get array element with dot notation', () => {
119 | assert(getIn({ a: [1, 2] }, 'a.1') === 2);
120 | });
121 |
122 | it('should get array element with bracket notation', () => {
123 | assert(getIn({ a: [1, 2] }, 'a[1]') === 2);
124 | });
125 |
126 | it('should get element that is deep array combination', () => {
127 | assert(getIn({ a: [1, { b: { c: 2 } }] }, 'a[1].b.c') === 2);
128 | });
129 |
130 | it('should return undefined when name is not exists', () => {
131 | // a is not a object
132 | assert(getIn({ a: 1 }, 'a.a') === undefined);
133 | // has not property
134 | assert(getIn({ a: {} }, 'b.c') === undefined);
135 | // is array without the index
136 | assert(getIn({ a: [] }, 'a[0]') === undefined);
137 | });
138 | });
139 |
140 | describe('setIn', () => {
141 | it('should initialize state with object when it is falsy and path is NaN', () => {
142 | assert(setIn(undefined, 'a', 5).a === 5);
143 | });
144 |
145 | it('should initialize state with array when it is falsy and path is NaN', () => {
146 | assert(setIn(undefined, '1', 5)[1] === 5);
147 | });
148 |
149 | it('should initialize state with whole path', () => {
150 | assert(setIn(undefined, 'a.b.c', 5).a.b.c === 5);
151 | });
152 |
153 | it('should not modify state when setting new value', () => {
154 | const state = { a: { b: { c: 1 } } };
155 | setIn(state, 'a.b.c', 5);
156 | assert(state.a.b.c === 1);
157 | });
158 |
159 | it('should duplicate state with new value', () => {
160 | const state = { a: { b: { c: 1 } } };
161 | const newState = setIn(state, 'a.b.c', 5);
162 | assert(newState.a.b.c === 5);
163 | });
164 |
165 | it('should handle array dot notation', () => {
166 | const state = { a: { b: [1, 2] } };
167 | const newState = setIn(state, 'a.b.1', 5);
168 | assert(newState.a.b[1] === 5);
169 | });
170 |
171 | it('should handle array bracket notation', () => {
172 | const state = { a: { b: [1, 2] } };
173 | const newState = setIn(state, 'a.b[1]', 5);
174 | assert(newState.a.b[1] === 5);
175 | });
176 |
177 | it('should add to existing nested object', () => {
178 | const state = { a: { b: 1 } };
179 | const newState = setIn(state, 'a.c.d', 5);
180 | assert(newState.a.c.d === 5);
181 | });
182 |
183 | it('should add to empty object', () => {
184 | const newState = setIn({}, 'a.b.c', 5);
185 | assert(newState.a.b.c === 5);
186 | });
187 | });
188 |
189 | describe('deleteIn', () => {
190 | it('should do nothing when name is not present', () => {
191 | assert.deepEqual(deleteIn({ a: { b: 1 } }, 'x'), { a: { b: 1 } });
192 | });
193 |
194 | it('should do nothing given empty object', () => {
195 | assert.deepEqual(deleteIn({}, 'x'), {});
196 | });
197 |
198 | it('should delete nested element, but leave object', () => {
199 | assert.deepEqual(deleteIn({ a: { b: 1 } }, 'a.b'), { a: {} });
200 | });
201 |
202 | it('should delete top level element, but leave object', () => {
203 | assert.deepEqual(deleteIn({ a: { b: 1 } }, 'a'), {});
204 | });
205 |
206 | it('should delete array element, but not change later indices', () => {
207 | assert.deepEqual(deleteIn({ a: { b: [1, 2, 3] } }, 'a.b.0'), {
208 | a: { b: [undefined, 2, 3] },
209 | });
210 | });
211 | });
212 |
213 | describe('cloneToRuleArr', () => {
214 | it('should return [] when rules is not empty', () => {
215 | assert.deepEqual(cloneToRuleArr(), []);
216 | assert.deepEqual(cloneToRuleArr(undefined), []);
217 | assert.deepEqual(cloneToRuleArr(null), []);
218 | });
219 |
220 | it('should always return array', () => {
221 | assert.deepEqual(cloneToRuleArr({ required: true }), [{ required: true }]);
222 | assert.deepEqual(cloneToRuleArr([{ required: true }]), [{ required: true }]);
223 | });
224 |
225 | it('should return cloned rule array', () => {
226 | const rule: Record = { required: true };
227 | const cloned = cloneToRuleArr(rule);
228 | cloned[0].validate = () => {};
229 | assert(!('validate' in rule));
230 | });
231 | });
232 |
233 | describe('isOverwritten', () => {
234 | it('should return true while name is in values', () => {
235 | assert.equal(isOverwritten({ a: { b: 1 } }, 'a.b'), true);
236 | assert.equal(isOverwritten({ a: { b: {} } }, 'a.b'), true);
237 | assert.equal(isOverwritten({ a: { b: ['b0'] } }, 'a.b[0]'), true);
238 | });
239 | it('should return true while miss the array index', () => {
240 | assert.equal(isOverwritten({ a: { b: ['b0', 'b1'] } }, 'a.b[5]'), true);
241 | assert.equal(isOverwritten({ a: { b: ['b0', 'b1'] } }, 'a.b.5'), true);
242 | });
243 | it('should return false while name is not in values', () => {
244 | assert.equal(isOverwritten({ a: { b: 1 } }, 'a.c'), false);
245 | assert.equal(isOverwritten({ a: { b: 1 } }, 'g'), false);
246 | });
247 |
248 | it('should return true while some paths of name is overwritten by values', () => {
249 | assert.equal(isOverwritten({ a: { b: 1 } }, 'a.b.c'), true);
250 | });
251 | it('should return false while parameters is illegal', () => {
252 | assert.equal(isOverwritten(null, 'a.b'), false);
253 | assert.equal(isOverwritten(undefined, 'a.b'), false);
254 | assert.equal(isOverwritten(0, 'a.b'), false);
255 | assert.equal(isOverwritten('xxx', 'a.b'), false);
256 | assert.equal(isOverwritten({ a: { b: 1 } }, undefined), false);
257 | assert.equal(isOverwritten({ a: { b: 1 } }, null), false);
258 | assert.equal(isOverwritten({ a: { b: 1 } }, 0), false);
259 | assert.equal(isOverwritten({ a: { b: 1 } }, 1), false);
260 | assert.equal(isOverwritten({ a: { b: 1 } }, {}), false);
261 | });
262 | });
263 | });
264 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES5",
5 | "module": "ESNext",
6 | "importHelpers": true,
7 | "declaration": true
8 | },
9 | "include": ["src"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "experimentalDecorators": true,
8 | "allowJs": false,
9 | "declaration": true,
10 | "jsx": "react",
11 | "downlevelIteration": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "skipLibCheck": true,
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 | "isolatedModules": false,
18 | "importHelpers": true,
19 | },
20 | "include": ["src", "test"],
21 | }
22 |
--------------------------------------------------------------------------------
/tsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json"
3 | }
4 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [
7 | react(),
8 | {
9 | name: 'my-polyfill',
10 | config() {
11 | return {
12 | optimizeDeps: {
13 | // include: ["@alifd/validate"]
14 | },
15 | resolve: {
16 | alias: [
17 | {
18 | find: /^moment$/,
19 | replacement: 'moment/moment.js',
20 | },
21 | ],
22 | },
23 | };
24 | },
25 | },
26 | ],
27 | });
28 |
--------------------------------------------------------------------------------