├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.config.json ├── dist ├── index.cjs.js ├── index.esm.js ├── index.umd.js ├── react-formutil.cjs.development.js ├── react-formutil.cjs.production.js ├── react-formutil.esm.development.js ├── react-formutil.esm.production.js ├── react-formutil.umd.development.js └── react-formutil.umd.production.js ├── docs ├── .gitignore ├── .tern-project ├── .tern-webpack-config.js ├── README.md ├── app │ ├── index.js │ └── modules │ │ ├── LoginForm │ │ ├── FieldCity.js │ │ └── index.js │ │ └── SourceCode │ │ ├── index.js │ │ └── style.scss ├── demo │ ├── index.html │ └── static │ │ ├── css │ │ ├── 5.6b7ad16f.css │ │ ├── 6.ea2d060c.css │ │ └── index.88fcec2f.css │ │ ├── images │ │ ├── glyphicons-halflings-regular.448c34a5.woff2 │ │ ├── glyphicons-halflings-regular.89889688.svg │ │ ├── glyphicons-halflings-regular.e18bbf61.ttf │ │ ├── glyphicons-halflings-regular.f4769f9b.eot │ │ └── glyphicons-halflings-regular.fa277232.woff │ │ └── js │ │ ├── 0.8e7ead2f.js │ │ ├── 5.34f0eeb4.js │ │ ├── 6.5034ec00.js │ │ ├── 7.4fb65dc8.js │ │ ├── 8.07f14e87.js │ │ ├── _vendor_.dbf0ba9b.js │ │ ├── index.d1448da7.js │ │ ├── runtime.67bf504e.js │ │ └── vendor.fce07163.js ├── global.d.ts ├── package.json ├── public │ └── index.html ├── scripts │ ├── build.js │ ├── cdn.js │ ├── config │ │ ├── checkMissDependencies.js │ │ ├── env.js │ │ ├── eslintrc.js │ │ ├── helper.js │ │ ├── paths.js │ │ ├── polyfills.js │ │ ├── tslintrc.json │ │ ├── webpack.config.dev.js │ │ └── webpack.config.prod.js │ ├── i18n.js │ └── start.js ├── source │ ├── LoginForm1 │ │ └── index.js │ └── LoginForm2 │ │ └── index.js └── tsconfig.json ├── eslint.config.js ├── hooks.d.ts ├── hooks.js ├── index.d.ts ├── jest ├── cssTransform.js ├── fileTransform.js ├── jest.config.js ├── setupTests.ts └── test.js ├── npm ├── index.cjs.js ├── index.esm.js └── index.umd.js ├── package.json ├── rollup.config.js ├── setupTests.ts ├── src ├── EasyField │ ├── Group.js │ ├── List.js │ ├── Native.js │ ├── easyFieldHandler.js │ └── index.js ├── Field.js ├── Form.js ├── connect.js ├── context.js ├── fieldHelper.js ├── hooks │ ├── Field.js │ ├── useField.js │ ├── useForm.js │ ├── useFormContext.js │ └── useHandler.js ├── index.js ├── utils.js ├── withField.js └── withForm.js ├── tests ├── EasyField.test.tsx ├── Field.test.tsx ├── Form.test.tsx ├── connect.test.tsx ├── dev.dist.test.tsx ├── helper.tsx ├── prod.dist.test.tsx ├── useField.test.tsx ├── utils.test.ts ├── withField.test.tsx └── withForm.test.tsx └── tsconfig.json /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 1 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript', 'typescript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | .DS_Store 3 | node_modules 4 | /package-lock.json 5 | /coverage 6 | /.git-tsconfig.json 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | src/ 3 | npm/ 4 | jest/ 5 | coverage/ 6 | tests/ 7 | *.un~ 8 | /jsconfig.json 9 | /tsconfig.json 10 | /rollup.* 11 | /babel.config.json 12 | /eslint.config.js 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 qiqiboy 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 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | "react-hot-loader/babel", 5 | [ 6 | "@babel/plugin-proposal-decorators", 7 | { 8 | "legacy": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /dist/index.cjs.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.cjs.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.cjs.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /dist/index.esm.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.esm.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.esm.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.umd.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.umd.development.js'); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | .*.swp 3 | .*.swo 4 | *.rdb 5 | .DS_Store 6 | node_modules 7 | bower_components 8 | /npm-debug.log 9 | /dump.rdb 10 | Thumbs.db 11 | 12 | # git pre-commit tsc lint 13 | .git-tsconfig.json -------------------------------------------------------------------------------- /docs/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": [ 3 | "browser", 4 | "jquery", 5 | "ecma5", 6 | "ecma6", 7 | "underscore", 8 | "angular" 9 | ], 10 | "plugins": { 11 | "angular": {}, 12 | "node": {}, 13 | "es_modules": {}, 14 | "commonjs": {}, 15 | "webpack": { 16 | "configPath": "./.tern-webpack-config.js" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/.tern-webpack-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin'); 3 | 4 | module.exports = { 5 | resolve: { 6 | modules: [path.resolve(__dirname, 'node_modules'), path.resolve(__dirname)], 7 | alias: { 8 | utils: path.resolve(__dirname, 'app/utils'), 9 | components: path.resolve(__dirname, 'app/components'), 10 | modules: path.resolve(__dirname, 'app/modules') 11 | }, 12 | plugins: [ 13 | new DirectoryNamedWebpackPlugin({ 14 | honorIndex: true, 15 | exclude: /node_modules|libs/ 16 | }) 17 | ] 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 支持React + ES6的标准开发环境 2 | ================ 3 | 4 | ### 开始开发 5 | $ npm start 6 | 7 | 该命令会在本地通过`webpack-dev-server`创建一个本地开发服务,支持hot reload. 8 | 9 | ### 部署测试 10 | $ npm run build:dev 11 | 12 | ### 上线 13 | $ npm run pack 14 | 15 | -------------------------------------------------------------------------------- /docs/app/index.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-webpack-loader-syntax: 0*/ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import SourceCode from 'modules/SourceCode'; 5 | import { HashRouter, Switch, Route, Redirect, NavLink as Link } from 'react-router-dom'; 6 | import 'bootstrap-sass/assets/stylesheets/_bootstrap.scss'; 7 | 8 | import LoginForm from 'modules/LoginForm'; 9 | 10 | const source1 = require('!!raw-loader!source/LoginForm1').default; 11 | const source2 = require('!!raw-loader!source/LoginForm2').default; 12 | const $error = {}; 13 | 14 | for (let name in $error) { 15 | if ($error.hasOwenProperty(name)) { 16 | console.log(name); 17 | } 18 | } 19 | 20 | render( 21 | 22 |
23 |
24 |

25 | react-formutil{' '} 26 | 27 | Github 28 | 29 |

30 |

31 | 这个例子将展示如何使用bootstrap和react-formutil快速制作一个登表单页面。示例包含了普通的文本输入、下拉框、双密码输入框(一致校验)、多项选择、多级联动等常见表单形式。 32 | { 36 | ev.preventDefault(); 37 | 38 | document.querySelector('#source-code').scrollIntoView({ 39 | behavior: 'smooth', 40 | block: 'start' 41 | }); 42 | }}> 43 | 查看源代码 44 | 45 |

46 | 47 |
48 | 49 |
50 |
51 |
有两种调用方式,你可以点击切换查看不同的实现代码
52 |
53 | 54 | 源码一 55 | 56 | 57 | 源码二 58 | 59 |
60 |
61 | 62 | } /> 63 | } /> 64 | 65 | 66 |
67 |
68 |
, 69 | document.getElementById('wrap') 70 | ); 71 | -------------------------------------------------------------------------------- /docs/app/modules/LoginForm/FieldCity.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withForm, withField, Field } from 'app/../../src'; 3 | 4 | const provinceData = [ 5 | { 6 | id: 'beijing', 7 | name: '北京', 8 | children: [ 9 | { 10 | id: 'chaoyang', 11 | name: '朝阳区' 12 | }, 13 | { 14 | id: 'haidian', 15 | name: '海淀区' 16 | } 17 | ] 18 | }, 19 | { 20 | id: 'shanghai', 21 | name: '上海', 22 | children: [ 23 | { 24 | id: 'pudong', 25 | name: '浦东新区' 26 | }, 27 | { 28 | id: 'jingan', 29 | name: '静安区' 30 | } 31 | ] 32 | }, 33 | { 34 | id: 'guangdong', 35 | name: '广东', 36 | children: [ 37 | { 38 | id: 'guangzhou', 39 | name: '广州市' 40 | }, 41 | { 42 | id: 'shenzhen', 43 | name: '深圳市' 44 | } 45 | ] 46 | } 47 | ]; 48 | 49 | class FieldCity extends Component { 50 | state = { 51 | loading: true 52 | }; 53 | 54 | // 模拟异步获取省份信息 55 | getProvinceData = () => { 56 | this.setState({ 57 | loading: true 58 | }); 59 | 60 | setTimeout(() => { 61 | this.setState({ 62 | provinceData, 63 | loading: false 64 | }); 65 | 66 | // 获取到省份信息后,如果有已经选择了省份,需要再去获取下城市列表 67 | if (this.props.$formutil.$params.province) { 68 | this.getCityData(this.props.$formutil.$params.province); 69 | } 70 | }, 1500); 71 | }; 72 | 73 | // 模拟异步获取城市信息 74 | getCityData = id => { 75 | if (id) { 76 | this.setState({ 77 | loading: true 78 | }); 79 | 80 | setTimeout( 81 | () => 82 | this.setState({ 83 | cityData: provinceData.find(item => item.id === id).children, 84 | loading: false 85 | }), 86 | 1500 87 | ); 88 | } 89 | }; 90 | 91 | onProvinceChange = () => { 92 | this.setState({ 93 | cityData: null 94 | }); 95 | 96 | this.props.$formutil.$setValues( 97 | { 98 | city: '' 99 | }, 100 | () => { 101 | this.onCityChange(); 102 | } 103 | ); 104 | 105 | if (this.props.$formutil.$params.province) { 106 | this.getCityData(this.props.$formutil.$params.province); 107 | } 108 | }; 109 | 110 | onCityChange = () => { 111 | if (this.props.$formutil.$invalid) { 112 | this.props.$fieldutil.$render(null); 113 | } else { 114 | this.props.$fieldutil.$render(this.props.$formutil.$params); 115 | } 116 | }; 117 | 118 | fieldProps = { 119 | required: true, 120 | $validators: { 121 | required: Boolean 122 | } 123 | }; 124 | 125 | componentDidMount() { 126 | this.getProvinceData(); 127 | 128 | if (this.props.$fieldutil.$value) { 129 | this.props.$formutil.$setValues(this.props.$fieldutil.$value); 130 | } 131 | } 132 | 133 | render() { 134 | const { provinceData, cityData, loading } = this.state; 135 | const { province = '', city = '' } = this.props.$fieldutil.$value || this.props.$formutil.$params; 136 | 137 | return ( 138 |
139 |
140 | 141 | {props => ( 142 | 154 | )} 155 | 156 |
157 |
158 | 159 | {props => ( 160 | 172 | )} 173 | 174 |
175 |
176 | ); 177 | } 178 | } 179 | 180 | // 这里先包装了withForm,方便可以获取到用户填写信息,再包装withField,是因为我们对外暴漏为一个Field 181 | export default withField(withForm(FieldCity), { 182 | $defaultValue: null 183 | }); 184 | -------------------------------------------------------------------------------- /docs/app/modules/SourceCode/index.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-webpack-loader-syntax: 0*/ 2 | import React, { Component } from 'react'; 3 | import './style.scss'; 4 | 5 | class SrouceCode extends Component { 6 | async componentDidMount() { 7 | const [{ default: Editor }] = await Promise.all([ 8 | import('codemirror'), // 异步载入编辑器代码 9 | import('codemirror/mode/javascript/javascript'), 10 | import('codemirror/mode/css/css'), 11 | import('codemirror/lib/codemirror.css'), // 载入编辑器样式 12 | import('codemirror/theme/solarized.css') // 载入编辑器主题 13 | ]); 14 | 15 | this.jsEditor = new Editor(this.editorNode, { 16 | mode: 'javascript', 17 | lineNumbers: true, 18 | theme: 'solarized', 19 | value: this.props.source, 20 | readOnly: true 21 | }); 22 | } 23 | 24 | refCallback = node => { 25 | this.editorNode = node; 26 | }; 27 | 28 | render() { 29 | return ( 30 |
31 |

源代码

32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default SrouceCode; 39 | -------------------------------------------------------------------------------- /docs/app/modules/SourceCode/style.scss: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | border: 1px solid #eee; 3 | height: auto !important; 4 | } 5 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | react-formutil
-------------------------------------------------------------------------------- /docs/demo/static/css/5.6b7ad16f.css: -------------------------------------------------------------------------------- 1 | .CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5)}.cm-animate-fat-cursor,.cm-fat-cursor-mark{-webkit-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;background-color:#7e7}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:none;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;-webkit-font-feature-settings:"calt";font-feature-settings:"calt";font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none} -------------------------------------------------------------------------------- /docs/demo/static/css/6.ea2d060c.css: -------------------------------------------------------------------------------- 1 | .solarized.base03{color:#002b36}.solarized.base02{color:#073642}.solarized.base01{color:#586e75}.solarized.base00{color:#657b83}.solarized.base0{color:#839496}.solarized.base1{color:#93a1a1}.solarized.base2{color:#eee8d5}.solarized.base3{color:#fdf6e3}.solarized.solar-yellow{color:#b58900}.solarized.solar-orange{color:#cb4b16}.solarized.solar-red{color:#dc322f}.solarized.solar-magenta{color:#d33682}.solarized.solar-violet{color:#6c71c4}.solarized.solar-blue{color:#268bd2}.solarized.solar-cyan{color:#2aa198}.solarized.solar-green{color:#859900}.cm-s-solarized{line-height:1.45em;color-profile:sRGB;rendering-intent:auto}.cm-s-solarized.cm-s-dark{color:#839496;background-color:#002b36;text-shadow:#002b36 0 1px}.cm-s-solarized.cm-s-light{background-color:#fdf6e3;color:#657b83;text-shadow:#eee8d5 0 1px}.cm-s-solarized .CodeMirror-widget{text-shadow:none}.cm-s-solarized .cm-header{color:#586e75}.cm-s-solarized .cm-quote{color:#93a1a1}.cm-s-solarized .cm-keyword{color:#cb4b16}.cm-s-solarized .cm-atom,.cm-s-solarized .cm-number{color:#d33682}.cm-s-solarized .cm-def{color:#2aa198}.cm-s-solarized .cm-variable{color:#839496}.cm-s-solarized .cm-variable-2{color:#b58900}.cm-s-solarized .cm-type,.cm-s-solarized .cm-variable-3{color:#6c71c4}.cm-s-solarized .cm-property{color:#2aa198}.cm-s-solarized .cm-operator{color:#6c71c4}.cm-s-solarized .cm-comment{color:#586e75;font-style:italic}.cm-s-solarized .cm-string{color:#859900}.cm-s-solarized .cm-string-2{color:#b58900}.cm-s-solarized .cm-meta{color:#859900}.cm-s-solarized .cm-qualifier{color:#b58900}.cm-s-solarized .cm-builtin{color:#d33682}.cm-s-solarized .cm-bracket{color:#cb4b16}.cm-s-solarized .CodeMirror-matchingbracket{color:#859900}.cm-s-solarized .CodeMirror-nonmatchingbracket{color:#dc322f}.cm-s-solarized .cm-tag{color:#93a1a1}.cm-s-solarized .cm-attribute{color:#2aa198}.cm-s-solarized .cm-hr{color:transparent;border-top:1px solid #586e75;display:block}.cm-s-solarized .cm-link{color:#93a1a1;cursor:pointer}.cm-s-solarized .cm-special{color:#6c71c4}.cm-s-solarized .cm-em{color:#999;text-decoration:underline;-webkit-text-decoration-style:dotted;text-decoration-style:dotted}.cm-s-solarized .cm-error,.cm-s-solarized .cm-invalidchar{color:#586e75;border-bottom:1px dotted #dc322f}.cm-s-solarized.cm-s-dark div.CodeMirror-selected{background:#073642}.cm-s-solarized.cm-s-dark.CodeMirror ::selection{background:rgba(7,54,66,.99)}.cm-s-dark .CodeMirror-line>span::-moz-selection,.cm-s-dark .CodeMirror-line>span>span::-moz-selection,.cm-s-solarized.cm-s-dark .CodeMirror-line::-moz-selection{background:rgba(7,54,66,.99)}.cm-s-solarized.cm-s-light div.CodeMirror-selected{background:#eee8d5}.cm-s-light .CodeMirror-line>span::selection,.cm-s-light .CodeMirror-line>span>span::selection,.cm-s-solarized.cm-s-light .CodeMirror-line::selection{background:#eee8d5}.cm-s-ligh .CodeMirror-line>span::-moz-selection,.cm-s-ligh .CodeMirror-line>span>span::-moz-selection,.cm-s-solarized.cm-s-light .CodeMirror-line::-moz-selection{background:#eee8d5}.cm-s-solarized.CodeMirror{box-shadow:inset 7px 0 12px -6px #000}.cm-s-solarized .CodeMirror-gutters{border-right:0}.cm-s-solarized.cm-s-dark .CodeMirror-gutters{background-color:#073642}.cm-s-solarized.cm-s-dark .CodeMirror-linenumber{color:#586e75;text-shadow:#021014 0 -1px}.cm-s-solarized.cm-s-light .CodeMirror-gutters{background-color:#eee8d5}.cm-s-solarized.cm-s-light .CodeMirror-linenumber{color:#839496}.cm-s-solarized .CodeMirror-linenumber{padding:0 5px}.cm-s-solarized .CodeMirror-guttermarker-subtle{color:#586e75}.cm-s-solarized.cm-s-dark .CodeMirror-guttermarker{color:#ddd}.cm-s-solarized.cm-s-light .CodeMirror-guttermarker{color:#cb4b16}.cm-s-solarized .CodeMirror-gutter .CodeMirror-gutter-text{color:#586e75}.cm-s-solarized .CodeMirror-cursor{border-left:1px solid #819090}.cm-s-solarized.cm-s-light.cm-fat-cursor .CodeMirror-cursor{background:#7e7}.cm-s-solarized.cm-s-light .cm-animate-fat-cursor{background-color:#7e7}.cm-s-solarized.cm-s-dark.cm-fat-cursor .CodeMirror-cursor{background:#586e75}.cm-s-solarized.cm-s-dark .cm-animate-fat-cursor{background-color:#586e75}.cm-s-solarized.cm-s-dark .CodeMirror-activeline-background{background:hsla(0,0%,100%,.06)}.cm-s-solarized.cm-s-light .CodeMirror-activeline-background{background:rgba(0,0,0,.06)} -------------------------------------------------------------------------------- /docs/demo/static/images/glyphicons-halflings-regular.448c34a5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.448c34a5.woff2 -------------------------------------------------------------------------------- /docs/demo/static/images/glyphicons-halflings-regular.e18bbf61.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.e18bbf61.ttf -------------------------------------------------------------------------------- /docs/demo/static/images/glyphicons-halflings-regular.f4769f9b.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.f4769f9b.eot -------------------------------------------------------------------------------- /docs/demo/static/images/glyphicons-halflings-regular.fa277232.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiqiboy/react-formutil/d531c1b6998c3285a87074eefd731943ec3f2a51/docs/demo/static/images/glyphicons-halflings-regular.fa277232.woff -------------------------------------------------------------------------------- /docs/demo/static/js/5.34f0eeb4.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonp=this.webpackJsonp||[]).push([[5],{HoVT:function(p,s,n){}}]); -------------------------------------------------------------------------------- /docs/demo/static/js/6.5034ec00.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonp=this.webpackJsonp||[]).push([[6],{lNMr:function(p,s,n){}}]); -------------------------------------------------------------------------------- /docs/demo/static/js/_vendor_.dbf0ba9b.js: -------------------------------------------------------------------------------- 1 | /*! @author qiqiboy */ 2 | (this.webpackJsonp=this.webpackJsonp||[]).push([[3],[],[[0,1,2]]]); -------------------------------------------------------------------------------- /docs/demo/static/js/runtime.67bf504e.js: -------------------------------------------------------------------------------- 1 | /*! @author qiqiboy */!function(e){function t(t){for(var n,o,u=t[0],c=t[1],s=t[2],f=0,d=[];f = Pick>; 21 | 22 | /** 23 | * create HOC(Higher Order Component) 24 | * 25 | */ 26 | type HOC = ( 27 | Component: React.ComponentType 28 | ) => React.ComponentType>; 29 | 30 | /** 31 | * i18n 32 | */ 33 | type I18nFunc = (key: string) => string; 34 | 35 | declare const __: I18nFunc; 36 | 37 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-formutil-demo", 3 | "author": "qiqiboy", 4 | "version": "1.0.0", 5 | "private": true, 6 | "vendor": [ 7 | "react", 8 | "react-dom" 9 | ], 10 | "noRewrite": true, 11 | "proxy": null, 12 | "scripts": { 13 | "start": "node scripts/start.js", 14 | "build": "node scripts/build.js", 15 | "build:dev": "node scripts/build.js --dev", 16 | "pack": "npm run build", 17 | "count": "node scripts/count.js", 18 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts', 'app/utils/i18n/*'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --checkJs false" 19 | }, 20 | "babel": { 21 | "presets": [ 22 | "react-app" 23 | ], 24 | "plugins": [ 25 | [ 26 | "@babel/plugin-proposal-decorators", 27 | { 28 | "legacy": true 29 | } 30 | ] 31 | ] 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "./scripts/config/eslintrc.js" 37 | ] 38 | }, 39 | "prettier": { 40 | "printWidth": 120, 41 | "tabWidth": 4, 42 | "parser": "babylon", 43 | "trailingComma": "none", 44 | "jsxBracketSameLine": true, 45 | "semi": true, 46 | "singleQuote": true, 47 | "overrides": [ 48 | { 49 | "files": "*.json", 50 | "options": { 51 | "tabWidth": 2 52 | } 53 | } 54 | ] 55 | }, 56 | "lint-staged": { 57 | "{app,static}/**/*.{js,jsx,mjs,css,scss,less,json,ts}": [ 58 | "node_modules/.bin/prettier --write", 59 | "git add" 60 | ] 61 | }, 62 | "dependencies": { 63 | "axios": "0.17.0", 64 | "bootstrap-sass": "3.4.1", 65 | "codemirror": "5.38.0", 66 | "jquery": "3.1.1", 67 | "lodash": "4.17.5", 68 | "normalize.css": "5.0.0", 69 | "prop-types": "15.7.2", 70 | "react-animated-router": "0.1.11", 71 | "react-awesome-snippets": "0.0.18", 72 | "react-router-dom": "5.2.0", 73 | "react-transition-group": "2.3.0" 74 | }, 75 | "devDependencies": { 76 | "@babel/core": "7.8.6", 77 | "@babel/plugin-proposal-decorators": "7.8.3", 78 | "@commitlint/cli": "7.6.1", 79 | "@commitlint/config-conventional": "7.6.0", 80 | "@svgr/webpack": "4.3.3", 81 | "@types/react-router-dom": "5.1.3", 82 | "@types/react-transition-group": "4.2.4", 83 | "@typescript-eslint/eslint-plugin": "2.22.0", 84 | "@typescript-eslint/parser": "2.22.0", 85 | "ali-oss": "6.5.1", 86 | "autoprefixer": "7.1.6", 87 | "babel-loader": "8.0.6", 88 | "babel-plugin-named-asset-import": "0.3.6", 89 | "babel-runtime": "6.26.0", 90 | "case-sensitive-paths-webpack-plugin": "2.2.0", 91 | "chalk": "3.0.0", 92 | "check-dependencies": "1.1.0", 93 | "classlist-polyfill": "1.2.0", 94 | "core-js": "3.6.4", 95 | "css-loader": "3.4.2", 96 | "cz-conventional-changelog": "2.1.0", 97 | "detect-port": "1.2.1", 98 | "directory-named-webpack-plugin": "4.0.1", 99 | "dotenv": "8.2.0", 100 | "dotenv-expand": "5.1.0", 101 | "eslint": "7.14.0", 102 | "eslint-config-react-app": "6.0.0", 103 | "eslint-loader": "4.0.2", 104 | "eslint-plugin-flowtype": "4.6.0", 105 | "eslint-plugin-import": "2.22.1", 106 | "eslint-plugin-jsx-a11y": "6.4.1", 107 | "eslint-plugin-react": "7.21.5", 108 | "eslint-plugin-react-hooks": "2.5.0", 109 | "file-loader": "4.3.0", 110 | "fork-ts-checker-webpack-plugin-alt": "0.4.14", 111 | "fs-extra": "8.1.0", 112 | "glob": "7.1.6", 113 | "html-loader": "1.0.0-alpha.0", 114 | "html-webpack-plugin": "4.0.0-beta.11", 115 | "husky": "3.1.0", 116 | "i18next-scanner": "2.10.3", 117 | "imagemin-webpack-plugin": "2.4.2", 118 | "inline-chunk-manifest-html-webpack-plugin": "2.0.0", 119 | "less": "3.11.1", 120 | "less-loader": "5.0.0", 121 | "lint-staged": "9.5.0", 122 | "mini-css-extract-plugin": "0.9.0", 123 | "node-xlsx": "0.15.0", 124 | "optimize-css-assets-webpack-plugin": "5.0.3", 125 | "ora": "4.0.3", 126 | "postcss-flexbugs-fixes": "4.2.0", 127 | "postcss-loader": "3.0.0", 128 | "postcss-preset-env": "6.7.0", 129 | "postcss-safe-parser": "4.0.2", 130 | "prettier": "1.19.1", 131 | "raf-dom": "1.1.0", 132 | "raw-loader": "3.1.0", 133 | "react-dev-utils": "10.1.0", 134 | "rsync": "0.6.1", 135 | "sass": "1.26.2", 136 | "sass-loader": "8.0.2", 137 | "style-loader": "1.1.3", 138 | "stylelint": "12.0.1", 139 | "stylelint-config-recommended": "3.0.0", 140 | "sw-precache-webpack-plugin": "0.11.5", 141 | "terser-webpack-plugin": "2.3.5", 142 | "typescript": "4.9.5", 143 | "uglifyjs-webpack-plugin": "1.0.1", 144 | "webpack": "4.42.0", 145 | "webpack-dev-server": "3.10.3" 146 | }, 147 | "husky": { 148 | "hooks": { 149 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --name-only --diff-filter AM --relative --staged | grep -E '.tsx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc; fi", 150 | "pre-push": "CF=$(git diff --diff-filter AM --name-only @{u}..) || CF=$(git diff --diff-filter AM --name-only origin/master...HEAD); FILES=$(echo \"$CF\" | grep -E '^app/.*\\.m?[jt]sx?$'); if [ -n \"$FILES\" ]; then node_modules/.bin/eslint $FILES --max-warnings 0; fi" 151 | } 152 | }, 153 | "stylelint": { 154 | "extends": "stylelint-config-recommended" 155 | }, 156 | "browserslist": [ 157 | ">0.2%", 158 | "not dead", 159 | "not op_mini all", 160 | "ie > 10" 161 | ], 162 | "config": { 163 | "commitizen": { 164 | "path": "cz-conventional-changelog" 165 | } 166 | }, 167 | "engines": { 168 | "node": ">=8.10.0", 169 | "tiger-new": "4.3.10" 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | react-formutil 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | 3 | const useDevConfig = process.argv[2] === '--dev'; 4 | 5 | process.env.BABEL_ENV = useDevConfig ? 'development' : 'production'; 6 | process.env.NODE_ENV = useDevConfig ? 'development' : 'production'; 7 | 8 | process.env.WEBPACK_BUILDING = 'true'; 9 | 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | require('./config/env'); 15 | 16 | const chalk = require('chalk'); 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | const webpack = require('webpack'); 20 | const config = require(useDevConfig ? './config/webpack.config.dev' : './config/webpack.config.prod'); 21 | const paths = require('./config/paths'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 24 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 25 | const clearConsole = require('react-dev-utils/clearConsole'); 26 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 27 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 28 | const checkMissDependencies = require('./config/checkMissDependencies'); 29 | const { ensureLocals } = require('./i18n'); 30 | const ora = require('ora'); 31 | 32 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 33 | console.log(); 34 | process.exit(1); 35 | } 36 | 37 | ensureLocals(); 38 | 39 | const spinner = ora('webpack启动中...').start(); 40 | // These sizes are pretty large. We'll warn for bundles exceeding them. 41 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 42 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 43 | 44 | checkMissDependencies(spinner) 45 | .then(() => { 46 | return measureFileSizesBeforeBuild(paths.appBuild); 47 | }) 48 | .then(previousFileSizes => { 49 | // Remove all content but keep the directory 50 | fs.emptyDirSync(paths.appBuild); 51 | // Merge with the public folder 52 | copyPublicFolder(); 53 | // Start the webpack build 54 | return build(previousFileSizes); 55 | }) 56 | .then(({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | spinner.warn(chalk.yellow('编译有警告产生:')); 59 | console.log(); 60 | console.log(warnings.join('\n\n')); 61 | console.log(); 62 | // Teach some ESLint tricks. 63 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.'); 64 | 65 | console.log( 66 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n' 67 | ); 68 | 69 | console.log(); 70 | console.log(); 71 | 72 | if (!useDevConfig) { 73 | spinner.fail(chalk.red('请处理所有的错误和警告后再build代码!')); 74 | 75 | console.log(); 76 | console.log(); 77 | process.exit(1); 78 | } 79 | } else { 80 | spinner.succeed(chalk.green('编译通过!!')); 81 | console.log(); 82 | } 83 | 84 | spinner.succeed('gzip后的文件大小:'); 85 | console.log(); 86 | 87 | printFileSizesAfterBuild( 88 | stats, 89 | previousFileSizes, 90 | paths.appBuild, 91 | WARN_AFTER_BUNDLE_GZIP_SIZE, 92 | WARN_AFTER_CHUNK_GZIP_SIZE 93 | ); 94 | 95 | console.log(); 96 | 97 | if (/^http/.test(config.output.publicPath) === false) { 98 | spinner.succeed(chalk.green('项目打包完成,运行以下命令可即时预览:')); 99 | console.log(); 100 | 101 | if (!paths.serve) { 102 | console.log(chalk.cyan('npm') + ' install -g serve'); 103 | } 104 | 105 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild)); 106 | } else { 107 | const publicPath = config.output.publicPath; 108 | 109 | spinner.succeed('项目打包完成,请确保资源已上传到:' + chalk.green(publicPath) + '.'); 110 | } 111 | 112 | console.log(); 113 | }) 114 | .catch(err => { 115 | if (err) { 116 | spinner.fail(chalk.red('编译失败!!')); 117 | console.log(); 118 | console.log(err.message || err); 119 | } 120 | 121 | console.log(); 122 | console.log(); 123 | process.exit(1); 124 | }); 125 | 126 | // Create the production build and print the deployment instructions. 127 | function build(previousFileSizes) { 128 | let packText = useDevConfig ? '启动测试环境打包编译...' : '启动生产环境打包压缩...'; 129 | let startTime = Date.now(); 130 | let timer; 131 | let logProgress = function(stop) { 132 | var text = packText + '已耗时:' + ((Date.now() - startTime) / 1000).toFixed(3) + 's'; 133 | 134 | if (stop) { 135 | clearTimeout(timer); 136 | spinner.succeed(chalk.green(text)); 137 | } else { 138 | spinner.text = chalk.cyan(text); 139 | 140 | timer = setTimeout(logProgress, 100); 141 | } 142 | }; 143 | 144 | clearConsole(); 145 | logProgress(); 146 | 147 | let compiler = webpack(config); 148 | 149 | return new Promise((resolve, reject) => { 150 | compiler.run((err, stats) => { 151 | let messages; 152 | 153 | logProgress(true); // 停止 154 | console.log(); 155 | 156 | if (err) { 157 | if (!err.message) { 158 | return reject(err); 159 | } 160 | 161 | messages = formatWebpackMessages({ 162 | errors: [err.message], 163 | warnings: [] 164 | }); 165 | } else { 166 | messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true })); 167 | } 168 | 169 | if (messages.errors.length) { 170 | if (messages.errors.length > 1) { 171 | messages.errors.length = 1; 172 | } 173 | 174 | return reject(new Error(messages.errors.join('\n\n'))); 175 | } 176 | 177 | const resolveArgs = { 178 | stats, 179 | previousFileSizes, 180 | warnings: messages.warnings 181 | }; 182 | 183 | return resolve(resolveArgs); 184 | }); 185 | }); 186 | } 187 | 188 | function copyPublicFolder() { 189 | fs.copySync(paths.appPublic, paths.appBuild, { 190 | dereference: true, 191 | filter: file => { 192 | const relative = path.relative(paths.appPublic, file); 193 | const basename = path.basename(file); 194 | const isDirectory = fs.statSync(file).isDirectory(); 195 | 196 | return isDirectory 197 | ? basename !== 'layout' // layout目录不复制 198 | : !paths.pageEntries.find(name => name + '.html' === relative); 199 | } 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /docs/scripts/cdn.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const Rsync = require('rsync'); 5 | const OSS = require('ali-oss'); 6 | const chalk = require('chalk'); 7 | const lodash = require('lodash'); 8 | const glob = require('glob'); 9 | const paths = require('./config/paths'); 10 | const ora = require('ora'); 11 | const pkg = require(paths.appPackageJson); 12 | 13 | const staticFileName = 'static.config.json'; 14 | const staticConfigFile = path.resolve(paths.root, staticFileName); 15 | const oldStaticConfig = lodash.invert(getStaticConfig(staticConfigFile)); 16 | const newStaticConfig = {}; 17 | 18 | const spinner = ora(); 19 | let throttleDelay = 0; 20 | 21 | if (process.env.SKIP_CDN === 'true') { 22 | spinner.info(chalk.cyan('本次构建忽略CDN任务')); 23 | } else if (pkg.cdn) { 24 | if ('server' in pkg.cdn || 'ali-oss' in pkg.cdn) { 25 | runCDN(); 26 | } else { 27 | spinner.fail(chalk.red(`未发现CDN服务连接配置信息!`)); 28 | spinner.fail(chalk.red(`如果不需要CDN服务,您可以移除 ${chalk.cyan('package.json')} 中的 cdn 字段;`)); 29 | spinner.fail(chalk.red(`或者,请根据下述信息在 ${chalk.cyan('package.json')} 中补充相关配置:`)); 30 | 31 | console.log( 32 | chalk.grey(` 33 | 支持两种cdn配置方式,分别需要在 ${chalk.cyan('package.json')} 中配置相关的cdn字段: 34 | 35 | { 36 | "name": "${pkg.name}", 37 | "version": "${pkg.version}", 38 | "cdn": { 39 | "host": "${pkg.cdn.host || 'https://xxx.com'}", 40 | "path": "${pkg.cdn.path || '/xxx'}", 41 | ${chalk.cyan(`"server": "host:path", 42 | "ali-oss": { 43 | ... 44 | }`)} 45 | }, 46 | ... 47 | } 48 | 49 | server和ali-oss字段必选其一配置,别对对应下述两种cdn配置方式: 50 | 51 | 1. 阿里云的OSS存储服务,对应ali-oss配置(具体需要配置的内容可以参考阿里云文档) 52 | 2. 通过ssh的rsync命令传到源服务器上,对应server字段配置,即rsync命令的目标服务器与路径,例如:BEIJING_HOST:/data0/webservice/static 53 | `) // ` 54 | ); 55 | } 56 | } else { 57 | spinner.info(chalk.cyan('未发现CDN配置信息,已跳过')); 58 | } 59 | 60 | function getStaticConfig(path) { 61 | try { 62 | return require(path) || {}; 63 | } catch (e) { 64 | return {}; 65 | } 66 | } 67 | 68 | function removeFileNameHash(fileName) { 69 | const pipes = fileName.split('.'); 70 | 71 | pipes.splice(-2, 1); 72 | return pipes.join('.'); 73 | } 74 | 75 | function runCDN() { 76 | throttleDelay = 0; 77 | 78 | spinner.start('开始上传'); 79 | 80 | let exitsNum = 0; 81 | const useOSS = !!pkg.cdn['ali-oss']; 82 | const allFiles = glob.sync(path.join(paths.appBuild, 'static/**/!(*.map)')); 83 | const allSyncPromises = allFiles 84 | .filter(function(file) { 85 | const relative = path.relative(paths.appBuild, file); 86 | 87 | // 文件夹不处理 88 | if (fs.statSync(file).isDirectory()) { 89 | return false; 90 | } 91 | 92 | newStaticConfig[/js$|css$/.test(relative) ? removeFileNameHash(relative) : relative] = relative; 93 | 94 | // 已经存在 95 | if (oldStaticConfig[relative]) { 96 | spinner.succeed(chalk.green('已存在:' + relative)); 97 | exitsNum++; 98 | return false; 99 | } 100 | 101 | return true; 102 | }) 103 | .map(useOSS ? createOSS : createRsync); 104 | 105 | Promise.all(allSyncPromises).then(function(rets) { 106 | let uploadNum = rets.filter(Boolean).length; 107 | let failNum = rets.length - uploadNum; 108 | 109 | console.log(); 110 | 111 | console.log( 112 | chalk[failNum ? 'red' : 'cyan']( 113 | '+++++++++++++++++++++++++++++++\n 文件上传完毕(' + 114 | chalk.blue(pkg.cdn.path) + 115 | ') \n ' + 116 | chalk.magenta('成功: ' + uploadNum) + 117 | ' \n ' + 118 | chalk.red('失败: ' + failNum) + 119 | ' \n ' + 120 | chalk.green('重复: ' + exitsNum) + 121 | '\n+++++++++++++++++++++++++++++++' 122 | ) 123 | ); 124 | 125 | if (!failNum) { 126 | fs.outputFileSync(staticConfigFile, JSON.stringify(newStaticConfig, '\n', 2)); 127 | console.log(chalk.blue('配置文件已经更新: ' + staticConfigFile)); 128 | console.log(); 129 | console.log(chalk.green('项目已经成功编译,运行以下命令可即时预览:')); 130 | 131 | if (!paths.serve) { 132 | console.log(chalk.cyan('npm') + ' install -g serve'); 133 | } 134 | 135 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild)); 136 | } else { 137 | console.log(chalk.red('文件未全部上传,请单独运行') + chalk.green(' npm run cdn ') + chalk.red('命令!')); 138 | process.exit(1); 139 | } 140 | 141 | console.log(); 142 | }); 143 | } 144 | 145 | function createRsync(file) { 146 | return new Promise(resolve => { 147 | setTimeout(() => { 148 | const rsync = new Rsync(); 149 | const relative = path.relative(paths.appBuild, file); 150 | 151 | rsync.cwd(paths.appBuild); 152 | 153 | rsync 154 | .flags('Rz') // 相对路径上传、压缩 155 | .source(relative) 156 | .destination(path.join(pkg.cdn.server || 'static:/data0/webservice/static', pkg.cdn.path)) 157 | .execute(function(error, code, cmd) { 158 | if (error) { 159 | resolve(false); 160 | spinner.fail(chalk.red('上传失败(' + error + '):' + relative)); 161 | } else { 162 | resolve(true); 163 | spinner.warn(chalk.yellow('已上传:' + relative)); 164 | } 165 | }); 166 | }, 200 * throttleDelay++); 167 | }); 168 | } 169 | 170 | function createOSS(file) { 171 | return new Promise(resolve => { 172 | setTimeout(() => { 173 | const client = new OSS(pkg.cdn['ali-oss']); 174 | const objectName = path.relative(paths.appBuild, file); 175 | 176 | client 177 | .put(path.join(pkg.cdn.path, objectName), file) 178 | .then(() => { 179 | resolve(true); 180 | spinner.warn(chalk.yellow('已上传:' + objectName)); 181 | }) 182 | .catch(error => { 183 | resolve(false); 184 | spinner.fail(chalk.red('上传失败(' + error + '):' + objectName)); 185 | }); 186 | }, 200 * throttleDelay++); 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /docs/scripts/config/checkMissDependencies.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const checkDependencies = require('check-dependencies'); 3 | const inquirer = require('react-dev-utils/inquirer'); 4 | const spawn = require('cross-spawn'); 5 | const chalk = require('chalk'); 6 | const paths = require('./paths'); 7 | 8 | async function checkMissDeps(spinner) { 9 | const result = await checkDependencies({ 10 | packageDir: paths.root 11 | }); 12 | 13 | if (result.status !== 0) { 14 | spinner.stop(); 15 | 16 | // 输出错误信息 17 | result.error.forEach(function(err) { 18 | console.log(err); 19 | }); 20 | 21 | console.log(); 22 | 23 | const { reInstall } = await inquirer.prompt([ 24 | { 25 | name: 'reInstall', 26 | type: 'confirm', 27 | message: 28 | '你当前安装的依赖版本和要求的不一致,是否要重新安装所有依赖?\n' + 29 | chalk.dim('重新运行 npm install 安装所有依赖项.'), 30 | default: true 31 | } 32 | ]); 33 | 34 | console.log(); 35 | 36 | if (reInstall) { 37 | await new Promise((resolve, reject) => { 38 | install(function(code, command, args) { 39 | if (code !== 0) { 40 | spinner.fail('`' + command + ' ' + args.join(' ') + '` 运行失败'); 41 | 42 | reject(); 43 | } else { 44 | resolve(); 45 | } 46 | }); 47 | }); 48 | 49 | spinner.succeed(chalk.green('项目依赖已更新')); 50 | } else { 51 | spinner.warn(chalk.yellow('你需要按照下面命令操作后才能继续:')); 52 | console.log(); 53 | 54 | console.log(chalk.green(' ' + paths.npmCommander + ' install')); 55 | 56 | return Promise.reject(); 57 | } 58 | } 59 | } 60 | 61 | function install(callback) { 62 | let command = paths.npmCommander; 63 | let args = ['install']; 64 | 65 | var child = spawn(command, args, { 66 | stdio: 'inherit' 67 | }); 68 | 69 | child.on('close', function(code) { 70 | callback(code, command, args); 71 | }); 72 | } 73 | 74 | module.exports = checkMissDeps; 75 | -------------------------------------------------------------------------------- /docs/scripts/config/env.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const paths = require('./paths'); 5 | const pkg = require(paths.appPackageJson); 6 | 7 | delete require.cache[require.resolve('./paths')]; 8 | 9 | const NODE_ENV = process.env.NODE_ENV; 10 | 11 | let dotenvFiles = [ 12 | `${paths.dotenv}.${NODE_ENV}.local`, 13 | `${paths.dotenv}.${NODE_ENV}`, 14 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 15 | paths.dotenv 16 | ].filter(Boolean); 17 | 18 | dotenvFiles.forEach(dotenvFile => { 19 | if (fs.existsSync(dotenvFile)) { 20 | require('dotenv-expand')( 21 | require('dotenv').config({ 22 | path: dotenvFile 23 | }) 24 | ); 25 | } 26 | }); 27 | 28 | const appDirectory = fs.realpathSync(process.cwd()); 29 | 30 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 31 | .split(path.delimiter) 32 | .filter(folder => folder && !path.isAbsolute(folder)) 33 | .map(folder => path.resolve(appDirectory, folder)) 34 | .join(path.delimiter); 35 | 36 | if (!('BASE_NAME' in process.env) && 'basename' in pkg) { 37 | process.env.BASE_NAME = pkg.basename; 38 | } 39 | 40 | const REACT_APP = /^(REACT_APP_|TIGER_)/i; 41 | const whitelists = ['BASE_NAME']; 42 | 43 | function getClientEnvironment(publicUrl) { 44 | const raw = Object.keys(process.env) 45 | .filter(key => REACT_APP.test(key) || whitelists.includes(key)) 46 | .reduce( 47 | (env, key) => { 48 | env[key] = process.env[key]; 49 | return env; 50 | }, 51 | { 52 | NODE_ENV: process.env.NODE_ENV || 'development', 53 | PUBLIC_URL: publicUrl 54 | } 55 | ); 56 | // Stringify all values so we can feed into Webpack DefinePlugin 57 | const stringified = { 58 | 'process.env': Object.keys(raw).reduce((env, key) => { 59 | env[key] = JSON.stringify(raw[key]); 60 | return env; 61 | }, {}) 62 | }; 63 | 64 | return { raw, stringified }; 65 | } 66 | 67 | module.exports = getClientEnvironment; 68 | -------------------------------------------------------------------------------- /docs/scripts/config/eslintrc.js: -------------------------------------------------------------------------------- 1 | const paths = require('./paths'); 2 | 3 | /** 4 | * 0: off 5 | * 1: warn 6 | * 2: error 7 | */ 8 | module.exports = { 9 | overrides: [ 10 | { 11 | files: ['**/*.ts?(x)'], 12 | /* parserOptions: { 13 | * project: paths.appTsConfig, 14 | * }, */ 15 | rules: { 16 | // typescript 17 | '@typescript-eslint/no-use-before-define': [2, { functions: false, classes: false }], 18 | '@typescript-eslint/unified-signatures': 1, 19 | // '@typescript-eslint/await-thenable': 2, 20 | '@typescript-eslint/camelcase': 0, 21 | 'no-unused-vars': 0, 22 | '@typescript-eslint/no-unused-vars': [ 23 | 1, 24 | { 25 | vars: 'all', 26 | args: 'after-used', 27 | ignoreRestSiblings: true, 28 | varsIgnorePattern: '^_', 29 | argsIgnorePattern: '^_|^err|^ev' // _xxx, err, error, ev, event 30 | } 31 | ], 32 | '@typescript-eslint/adjacent-overload-signatures': 2, 33 | '@typescript-eslint/array-type': [ 34 | 1, 35 | { 36 | default: 'array-simple' 37 | } 38 | ], 39 | '@typescript-eslint/ban-types': 2, 40 | '@typescript-eslint/class-name-casing': 2, 41 | '@typescript-eslint/explicit-function-return-type': 0, 42 | '@typescript-eslint/explicit-member-accessibility': 0, 43 | '@typescript-eslint/interface-name-prefix': 0, 44 | '@typescript-eslint/member-delimiter-style': 2, 45 | '@typescript-eslint/no-empty-interface': 1, 46 | '@typescript-eslint/no-extra-non-null-assertion': 2, 47 | '@typescript-eslint/no-explicit-any': 0, 48 | '@typescript-eslint/no-inferrable-types': 0, 49 | '@typescript-eslint/no-misused-new': 2, 50 | '@typescript-eslint/no-non-null-assertion': 0, 51 | '@typescript-eslint/consistent-type-assertions': 2, 52 | '@typescript-eslint/no-parameter-properties': 2, 53 | '@typescript-eslint/triple-slash-reference': 2, 54 | '@typescript-eslint/no-var-requires': 2, 55 | '@typescript-eslint/consistent-type-definitions': [2, 'interface'], 56 | '@typescript-eslint/no-namespace': 2, 57 | '@typescript-eslint/prefer-namespace-keyword': 2, 58 | '@typescript-eslint/type-annotation-spacing': 1 59 | } 60 | } 61 | ], 62 | globals: { 63 | __: true 64 | }, 65 | parserOptions: { 66 | ecmaFeatures: { 67 | legacyDecorators: true 68 | } 69 | }, 70 | rules: { 71 | 'react/jsx-no-target-blank': 0, 72 | 'react/no-unsafe': [2, { checkAliases: true }], 73 | 'react/no-deprecated': 2, 74 | 'react/no-string-refs': [1, { noTemplateLiterals: true }], 75 | 'jsx-a11y/anchor-is-valid': 0, 76 | 'import/no-anonymous-default-export': [ 77 | 2, 78 | { 79 | allowArray: true, 80 | allowArrowFunction: false, 81 | allowAnonymousClass: false, 82 | allowAnonymousFunction: false, 83 | allowCallExpression: true, // The true value here is for backward compatibility 84 | allowLiteral: true, 85 | allowObject: true 86 | } 87 | ], 88 | eqeqeq: [1, 'smart'], 89 | radix: 0, 90 | 'no-script-url': 0, 91 | 'linebreak-style': [1, 'unix'], 92 | indent: 0, // process by prettier 93 | semi: 0, // process by prettier 94 | 'semi-spacing': [1, { before: false }], 95 | 'no-extra-semi': 1, 96 | 'padded-blocks': [1, 'never'], 97 | 'one-var-declaration-per-line': [1, 'initializations'], 98 | 'spaced-comment': [1, 'always'], 99 | 'space-in-parens': [1, 'never'], 100 | 'space-before-function-paren': [ 101 | 1, 102 | { 103 | anonymous: 'never', 104 | named: 'never', 105 | asyncArrow: 'always' 106 | } 107 | ], 108 | 'space-unary-ops': 1, 109 | 'space-infix-ops': 1, 110 | 'space-before-blocks': 1, 111 | 'no-trailing-spaces': [1, { ignoreComments: true }], 112 | 'key-spacing': [1, { mode: 'strict' }], 113 | 'switch-colon-spacing': 1, 114 | 'func-call-spacing': [1, 'never'], 115 | 'keyword-spacing': 1, 116 | 'no-multiple-empty-lines': [ 117 | 1, 118 | { 119 | max: 2, 120 | maxEOF: 1, 121 | maxBOF: 1 122 | } 123 | ], 124 | 'default-case': [1, { commentPattern: '^no[-\\s]+default$' }], 125 | curly: 2, 126 | 'dot-notation': 1, 127 | 'no-else-return': 2, 128 | 'guard-for-in': 2, 129 | 'no-empty-pattern': 2, 130 | 'no-implied-eval': 2, 131 | 'no-global-assign': 2, 132 | 'no-multi-spaces': [ 133 | 1, 134 | { 135 | ignoreEOLComments: true, 136 | exceptions: { 137 | VariableDeclarator: true, 138 | ImportDeclaration: true 139 | } 140 | } 141 | ], 142 | 'no-lone-blocks': 2, 143 | 'no-self-compare': 2, 144 | 'no-sequences': 2, 145 | yoda: 1, 146 | 'no-unexpected-multiline': 1, 147 | 'no-with': 2, 148 | 'no-useless-escape': 2, 149 | 'no-useless-concat': 2, 150 | 'no-unused-vars': [ 151 | 1, 152 | { 153 | vars: 'all', 154 | args: 'after-used', 155 | ignoreRestSiblings: true, 156 | varsIgnorePattern: '^_', 157 | argsIgnorePattern: '^_|^err|^ev' // _xxx, err, error, ev, event 158 | } 159 | ], 160 | 'no-unmodified-loop-condition': 2, 161 | 'wrap-iife': [2, 'inside'], 162 | 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }], 163 | 'padding-line-between-statements': [ 164 | 1, 165 | { 166 | blankLine: 'always', 167 | prev: [ 168 | 'multiline-block-like', 169 | 'multiline-expression', 170 | 'const', 171 | 'let', 172 | 'var', 173 | 'cjs-import', 174 | 'import', 175 | 'export', 176 | 'cjs-export', 177 | 'class', 178 | 'throw', 179 | 'directive' 180 | ], 181 | next: '*' 182 | }, 183 | { 184 | blankLine: 'always', 185 | prev: '*', 186 | next: [ 187 | 'multiline-block-like', 188 | 'multiline-expression', 189 | 'const', 190 | 'let', 191 | 'var', 192 | 'cjs-import', 193 | 'import', 194 | 'export', 195 | 'cjs-export', 196 | 'class', 197 | 'throw', 198 | 'return' 199 | ] 200 | }, 201 | { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] }, 202 | { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] }, 203 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] } 204 | ] 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /docs/scripts/config/helper.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const getProcessForPort = require('react-dev-utils/getProcessForPort'); 3 | const clearConsole = require('react-dev-utils/clearConsole'); 4 | const detect = require('detect-port-alt'); 5 | const inquirer = require('react-dev-utils/inquirer'); 6 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 7 | const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); 8 | const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware'); 9 | const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware'); 10 | const forkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); 11 | const ignoredFiles = require('react-dev-utils/ignoredFiles'); 12 | const typescriptFormatter = require('react-dev-utils/typescriptFormatter'); 13 | const config = require('./webpack.config.dev'); 14 | const paths = require('./paths'); 15 | const chalk = require('chalk'); 16 | const fs = require('fs'); 17 | const path = require('path'); 18 | const pkg = require(paths.appPackageJson); 19 | 20 | const isInteractive = process.stdout.isTTY; 21 | 22 | function choosePort(host, defaultPort, spinner) { 23 | return detect(defaultPort, host).then( 24 | port => 25 | new Promise(resolve => { 26 | if (port === defaultPort) { 27 | return resolve(port); 28 | } 29 | 30 | spinner.stop(); 31 | clearConsole(); 32 | 33 | const existingProcess = getProcessForPort(defaultPort); 34 | const question = { 35 | type: 'confirm', 36 | name: 'shouldChangePort', 37 | message: 38 | '端口(' + 39 | chalk.yellow(defaultPort) + 40 | ')被占用,可能的程序是: \n ' + 41 | existingProcess + 42 | '\n' + 43 | ' 要换一个端口运行本程序吗?', 44 | default: true 45 | }; 46 | 47 | inquirer.prompt(question).then(answer => { 48 | if (answer.shouldChangePort) { 49 | resolve(port); 50 | } else { 51 | resolve(null); 52 | } 53 | }); 54 | }), 55 | err => { 56 | throw new Error( 57 | chalk.red(`无法为 ${chalk.bold(host)} 找到可用的端口.`) + 58 | '\n' + 59 | ('错误信息: ' + err.message || err) + 60 | '\n' 61 | ); 62 | } 63 | ); 64 | } 65 | 66 | function createCompiler(webpack, config, appName, urls, devSocket, spinner) { 67 | let compiler; 68 | let stime = Date.now(); 69 | 70 | try { 71 | compiler = webpack(config); 72 | } catch (err) { 73 | console.log(chalk.red('启动编译失败!')); 74 | console.log(); 75 | console.log(err.message || err); 76 | console.log(); 77 | process.exit(1); 78 | } 79 | 80 | compiler.hooks.invalid.tap('invalid', () => { 81 | if (isInteractive) { 82 | clearConsole(); 83 | } 84 | 85 | stime = Date.now(); 86 | spinner.text = chalk.cyan('重新编译...'); 87 | }); 88 | 89 | let isFirstCompile = true; 90 | let tsMessagesPromise; 91 | let tsMessagesResolver; 92 | 93 | compiler.hooks.beforeCompile.tap('beforeCompile', () => { 94 | tsMessagesPromise = new Promise(resolve => { 95 | tsMessagesResolver = msgs => resolve(msgs); 96 | }); 97 | }); 98 | 99 | forkTsCheckerWebpackPlugin.getCompilerHooks(compiler).receive.tap('afterTypeScriptCheck', (diagnostics, lints) => { 100 | const allMsgs = [...diagnostics, ...lints]; 101 | const format = message => `${message.file}\n${typescriptFormatter(message, true)}`; 102 | 103 | tsMessagesResolver({ 104 | errors: allMsgs.filter(msg => msg.severity === 'error').map(format), 105 | warnings: allMsgs.filter(msg => msg.severity === 'warning').map(format) 106 | }); 107 | }); 108 | 109 | // "done" event fires when Webpack has finished recompiling the bundle. 110 | // Whether or not you have warnings or errors, you will get this event. 111 | compiler.hooks.done.tap('done', async stats => { 112 | if (isInteractive) { 113 | clearConsole(); 114 | } 115 | 116 | const useTimer = (isTotal = false) => 117 | chalk.grey(`(编译${isTotal ? '总' : '已'}耗时: ${(Date.now() - stime) / 1000}s)`); 118 | 119 | const statsData = stats.toJson({ 120 | all: false, 121 | warnings: true, 122 | errors: true 123 | }); 124 | 125 | if (statsData.errors.length === 0) { 126 | const delayedMsg = setTimeout(() => { 127 | spinner.text = chalk.cyan('文件已编译,正在TSC检查...') + useTimer(); 128 | }, 100); 129 | 130 | const messages = await tsMessagesPromise; 131 | 132 | clearTimeout(delayedMsg); 133 | statsData.errors.push(...messages.errors); 134 | statsData.warnings.push(...messages.warnings); 135 | 136 | stats.compilation.errors.push(...messages.errors); 137 | stats.compilation.warnings.push(...messages.warnings); 138 | 139 | if (messages.errors.length > 0) { 140 | devSocket.errors(messages.errors); 141 | } else if (messages.warnings.length > 0) { 142 | devSocket.warnings(messages.warnings); 143 | } 144 | 145 | if (isInteractive) { 146 | clearConsole(); 147 | } 148 | } 149 | 150 | const messages = formatWebpackMessages(statsData); 151 | const isSuccessful = !messages.errors.length && !messages.warnings.length; 152 | 153 | if (isSuccessful && (isInteractive || isFirstCompile)) { 154 | spinner.succeed(chalk.green('编译通过!' + useTimer(true))); 155 | console.log(); 156 | spinner.succeed(chalk.green('应用(' + appName + ')已启动:')); 157 | console.log(); 158 | 159 | if (urls.lanUrlForTerminal) { 160 | console.log(` ${chalk.bold('本地:')} ${chalk.cyan(urls.localUrlForTerminal)}`); 161 | console.log(` ${chalk.bold('远程:')} ${chalk.cyan(urls.lanUrlForTerminal)}`); 162 | } else { 163 | console.log(` ${urls.localUrlForTerminal}`); 164 | } 165 | } 166 | 167 | isFirstCompile = false; 168 | 169 | // If errors exist, only show errors. 170 | if (messages.errors.length) { 171 | if (messages.errors.length > 1) { 172 | messages.errors.length = 1; 173 | } 174 | 175 | spinner.fail(chalk.red('编译失败!!' + useTimer(true))); 176 | console.log(); 177 | console.log(messages.errors.join('\n\n')); 178 | console.log(); 179 | } 180 | 181 | // Show warnings if no errors were found. 182 | if (messages.warnings.length) { 183 | spinner.warn(chalk.yellow('编译有警告产生:' + useTimer(true))); 184 | console.log(); 185 | console.log(messages.warnings.join('\n\n')); 186 | console.log(); 187 | 188 | // Teach some ESLint tricks. 189 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.'); 190 | 191 | console.log( 192 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n' 193 | ); 194 | } 195 | 196 | console.log(); 197 | spinner.text = chalk.cyan('webpack运行中...'); 198 | spinner.render().start(); 199 | }); 200 | 201 | return compiler; 202 | } 203 | 204 | function createDevServerConfig(proxy, allowedHost) { 205 | return { 206 | headers: { 207 | 'Access-Control-Allow-Origin': '*', 208 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, HEAD, DELETE, FETCH' 209 | }, 210 | disableHostCheck: !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', 211 | // Enable gzip 212 | compress: true, 213 | clientLogLevel: 'none', 214 | contentBase: paths.appPublic, 215 | watchContentBase: true, 216 | hot: true, 217 | transportMode: 'ws', 218 | injectClient: false, 219 | publicPath: config.output.publicPath, 220 | quiet: true, 221 | watchOptions: { 222 | ignored: ignoredFiles(paths.appSrc) 223 | }, 224 | https: process.env.HTTPS === 'true', 225 | host: process.env.HOST || '0.0.0.0', 226 | overlay: false, 227 | historyApiFallback: pkg.noRewrite 228 | ? false 229 | : { 230 | disableDotRule: true 231 | }, 232 | public: allowedHost, 233 | proxy, 234 | before(app, server) { 235 | if (fs.existsSync(paths.proxySetup)) { 236 | require(paths.proxySetup)(app); 237 | } 238 | 239 | app.use(evalSourceMapMiddleware(server)); 240 | app.use(errorOverlayMiddleware()); 241 | app.use(noopServiceWorkerMiddleware()); 242 | } 243 | }; 244 | } 245 | 246 | function onProxyError(proxy) { 247 | return function(err, req, res) { 248 | var host = req.headers && req.headers.host; 249 | 250 | console.log( 251 | chalk.red('代理错误:') + 252 | '无法将 ' + 253 | chalk.cyan(req.url) + 254 | ' 的请求从 ' + 255 | chalk.cyan(host) + 256 | ' 转发到 ' + 257 | chalk.cyan(proxy) + 258 | '.' 259 | ); 260 | 261 | console.log( 262 | '点击 https://nodejs.org/api/errors.html#errors_common_system_errors 查看更多信息 (' + 263 | chalk.cyan(err.code) + 264 | ').' 265 | ); 266 | 267 | console.log(); 268 | 269 | if (res.writeHead && !res.headersSent) { 270 | res.writeHead(500); 271 | } 272 | 273 | res.end('代理错误: 无法将 ' + req.url + ' 的请求从 ' + host + ' 转发到 ' + proxy + ' (' + err.code + ').'); 274 | }; 275 | } 276 | 277 | function mayProxy(pathname) { 278 | const maybePublicPath = path.resolve(paths.appPublic, pathname.slice(1)); 279 | 280 | return !fs.existsSync(maybePublicPath); 281 | } 282 | 283 | function prepareProxy(proxy) { 284 | if (!proxy) { 285 | return undefined; 286 | } 287 | 288 | if (typeof proxy === 'object') { 289 | return Object.keys(proxy).map(function(path) { 290 | const opt = 291 | typeof proxy[path] === 'object' 292 | ? proxy[path] 293 | : { 294 | target: proxy[path] 295 | }; 296 | const target = opt.target; 297 | 298 | return Object.assign({}, opt, { 299 | context: function(pathname) { 300 | return mayProxy(pathname) && pathname.match(path); 301 | }, 302 | onProxyReq: proxyReq => { 303 | if (proxyReq.getHeader('origin')) { 304 | proxyReq.setHeader('origin', target); 305 | } 306 | }, 307 | onError: onProxyError(target) 308 | }); 309 | }); 310 | } 311 | 312 | if (!/^http(s)?:\/\//.test(proxy)) { 313 | console.log(chalk.red('proxy 只能是一个 http:// 或者 https:// 开头的字符串或者一个object配置')); 314 | console.log(chalk.red('当前 proxy 的类型是 "' + typeof proxy + '"。')); 315 | console.log(chalk.red('你可以从 package.json 中移除它,或者设置一个字符串地址(目标服务器)')); 316 | process.exit(1); 317 | } 318 | 319 | return [ 320 | { 321 | target: proxy, 322 | logLevel: 'silent', 323 | context: function(pathname, req) { 324 | return ( 325 | req.method !== 'GET' || 326 | (mayProxy(pathname) && req.headers.accept && req.headers.accept.indexOf('text/html') === -1) 327 | ); 328 | }, 329 | onProxyReq: proxyReq => { 330 | if (proxyReq.getHeader('origin')) { 331 | proxyReq.setHeader('origin', proxy); 332 | } 333 | }, 334 | onError: onProxyError(proxy), 335 | secure: false, 336 | changeOrigin: true, 337 | ws: true, 338 | xfwd: true 339 | } 340 | ]; 341 | } 342 | 343 | module.exports = { 344 | choosePort, 345 | prepareProxy, 346 | createCompiler, 347 | createDevServerConfig 348 | }; 349 | -------------------------------------------------------------------------------- /docs/scripts/config/paths.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const glob = require('glob'); 5 | const execSync = require('child_process').execSync; 6 | const isDev = process.env.NODE_ENV === 'development'; 7 | const lodash = require('lodash'); 8 | 9 | // Make sure any symlinks in the project folder are resolved: 10 | // https://github.com/facebookincubator/create-react-app/issues/637 11 | const appDirectory = fs.realpathSync(process.cwd()); 12 | const pkg = require(resolveApp('package.json')); 13 | 14 | function resolveApp(relativePath) { 15 | return path.resolve(appDirectory, relativePath); 16 | } 17 | 18 | const nodePaths = (process.env.NODE_PATH || '') 19 | .split(path.delimiter) 20 | .filter(Boolean) 21 | .map(resolveApp); 22 | 23 | const entries = {}; 24 | 25 | glob.sync(resolveApp('app/!(_)*.{j,t}s?(x)')).forEach(function(file) { 26 | const basename = path.basename(file).replace(/\.[jt]sx?$/, ''); 27 | 28 | entries[basename] = file; 29 | }); 30 | 31 | const alias = Object.assign( 32 | { 33 | components: resolveApp('app/components'), 34 | modules: resolveApp('app/modules'), 35 | utils: resolveApp('app/utils'), 36 | stores: resolveApp('app/stores'), 37 | types: resolveApp('app/types'), 38 | hooks: resolveApp('app/hooks') 39 | }, 40 | lodash.mapValues(pkg.alias, function(relativePath) { 41 | if (fs.pathExistsSync(resolveApp(relativePath))) { 42 | return resolveApp(relativePath); 43 | } 44 | 45 | return relativePath; 46 | }) 47 | ); 48 | 49 | // config after eject: we're in ./config/ 50 | module.exports = { 51 | dotenv: resolveApp('.env'), 52 | root: resolveApp(''), 53 | appBuild: resolveApp(process.env.BUILD_DIR || 'demo'), 54 | appPublic: resolveApp('public'), 55 | appHtml: resolveApp('public/index.html'), 56 | appIndexJs: Object.values(entries)[0] || resolveApp('app/index.js'), 57 | appPackageJson: resolveApp('package.json'), 58 | appSrc: resolveApp('app'), 59 | formutilSrc: resolveApp('app/../../src'), 60 | appTsConfig: resolveApp('tsconfig.json'), 61 | staticSrc: resolveApp('static'), 62 | locals: resolveApp('locals'), 63 | proxySetup: resolveApp('setupProxy.js'), 64 | appNodeModules: resolveApp('node_modules'), 65 | ownNodeModules: resolveApp('node_modules'), 66 | nodePaths: nodePaths, 67 | alias: alias, 68 | entries: entries, 69 | pageEntries: glob.sync(resolveApp('public/!(_)*.html')).map(function(file) { 70 | return path.basename(file, '.html'); 71 | }), 72 | moduleFileExtensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx'], 73 | // 一些命令检测 74 | serve: hasInstall('serve'), 75 | npmCommander: ['tnpm', 'cnpm', 'npm'].find(hasInstall) 76 | }; 77 | 78 | function hasInstall(command) { 79 | try { 80 | execSync(command + ' --version', { 81 | stdio: 'ignore' 82 | }); 83 | 84 | return true; 85 | } catch (e) { 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/scripts/config/polyfills.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | // classList 3 | require('classlist-polyfill'); 4 | 5 | // requestAnimationFrame 6 | require('raf-dom').polyfill(); 7 | 8 | // ECMAScript 9 | require('core-js/features/object'); 10 | require('core-js/features/promise'); 11 | require('core-js/features/map'); 12 | require('core-js/features/set'); 13 | require('core-js/features/array'); 14 | require('core-js/features/string'); 15 | require('core-js/features/number'); 16 | require('core-js/features/symbol'); 17 | 18 | // require('core-js/features/url'); 19 | // require('core-js/features/url-search-params'); 20 | -------------------------------------------------------------------------------- /docs/scripts/config/tslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "trailing-comma": false, 4 | "quotemark": false, 5 | "no-console": false, 6 | "semicolon": false, 7 | "no-namespace": [true, "allow-declarations"], 8 | "max-classes-per-file": false, 9 | "ordered-imports": false, 10 | "member-ordering": false, 11 | "object-literal-sort-keys": false, 12 | "member-access": false, 13 | "arrow-parens": [true, "ban-single-arg-parens"], 14 | "variable-name": false, 15 | "prefer-const": false, 16 | "jsx-boolean-value": false, 17 | "only-arrow-functions": false, 18 | "jsx-no-lambda": false, 19 | "interface-name": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/scripts/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const resolve = require('resolve'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 9 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 10 | const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); 11 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin'); 12 | const getClientEnvironment = require('./env'); 13 | const paths = require('./paths'); 14 | const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); 15 | const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); 16 | const pkg = require(paths.appPackageJson); 17 | const isBuilding = process.env.WEBPACK_BUILDING === 'true'; 18 | 19 | const publicPath = isBuilding ? path.join(pkg.noRewrite ? '.' : process.env.BASE_NAME || '', '/') : '/'; 20 | const publicUrl = publicPath.slice(0, -1); 21 | const env = getClientEnvironment(publicUrl); 22 | const injects = []; 23 | 24 | // eslint-disable-next-line 25 | const matchScriptStylePattern = /<\!--\s*script:\s*([\w]+)(?:\.[jt]sx?)?\s*-->/g; 26 | const hotDev = require.resolve('react-dev-utils/webpackHotDevClient'); 27 | const babelOption = { 28 | babelrc: false, 29 | configFile: false, 30 | compact: false, 31 | presets: [[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }]], 32 | cacheDirectory: true, 33 | cacheCompression: false, 34 | sourceMaps: false 35 | }; 36 | 37 | paths.pageEntries.forEach(function(name) { 38 | var chunks = ['_vendor_']; 39 | var file = path.resolve(paths.appPublic, name + '.html'); 40 | 41 | if (paths.entries[name]) { 42 | chunks.push(name); 43 | } 44 | 45 | var contents = fs.readFileSync(file); 46 | var matches; 47 | 48 | while ((matches = matchScriptStylePattern.exec(contents))) { 49 | chunks.push(matches[1]); 50 | } 51 | 52 | injects.push( 53 | new HtmlWebpackPlugin({ 54 | chunks: chunks, 55 | filename: name + '.html', 56 | template: file, 57 | inject: true, 58 | chunksSortMode: 'manual' 59 | }) 60 | ); 61 | }); 62 | 63 | module.exports = { 64 | mode: 'development', 65 | devtool: 'cheap-module-source-map', 66 | entry: Object.assign( 67 | { 68 | _vendor_: [require.resolve('./polyfills'), !isBuilding && hotDev].filter(Boolean).concat(pkg.vendor || []) 69 | }, 70 | paths.entries 71 | ), 72 | output: { 73 | path: paths.appBuild, 74 | pathinfo: true, 75 | filename: 'static/js/[name].[hash:8].js', 76 | chunkFilename: 'static/js/[name].[hash:8].js', 77 | publicPath: publicPath, 78 | crossOriginLoading: 'anonymous', 79 | devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), 80 | globalObject: 'this' 81 | }, 82 | optimization: { 83 | runtimeChunk: 'single', 84 | splitChunks: { 85 | chunks: 'async', 86 | name: false, 87 | cacheGroups: { 88 | vendors: { 89 | chunks: 'all', 90 | test: '_vendor_', 91 | name: 'vendor' 92 | }, 93 | i18n: { 94 | chunks: 'all', 95 | test: /utils\/i18n|locals\/\w+\.json/, 96 | enforce: true, 97 | name: 'i18n' 98 | } 99 | } 100 | } 101 | }, 102 | resolve: { 103 | modules: ['node_modules', paths.appNodeModules, paths.root].concat(paths.nodePaths), 104 | extensions: paths.moduleFileExtensions, 105 | alias: Object.assign( 106 | { 107 | 'react-native': 'react-native-web' 108 | }, 109 | paths.alias 110 | ), 111 | plugins: [ 112 | new DirectoryNamedWebpackPlugin({ 113 | honorIndex: true, 114 | exclude: /node_modules|libs/ 115 | }) 116 | ] 117 | }, 118 | 119 | module: { 120 | strictExportPresence: true, 121 | rules: [ 122 | { parser: { requireEnsure: false } }, 123 | 124 | { 125 | test: /\.(js|mjs|jsx|ts|tsx)$/, 126 | enforce: 'pre', 127 | use: [ 128 | { 129 | options: { 130 | cache: true, 131 | formatter: require.resolve('react-dev-utils/eslintFormatter'), 132 | eslintPath: require.resolve('eslint'), 133 | resolvePluginsRelativeTo: __dirname 134 | }, 135 | loader: require.resolve('eslint-loader') 136 | } 137 | ], 138 | include: [paths.formutilSrc, paths.appSrc] 139 | }, 140 | { 141 | oneOf: [ 142 | { 143 | test: /\.html$/, 144 | use: [ 145 | { 146 | loader: require.resolve('babel-loader'), 147 | options: babelOption 148 | }, 149 | { 150 | loader: require.resolve('html-loader'), 151 | options: { 152 | url(url) { 153 | return !/\.(webp|png|jpeg|jpg|gif|svg|mp3|wmv|mp4|ogg|webm|s[ac]ss|css|less|m?[tj]sx?)$/.test( 154 | url 155 | ); 156 | }, 157 | import: true 158 | } 159 | } 160 | ] 161 | }, 162 | { 163 | test: /\.(js|mjs|jsx|ts|tsx)$/, 164 | include: [paths.formutilSrc, paths.appSrc], 165 | loader: require.resolve('babel-loader'), 166 | options: { 167 | customize: require.resolve('babel-preset-react-app/webpack-overrides'), 168 | plugins: [ 169 | [ 170 | require.resolve('babel-plugin-named-asset-import'), 171 | { 172 | loaderMap: { 173 | svg: { 174 | ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]' 175 | } 176 | } 177 | } 178 | ] 179 | ], 180 | cacheDirectory: true, 181 | cacheCompression: false, 182 | rootMode: 'upward' 183 | } 184 | }, 185 | { 186 | test: /\.(js|mjs)$/, 187 | exclude: /@babel(?:\/|\\{1,2})runtime/, 188 | loader: require.resolve('babel-loader'), 189 | options: babelOption 190 | }, 191 | { 192 | test: /\.css$/, 193 | exclude: /\.module\.css$/, 194 | use: getStyleLoaders({ 195 | importLoaders: 1 196 | }) 197 | }, 198 | // Adds support for CSS Modules (https://github.com/css-modules/css-modules) 199 | // using the extension .module.css 200 | { 201 | test: /\.module\.css$/, 202 | use: getStyleLoaders({ 203 | importLoaders: 1, 204 | modules: { 205 | getLocalIdent: getCSSModuleLocalIdent 206 | } 207 | }) 208 | }, 209 | { 210 | test: /\.s[ac]ss$/, 211 | exclude: /\.module\.s[ac]ss$/, 212 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader') 213 | }, 214 | { 215 | test: /\.module\.s[ac]ss$/, 216 | use: getStyleLoaders( 217 | { 218 | importLoaders: 2, 219 | modules: { 220 | getLocalIdent: getCSSModuleLocalIdent 221 | } 222 | }, 223 | 'sass-loader' 224 | ) 225 | }, 226 | { 227 | test: /\.less$/, 228 | exclude: /\.module\.less$/, 229 | use: getStyleLoaders({ importLoaders: 2 }, 'less-loader') 230 | }, 231 | { 232 | test: /\.module\.less$/, 233 | use: getStyleLoaders( 234 | { 235 | importLoaders: 2, 236 | modules: { 237 | getLocalIdent: getCSSModuleLocalIdent 238 | } 239 | }, 240 | 'less-loader' 241 | ) 242 | }, 243 | { 244 | test: /\.(txt|htm)$/, 245 | loader: require.resolve('raw-loader') 246 | }, 247 | { 248 | test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)$/, 249 | loader: require.resolve('file-loader'), 250 | options: { 251 | name: 'static/media/[name].[hash:8].[ext]' 252 | } 253 | }, 254 | { 255 | exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.(txt|htm)$/], 256 | loader: require.resolve('file-loader'), 257 | options: { 258 | name: 'static/images/[name].[hash:8].[ext]' 259 | } 260 | } 261 | ] 262 | } 263 | ] 264 | }, 265 | plugins: injects 266 | .concat([ 267 | isBuilding && 268 | new MiniCssExtractPlugin({ 269 | filename: 'static/css/[name].[hash:8].css', 270 | ignoreOrder: !!pkg.ignoreCssOrderWarnings || process.env.IGNORE_CSS_ORDER_WARNINGS === 'true' 271 | }), 272 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), 273 | new ModuleNotFoundPlugin(paths.root), 274 | new webpack.EnvironmentPlugin(env.raw), 275 | new CaseSensitivePathsPlugin(), 276 | new webpack.IgnorePlugin({ 277 | resourceRegExp: /^\.\/locale$/, 278 | contextRegExp: /moment$/ 279 | }), 280 | new ForkTsCheckerWebpackPlugin({ 281 | typescript: resolve.sync('typescript', { 282 | basedir: paths.appNodeModules 283 | }), 284 | async: true, 285 | useTypescriptIncrementalApi: true, 286 | checkSyntacticErrors: true, 287 | tsconfig: paths.appTsConfig, 288 | compilerOptions: { 289 | jsx: 'preserve', 290 | checkJs: false 291 | }, 292 | reportFiles: ['**/*.(ts|tsx)', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], 293 | silent: true 294 | }), 295 | new webpack.BannerPlugin({ 296 | banner: '@author ' + pkg.author, 297 | entryOnly: true 298 | }) 299 | ]) 300 | .filter(Boolean), 301 | 302 | node: { 303 | dgram: 'empty', 304 | fs: 'empty', 305 | net: 'empty', 306 | tls: 'empty', 307 | child_process: 'empty' 308 | }, 309 | 310 | performance: false 311 | }; 312 | 313 | function getStyleLoaders(cssOptions, preProcessor) { 314 | const loaders = [ 315 | isBuilding 316 | ? { 317 | loader: MiniCssExtractPlugin.loader, 318 | options: { publicPath: '../../', sourceMap: true, esModule: true } 319 | } 320 | : require.resolve('style-loader'), 321 | { 322 | loader: require.resolve('css-loader'), 323 | options: Object.assign({ sourceMap: true }, cssOptions) 324 | }, 325 | { 326 | loader: require.resolve('postcss-loader'), 327 | options: { 328 | ident: 'postcss', 329 | plugins: () => [ 330 | require('postcss-flexbugs-fixes'), 331 | require('postcss-preset-env')({ 332 | autoprefixer: { 333 | flexbox: 'no-2009' 334 | }, 335 | stage: 3 336 | }) 337 | ], 338 | sourceMap: true 339 | } 340 | } 341 | ]; 342 | 343 | if (preProcessor) { 344 | loaders.push({ 345 | loader: require.resolve(preProcessor), 346 | options: Object.assign( 347 | {}, 348 | { sourceMap: true }, 349 | preProcessor === 'less-loader' 350 | ? { 351 | javascriptEnabled: true 352 | } 353 | : { 354 | implementation: require('sass') 355 | } 356 | ) 357 | }); 358 | } 359 | 360 | return loaders; 361 | } 362 | -------------------------------------------------------------------------------- /docs/scripts/i18n.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs-extra'); 3 | const glob = require('glob'); 4 | const Parser = require('i18next-scanner').Parser; 5 | const xlsx = require('node-xlsx'); 6 | const paths = require('./config/paths'); 7 | const path = require('path'); 8 | const chalk = require('chalk'); 9 | const ora = require('ora'); 10 | const lodash = require('lodash'); 11 | const pkg = require(paths.appPackageJson); 12 | 13 | const spinner = ora(); 14 | 15 | const xlsxOptions = { 16 | '!cols': [{ wch: 50 }, { wch: 50 }] 17 | }; 18 | 19 | const terminalArg = process.argv[2]; 20 | 21 | if (terminalArg === '--scan') { 22 | ensureLocalsConfig(); 23 | scanner(); 24 | } else if (terminalArg === '--read') { 25 | ensureLocalsConfig(); 26 | reader(); 27 | } 28 | 29 | function ensureLocalsConfig() { 30 | if (Array.isArray(pkg.locals) === false) { 31 | spinner.fail(chalk.red('未在 package.json 中找到相关语言包配置!')); 32 | 33 | spinner.warn( 34 | chalk.yellow('需要 package.json 中添加 { "locals": ["zh_CN", "en_US"] } 配置后,才能运行该命令!') 35 | ); 36 | 37 | process.exit(0); 38 | } 39 | } 40 | 41 | /** 42 | * @description 43 | * 扫描源代码文件,匹配需要翻译的文案,并输出excel文件待翻译 44 | */ 45 | function scanner() { 46 | const i18nParser = new Parser({ 47 | lngs: pkg.locals, 48 | nsSeparator: false, 49 | keySeparator: false, 50 | pluralSeparator: false, 51 | contextSeparator: false 52 | }); 53 | 54 | fs.ensureDirSync(path.join(paths.locals, 'xlsx')); 55 | 56 | glob.sync(paths.appSrc + '/**/*.{js,jsx,ts,tsx}').forEach(file => { 57 | const content = fs.readFileSync(file); 58 | 59 | i18nParser.parseFuncFromString(content, { list: ['__', 'i18n.__', 'window.__'] }, key => { 60 | if (key) { 61 | i18nParser.set(key, key); 62 | } 63 | }); 64 | }); 65 | 66 | const i18nJson = i18nParser.get(); 67 | 68 | Object.keys(i18nJson).forEach(key => { 69 | const jsonDestination = path.join(paths.locals, key + '.json'); 70 | const excelDestination = path.join(paths.locals, 'xlsx', key + '.xlsx'); 71 | 72 | const translation = i18nJson[key].translation; 73 | const existConfig = fs.existsSync(jsonDestination) ? JSON.parse(fs.readFileSync(jsonDestination)) : {}; 74 | const newConfig = lodash.pickBy(existConfig, (value, key) => key in translation); 75 | 76 | lodash.each(translation, (value, key) => { 77 | if (!(key in newConfig)) { 78 | newConfig[key] = value; 79 | } 80 | }); 81 | 82 | fs.outputFile(path.join(paths.locals, key + '.json'), JSON.stringify(newConfig, '\n', 2)); 83 | 84 | convertJson2Excel(newConfig, key, path.join(excelDestination)); 85 | 86 | spinner.succeed('输出 ' + chalk.bold(chalk.green(key)) + ' 到 ' + chalk.cyan(excelDestination)); 87 | }); 88 | 89 | console.log(); 90 | spinner.warn(chalk.yellow('你可以将生成的excel文件进行翻译后,放回原处。然后运行:')); 91 | console.log(chalk.green(' npm run i18n-read')); 92 | } 93 | 94 | /** 95 | * @description 96 | * 读取excel文件,并转换为json语言包 97 | */ 98 | function reader() { 99 | glob.sync(path.join(paths.locals, 'xlsx', '!(~$)*.xlsx')).forEach(file => { 100 | const lang = path.basename(file, '.xlsx'); 101 | const jsonDestination = path.join(paths.locals, lang + '.json'); 102 | 103 | convertExcel2Json(file, lang, jsonDestination); 104 | 105 | spinner.succeed('输出 ' + chalk.bold(chalk.green(lang)) + ' 到 ' + chalk.cyan(jsonDestination)); 106 | }); 107 | 108 | console.log(); 109 | spinner.succeed(chalk.green('语言包转换成功!')); 110 | } 111 | 112 | function convertJson2Excel(jsonContent, lang, destination) { 113 | const sheets = [[pkg.name + ' v' + pkg.version, lang], ['原始文案(禁止修改)', '翻译文案'], []]; 114 | 115 | Object.keys(jsonContent).forEach(key => { 116 | const text = jsonContent[key]; 117 | 118 | sheets.push([key, text]); 119 | }); 120 | 121 | const buffer = xlsx.build([{ name: 'locals', data: sheets }], xlsxOptions); 122 | 123 | fs.writeFileSync(destination, buffer); 124 | } 125 | 126 | function convertExcel2Json(file, lang, destination) { 127 | const sheets = xlsx.parse(fs.readFileSync(file)); 128 | 129 | const jsonData = require(destination) || {}; 130 | 131 | sheets[0].data.slice(2).forEach(item => { 132 | if (item.length) { 133 | jsonData[item[0]] = item[1]; 134 | } 135 | }); 136 | 137 | fs.outputFileSync(destination, JSON.stringify(jsonData, '\n', 2)); 138 | } 139 | 140 | exports.ensureLocals = function() { 141 | fs.ensureDirSync(path.join(paths.locals)); 142 | 143 | if (Array.isArray(pkg.locals)) { 144 | pkg.locals.forEach(lang => { 145 | const file = path.join(paths.locals, lang + '.json'); 146 | 147 | if (!fs.existsSync(file)) { 148 | fs.outputJSONSync(file, {}); 149 | } 150 | }); 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /docs/scripts/start.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | process.env.BABEL_ENV = 'development'; 3 | process.env.NODE_ENV = 'development'; 4 | 5 | process.on('unhandledRejection', err => { 6 | throw err; 7 | }); 8 | 9 | require('./config/env'); 10 | 11 | const chalk = require('chalk'); 12 | const webpack = require('webpack'); 13 | const ora = require('ora'); 14 | const WebpackDevServer = require('webpack-dev-server'); 15 | const { choosePort, prepareProxy, createCompiler, createDevServerConfig } = require('./config/helper'); 16 | const { prepareUrls } = require('react-dev-utils/WebpackDevServerUtils'); 17 | const openBrowser = require('react-dev-utils/openBrowser'); 18 | const clearConsole = require('react-dev-utils/clearConsole'); 19 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | const checkMissDependencies = require('./config/checkMissDependencies'); 21 | const config = require('./config/webpack.config.dev'); 22 | const paths = require('./config/paths'); 23 | const { ensureLocals } = require('./i18n'); 24 | const pkg = require(paths.appPackageJson); 25 | 26 | const spinner = ora('webpack启动中...').start(); 27 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 28 | const HOST = process.env.HOST || '0.0.0.0'; 29 | const isInteractive = process.stdout.isTTY; 30 | 31 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 32 | console.log(); 33 | process.exit(1); 34 | } 35 | 36 | ensureLocals(); 37 | 38 | checkMissDependencies(spinner) 39 | .then(() => { 40 | return choosePort(HOST, DEFAULT_PORT, spinner).then(port => { 41 | if (port === null) { 42 | console.log(); 43 | 44 | spinner.fail( 45 | '请关闭占用 ' + 46 | chalk.bold(chalk.yellow(DEFAULT_PORT)) + 47 | ' 端口的程序后再运行;或者指定一个新的端口:' + 48 | chalk.bold(chalk.yellow('PORT=4000 npm start')) 49 | ); 50 | 51 | console.log(); 52 | process.exit(0); 53 | } else { 54 | spinner.start(); 55 | 56 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 57 | const appName = pkg.name; 58 | const urls = prepareUrls(protocol, HOST, port); 59 | const devSocket = { 60 | warnings: warnings => devServer.sockWrite(devServer.sockets, 'warnings', warnings), 61 | errors: errors => devServer.sockWrite(devServer.sockets, 'errors', errors) 62 | }; 63 | const compiler = createCompiler(webpack, config, appName, urls, devSocket, spinner); 64 | const proxyConfig = prepareProxy(process.env.PROXY || pkg.proxy, paths.appPublic); 65 | const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig); 66 | const devServer = new WebpackDevServer(compiler, serverConfig); 67 | 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | 74 | if (isInteractive) { 75 | clearConsole(); 76 | } 77 | 78 | spinner.text = chalk.cyan('正在启动测试服务器...'); 79 | openBrowser(urls.localUrlForBrowser); 80 | }); 81 | 82 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 83 | process.on(sig, function() { 84 | devServer.close(); 85 | spinner.stop(); 86 | process.exit(); 87 | }); 88 | }); 89 | } 90 | }); 91 | }) 92 | .catch(function(err) { 93 | if (err) { 94 | console.log(err.message || err); 95 | console.log(); 96 | } 97 | 98 | process.kill(process.pid, 'SIGINT'); 99 | }); 100 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "allowJs": true, 5 | "checkJs": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "jsx": "react", 9 | "resolveJsonModule": true, 10 | "experimentalDecorators": true, 11 | "allowSyntheticDefaultImports": true, 12 | "allowUnreachableCode": false, 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "skipLibCheck": true, 17 | "noEmit": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": false, 21 | "importHelpers": true, 22 | "strictNullChecks": true, 23 | "suppressImplicitAnyIndexErrors": true, 24 | "noUnusedLocals": false, 25 | "baseUrl": ".", 26 | "paths": { 27 | "*": ["*", "app/*"] 28 | } 29 | }, 30 | "exclude": ["scripts", "node_modules", "dist", "demo", "buildDev"] 31 | } 32 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | /** 4 | * 0: off 5 | * 1: warn 6 | * 2: error 7 | */ 8 | module.exports = { 9 | overrides: [ 10 | { 11 | files: ['**/__tests__/**/*', '**/*.{spec,test}.*'], 12 | rules: { 13 | 'jest/consistent-test-it': [1, { fn: 'test' }], 14 | 'jest/expect-expect': 1, 15 | 'jest/no-deprecated-functions': 2 16 | } 17 | } 18 | ], 19 | settings: { 20 | 'import/core-modules': [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] 21 | }, 22 | rules: { 23 | 'react/react-in-jsx-scope': 2, 24 | 'react/no-unsafe': [2, { checkAliases: true }], 25 | 'react/no-deprecated': 2, 26 | 'import/no-anonymous-default-export': [ 27 | 2, 28 | { 29 | allowArray: true, 30 | allowArrowFunction: false, 31 | allowAnonymousClass: false, 32 | allowAnonymousFunction: false, 33 | allowCallExpression: true, // The true value here is for backward compatibility 34 | allowLiteral: true, 35 | allowObject: true 36 | } 37 | ], 38 | 'import/no-duplicates': 1, 39 | 'import/order': [ 40 | 1, 41 | { 42 | groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'unknown'] 43 | } 44 | ], 45 | 'import/no-useless-path-segments': [ 46 | 1, 47 | { 48 | noUselessIndex: true 49 | } 50 | ], 51 | 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }], 52 | 'padding-line-between-statements': [ 53 | 1, 54 | { 55 | blankLine: 'always', 56 | prev: [ 57 | 'multiline-block-like', 58 | 'multiline-expression', 59 | 'const', 60 | 'let', 61 | 'var', 62 | 'cjs-import', 63 | 'import', 64 | 'export', 65 | 'cjs-export', 66 | 'class', 67 | 'throw', 68 | 'directive' 69 | ], 70 | next: '*' 71 | }, 72 | { 73 | blankLine: 'always', 74 | prev: '*', 75 | next: [ 76 | 'multiline-block-like', 77 | 'multiline-expression', 78 | 'const', 79 | 'let', 80 | 'var', 81 | 'cjs-import', 82 | 'import', 83 | 'export', 84 | 'cjs-export', 85 | 'class', 86 | 'throw', 87 | 'return' 88 | ] 89 | }, 90 | { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] }, 91 | { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] }, 92 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] } 93 | ] 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /hooks.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-formutil/hooks@>0.5.0 2 | // Project: react-formutil 3 | // Definitions by: qiqiboy 4 | 5 | export * from './index'; 6 | -------------------------------------------------------------------------------- /hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = require('.'); 2 | -------------------------------------------------------------------------------- /jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /jest/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const fs = require('fs-extra'); 4 | 5 | module.exports = { 6 | projects: [ 7 | { 8 | displayName: 'lint', 9 | runner: 'eslint', 10 | rootDir: process.cwd(), 11 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean), 12 | testMatch: [ 13 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 14 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}' 15 | ] 16 | }, 17 | { 18 | displayName: 'test', 19 | rootDir: process.cwd(), 20 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean), 21 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], 22 | setupFiles: [], 23 | setupFilesAfterEnv: ['/jest/setupTests.ts'], 24 | testMatch: [ 25 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 26 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}' 27 | ], 28 | testEnvironment: 'jest-environment-jsdom-fourteen', 29 | transform: { 30 | '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest', 31 | '^.+\\.(css|less|sass|scss$)': '/jest/cssTransform.js', 32 | '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|json)$)': '/jest/fileTransform.js' 33 | }, 34 | transformIgnorePatterns: [ 35 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', 36 | '^.+\\.module\\.(css|sass|scss|less)$' 37 | ], 38 | modulePaths: [], 39 | moduleNameMapper: { 40 | '^react-native$': 'react-native-web', 41 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy' 42 | }, 43 | moduleFileExtensions: ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'], 44 | verbose: true, 45 | // resetMocks: true, 46 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'] 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /jest/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /jest/test.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'test'; 3 | process.env.NODE_ENV = 'test'; 4 | process.env.PUBLIC_URL = ''; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | const jest = require('jest'); 14 | const execSync = require('child_process').execSync; 15 | let argv = process.argv.slice(2); 16 | 17 | argv.push('-c', 'jest/jest.config.js'); 18 | 19 | function isInGitRepository() { 20 | try { 21 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 22 | 23 | return true; 24 | } catch (e) { 25 | return false; 26 | } 27 | } 28 | 29 | function isInMercurialRepository() { 30 | try { 31 | execSync('hg --cwd . root', { stdio: 'ignore' }); 32 | 33 | return true; 34 | } catch (e) { 35 | return false; 36 | } 37 | } 38 | 39 | // Watch unless on CI or explicitly running all tests 40 | if (!process.env.CI && argv.indexOf('--watchAll') === -1 && argv.indexOf('--watchAll=false') === -1) { 41 | // https://github.com/facebook/create-react-app/issues/5210 42 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 43 | 44 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 45 | } 46 | 47 | jest.run(argv); 48 | -------------------------------------------------------------------------------- /npm/index.cjs.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.cjs.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.cjs.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /npm/index.esm.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.esm.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.esm.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /npm/index.umd.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-formutil.umd.production.js'); 3 | } else { 4 | module.exports = require('./react-formutil.umd.development.js'); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-formutil", 3 | "version": "1.1.9", 4 | "description": "Happy to build the forms in React ^_^", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "directories": { 8 | "doc": "docs" 9 | }, 10 | "entryFile": "src/index.js", 11 | "scripts": { 12 | "start": "cd docs && npm start", 13 | "build": "npm run clear && rollup -c", 14 | "clear": "rimraf dist", 15 | "test": "node jest/test.js", 16 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --noEmit --checkJs false" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/qiqiboy/react-formutil.git" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "react-components", 25 | "react-form", 26 | "react-formutil", 27 | "forms", 28 | "create-form", 29 | "umd" 30 | ], 31 | "author": "qiqiboy", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/qiqiboy/react-formutil/issues" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest", 40 | "./eslint.config.js" 41 | ] 42 | }, 43 | "prettier": { 44 | "printWidth": 120, 45 | "tabWidth": 4, 46 | "trailingComma": "none", 47 | "jsxBracketSameLine": true, 48 | "semi": true, 49 | "arrowParens": "avoid", 50 | "singleQuote": true, 51 | "overrides": [ 52 | { 53 | "files": "*.json", 54 | "options": { 55 | "tabWidth": 2 56 | } 57 | } 58 | ] 59 | }, 60 | "homepage": "https://github.com/qiqiboy/react-formutil#readme", 61 | "peerDependencies": { 62 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0", 63 | "prop-types": "^15.0.0" 64 | }, 65 | "types": "./index.d.ts", 66 | "devDependencies": { 67 | "@babel/cli": "7.12.1", 68 | "@babel/core": "7.12.3", 69 | "@commitlint/cli": "11.0.0", 70 | "@commitlint/config-conventional": "11.0.0", 71 | "@rollup/plugin-babel": "5.2.1", 72 | "@rollup/plugin-commonjs": "11.1.0", 73 | "@rollup/plugin-eslint": "8.0.0", 74 | "@rollup/plugin-node-resolve": "10.0.0", 75 | "@rollup/plugin-replace": "2.3.4", 76 | "@testing-library/jest-dom": "5.11.5", 77 | "@testing-library/react": "11.1.2", 78 | "@testing-library/user-event": "12.2.2", 79 | "@types/jest": "26.0.15", 80 | "@types/node": "14.14.7", 81 | "@types/react": "16.9.23", 82 | "@types/react-dom": "16.9.5", 83 | "@types/react-is": "16.7.1", 84 | "@typescript-eslint/eslint-plugin": "4.7.0", 85 | "@typescript-eslint/parser": "4.7.0", 86 | "babel-eslint": "10.1.0", 87 | "babel-jest": "26.6.3", 88 | "babel-preset-react-app": "10.0.0", 89 | "eslint": "7.13.0", 90 | "eslint-config-react-app": "6.0.0", 91 | "eslint-plugin-flowtype": "5.2.0", 92 | "eslint-plugin-import": "2.22.1", 93 | "eslint-plugin-jest": "24.1.3", 94 | "eslint-plugin-jsx-a11y": "6.4.1", 95 | "eslint-plugin-react": "7.21.5", 96 | "eslint-plugin-react-hooks": "4.2.0", 97 | "eslint-plugin-testing-library": "3.10.0", 98 | "husky": "3.1.0", 99 | "identity-obj-proxy": "3.0.0", 100 | "jest": "26.6.3", 101 | "jest-environment-jsdom-fourteen": "1.0.1", 102 | "jest-resolve": "26.6.2", 103 | "jest-runner-eslint": "0.10.0", 104 | "jest-watch-typeahead": "0.4.2", 105 | "lint-staged": "10.5.1", 106 | "prettier": "2.1.2", 107 | "prop-types": "15.7.2", 108 | "react": "17.0.1", 109 | "react-app-polyfill": "^1.0.6", 110 | "react-dom": "17.0.1", 111 | "react-hot-loader": "^4.12.19", 112 | "rimraf": "3.0.2", 113 | "rollup": "2.33.1", 114 | "rollup-plugin-clear": "^2.0.7", 115 | "rollup-plugin-copy": "3.3.0", 116 | "rollup-plugin-filesize": "9.0.2", 117 | "rollup-plugin-sass": "1.2.2", 118 | "rollup-plugin-terser": "7.0.2", 119 | "typescript": "5.2.2" 120 | }, 121 | "dependencies": { 122 | "hoist-non-react-statics": "^3.3.2", 123 | "react-fast-compare": "^3.0.1", 124 | "warning": "^4.0.3" 125 | }, 126 | "sideEffects": false, 127 | "husky": { 128 | "hooks": { 129 | "commit-msg": "node_modules/.bin/commitlint --edit $HUSKY_GIT_PARAMS", 130 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --diff-filter AM --name-only --relative --staged | grep -E '^src/.*\\.m?[jt]sx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc ; fi" 131 | } 132 | }, 133 | "lint-staged": { 134 | "src/**/*.{js,jsx,mjs,ts,tsx}": [ 135 | "node_modules/.bin/prettier --write", 136 | "node_modules/.bin/eslint --fix" 137 | ], 138 | "src/**/*.{css,scss,less,json,html,md}": [ 139 | "node_modules/.bin/prettier --write" 140 | ] 141 | }, 142 | "stylelint": { 143 | "extends": "stylelint-config-recommended" 144 | }, 145 | "browserslist": [ 146 | ">0.2%", 147 | "not dead", 148 | "not op_mini all" 149 | ], 150 | "config": { 151 | "commitizen": { 152 | "path": "cz-conventional-changelog" 153 | } 154 | }, 155 | "engines": { 156 | "node": ">=10.13.0", 157 | "tiger-new": "6.2.7" 158 | }, 159 | "commitlint": { 160 | "extends": [ 161 | "@commitlint/config-conventional" 162 | ], 163 | "rules": { 164 | "subject-case": [ 165 | 0 166 | ], 167 | "scope-case": [ 168 | 0 169 | ] 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const commonjs = require('@rollup/plugin-commonjs'); 6 | const replace = require('@rollup/plugin-replace'); 7 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 8 | const babel = require('@rollup/plugin-babel').default; 9 | const filesize = require('rollup-plugin-filesize'); 10 | const copy = require('rollup-plugin-copy'); 11 | const sass = require('rollup-plugin-sass'); 12 | const { terser } = require('rollup-plugin-terser'); 13 | const eslint = require('@rollup/plugin-eslint'); 14 | const pkg = require('./package.json'); 15 | 16 | /** 17 | * 如果希望将某些模块代码直接构建进输出文件,可以再这里指定这些模块名称 18 | */ 19 | const externalExclude = [ 20 | '@babel/runtime', 'regenerator-runtime' 21 | ]; 22 | 23 | const exportName = pkg.exportName || pkg.name.split('/').slice(-1)[0]; 24 | /** 25 | * 如果你希望编译后的代码里依然自动包含进去编译后的css,那么这里可以设置为 true 26 | */ 27 | const shouldPreserveCss = false; 28 | 29 | function createConfig(env, module) { 30 | const isProd = env === 'production'; 31 | // for umd globals 32 | const globals = { 33 | react: 'React', 34 | 'react-dom': 'ReactDOM', 35 | 'prop-types': 'PropTypes' 36 | }; 37 | 38 | return { 39 | /** 40 | * 入口文件位置,如果你更改了entryFile,别忘了同时修改 npm/index.cjs.js 和 npm/index.esm.js 里的文件引用名称 41 | */ 42 | input: pkg.entryFile || 'src/index.ts', 43 | external: 44 | module === 'umd' 45 | ? Object.keys(globals) 46 | : id => 47 | !externalExclude.some(name => id.startsWith(name)) && !id.startsWith('.') && !path.isAbsolute(id), 48 | output: { 49 | name: 'ReactFormutil', 50 | file: `dist/${exportName}.${module}.${env}.js`, 51 | format: module, 52 | exports: 'named', 53 | sourcemap: false, 54 | intro: 55 | module !== 'umd' && shouldPreserveCss 56 | ? module === 'cjs' 57 | ? `require('./${exportName}.css');` 58 | : `import('./${exportName}.css');` 59 | : undefined, 60 | globals 61 | }, 62 | treeshake: { 63 | /** 64 | * 如果你有引入一些有副作用的代码模块,或者构建后的代码运行异常,可以尝试将该项设置为 true 65 | */ 66 | moduleSideEffects: false 67 | }, 68 | plugins: [ 69 | eslint({ 70 | fix: true, 71 | throwOnError: true, 72 | throwOnWarning: true 73 | }), 74 | nodeResolve({ 75 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 76 | }), 77 | commonjs({ 78 | include: /node_modules/ 79 | }), 80 | replace({ 81 | 'process.env.NODE_ENV': JSON.stringify(env) 82 | }), 83 | babel({ 84 | exclude: 'node_modules/**', 85 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 86 | babelHelpers: 'runtime', 87 | babelrc: false, 88 | configFile: false, 89 | presets: [ 90 | [ 91 | '@babel/preset-env', 92 | { 93 | useBuiltIns: 'entry', 94 | corejs: 3, 95 | modules: false, 96 | exclude: ['transform-typeof-symbol'] 97 | } 98 | ], 99 | [ 100 | '@babel/preset-react', 101 | { 102 | development: false, 103 | useBuiltIns: true, 104 | runtime: 'classic' 105 | } 106 | ], 107 | ['@babel/preset-typescript'] 108 | ], 109 | plugins: [ 110 | 'babel-plugin-macros', 111 | ['@babel/plugin-proposal-decorators', { legacy: true }], 112 | [ 113 | '@babel/plugin-proposal-class-properties', 114 | { 115 | loose: true 116 | } 117 | ], 118 | [ 119 | '@babel/plugin-transform-runtime', 120 | { 121 | version: require('@babel/helpers/package.json').version, 122 | corejs: false, 123 | helpers: true, 124 | regenerator: true, 125 | useESModules: module === 'esm', 126 | absoluteRuntime: false 127 | } 128 | ], 129 | isProd && [ 130 | // Remove PropTypes from production build 131 | 'babel-plugin-transform-react-remove-prop-types', 132 | { 133 | removeImport: true 134 | } 135 | ], 136 | require('@babel/plugin-proposal-optional-chaining').default, 137 | require('@babel/plugin-proposal-nullish-coalescing-operator').default, 138 | // Adds Numeric Separators 139 | require('@babel/plugin-proposal-numeric-separator').default 140 | ].filter(Boolean) 141 | }), 142 | module !== 'umd' && 143 | sass({ 144 | output: `dist/${exportName}.css` 145 | }), 146 | isProd && 147 | terser({ 148 | output: { comments: false }, 149 | compress: false, 150 | warnings: false, 151 | ecma: 5, 152 | ie8: false, 153 | toplevel: true 154 | }), 155 | filesize(), 156 | copy({ 157 | targets: [ 158 | { 159 | src: `npm/index.${module}.js`, 160 | dest: 'dist' 161 | } 162 | ], 163 | verbose: false 164 | }) 165 | ].filter(Boolean) 166 | }; 167 | } 168 | 169 | module.exports = ['cjs', 'esm', 'umd'].reduce((configQueue, module) => { 170 | return fs.existsSync(`./npm/index.${module}.js`) 171 | ? configQueue.concat(createConfig('development', module), createConfig('production', module)) 172 | : configQueue; 173 | }, []); 174 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/EasyField/Group.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Children, cloneElement, createContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import warning from 'warning'; 4 | import { isFunction } from '../utils'; 5 | 6 | /** @type {any} */ 7 | const { Provider, Consumer } = createContext(() => ({})); 8 | 9 | class EasyFieldGroup extends Component { 10 | static displayName = 'React.Formutil.EasyField.Group'; 11 | 12 | static propTypes = { 13 | onChange: PropTypes.func, 14 | onFocus: PropTypes.func, 15 | onBlur: PropTypes.func, 16 | 17 | value: PropTypes.any, 18 | name: PropTypes.string, 19 | type: PropTypes.string.isRequired, 20 | groupNode: PropTypes.any, 21 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired 22 | }; 23 | 24 | static defaultProps = { 25 | type: 'checkbox', 26 | groupNode: 'div' 27 | }; 28 | 29 | getGroupContext = () => this.props; 30 | 31 | _render() { 32 | const { className, groupNode: Element, children } = this.props; 33 | 34 | const GroupOptionProps = { 35 | GroupOption: EasyFieldGroupOption, 36 | Field: DeprecatedEasyFieldGroupOption 37 | }; 38 | 39 | const childNodes = isFunction(children) 40 | ? children(GroupOptionProps) 41 | : Children.map(children, child => cloneElement(child, GroupOptionProps)); 42 | 43 | if (Element === null) { 44 | return childNodes; 45 | } 46 | 47 | return {childNodes}; 48 | } 49 | 50 | render() { 51 | return {this._render()}; 52 | } 53 | } 54 | 55 | class EasyFieldGroupOption extends Component { 56 | static displayName = 'React.Formutil.EasyField.Group.Option'; 57 | 58 | static propTypes = { 59 | $value: PropTypes.any.isRequired 60 | }; 61 | 62 | componentDidMount() { 63 | warning('$value' in this.props, `You should pass a $value to .`); 64 | } 65 | 66 | render() { 67 | const { $value, onChange, onFocus, onBlur, ...others } = this.props; 68 | 69 | return ( 70 | 71 | {getGroupContext => { 72 | const $groupHandler = getGroupContext(); 73 | const { type, name } = $groupHandler; 74 | 75 | const elemProps = 76 | type === 'radio' 77 | ? { 78 | checked: $groupHandler.value === $value, 79 | onChange: ev => { 80 | $groupHandler.onChange($value, ev); 81 | 82 | onChange && onChange(ev); 83 | } 84 | } 85 | : type === 'checkbox' 86 | ? { 87 | checked: $groupHandler.value.indexOf($value) > -1, 88 | onChange: ev => { 89 | $groupHandler.onChange( 90 | ev.target.checked 91 | ? $groupHandler.value.concat($value) 92 | : $groupHandler.value.filter(value => value !== $value), 93 | ev 94 | ); 95 | 96 | onChange && onChange(ev); 97 | } 98 | } 99 | : { 100 | value: $groupHandler.value, 101 | onChange: ev => { 102 | $groupHandler.onChange(ev); 103 | 104 | onChange && onChange(ev); 105 | } 106 | }; 107 | 108 | return ( 109 | { 115 | $groupHandler.onFocus(ev); 116 | onFocus && onFocus(ev); 117 | }} 118 | onBlur={ev => { 119 | $groupHandler.onBlur(ev); 120 | onBlur && onBlur(ev); 121 | }} 122 | /> 123 | ); 124 | }} 125 | 126 | ); 127 | } 128 | } 129 | 130 | class DeprecatedEasyFieldGroupOption extends Component { 131 | static displayName = 'React.Formutil.EasyField.Group.Option.Deprecated'; 132 | 133 | componentDidMount() { 134 | warning( 135 | false, 136 | `The "Field" property in EasyField's children-props has been deprecated. Please use "GroupOption" instead.` 137 | ); 138 | } 139 | 140 | render() { 141 | return ; 142 | } 143 | } 144 | 145 | export default EasyFieldGroup; 146 | -------------------------------------------------------------------------------- /src/EasyField/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import isEqual from 'react-fast-compare'; 4 | import Form from '../Form'; 5 | import Field from '../Field'; 6 | import { isUndefined, isFunction, runCallback } from '../utils'; 7 | 8 | const Wrapper = React.Frament || 'div'; 9 | 10 | class EasyFieldList extends Component { 11 | static displayName = 'React.Formutil.EasyField.List'; 12 | 13 | static propTypes = { 14 | onChange: PropTypes.func, 15 | onFocus: PropTypes.func, 16 | onBlur: PropTypes.func, 17 | value: PropTypes.array, 18 | 19 | children: PropTypes.func.isRequired 20 | }; 21 | 22 | id = 0; 23 | latestValue = this.props.value; 24 | $formutil; 25 | 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | items: props.value.length ? props.value.map(() => this.getId()) : [this.getId()], 31 | formKey: 0 32 | }; 33 | } 34 | 35 | componentDidUpdate(prevProps) { 36 | if (this.props.value !== this.latestValue) { 37 | this.setState({ 38 | items: this.props.value.length ? this.props.value.map(() => this.getId()) : [this.getId()], 39 | formKey: this.state.formKey + 1 40 | }); 41 | 42 | this.latestValue = this.props.value; 43 | } 44 | } 45 | 46 | getId(values) { 47 | return { 48 | id: this.id++, 49 | values 50 | }; 51 | } 52 | 53 | FieldValidators = { 54 | required(value) { 55 | return value !== null; 56 | } 57 | }; 58 | 59 | $onFormChange = $formutil => { 60 | $formutil.$onValidates($formutil => { 61 | const { $invalid, $params } = $formutil; 62 | 63 | if ($invalid) { 64 | if (this.props.value.length) { 65 | this.props.onChange((this.latestValue = [])); 66 | } 67 | } else if (!isEqual(this.props.value, $params.list)) { 68 | this.props.onChange((this.latestValue = $params.list)); 69 | } 70 | }); 71 | }; 72 | 73 | swap = (m, n, callback) => 74 | this.$setState(({ items }) => { 75 | [items[n], items[m]] = [items[m], items[n]]; 76 | 77 | return items; 78 | }, callback); 79 | 80 | insert = (...args) => { 81 | let m, values, callback; 82 | 83 | args.forEach(arg => { 84 | if (isFunction(arg)) { 85 | callback = arg; 86 | } else if (typeof arg === 'number') { 87 | m = arg; 88 | } else if (typeof arg === 'object') { 89 | values = arg; 90 | } 91 | }); 92 | 93 | return this.$setState(({ items }) => { 94 | if (isUndefined(m)) { 95 | items.push(this.getId(values)); 96 | } else { 97 | items.splice(m, 0, this.getId(values)); 98 | } 99 | 100 | return { items }; 101 | }, callback); 102 | }; 103 | 104 | remove = (...args) => { 105 | let m, callback; 106 | 107 | args.forEach(arg => { 108 | if (isFunction(arg)) { 109 | callback = arg; 110 | } else if (typeof arg === 'number') { 111 | m = arg; 112 | } 113 | }); 114 | 115 | return this.$setState(({ items }) => { 116 | if (isUndefined(m)) { 117 | items.pop(); 118 | } else { 119 | items.splice(m, 1); 120 | } 121 | 122 | if (!items.length) { 123 | items = [this.getId()]; 124 | } 125 | 126 | return { items }; 127 | }, callback); 128 | }; 129 | 130 | $setState = (updater, callback) => 131 | new Promise(resolve => 132 | this.setState(updater, () => 133 | this.$formutil.$onValidates($formutil => resolve(runCallback(callback, $formutil))) 134 | ) 135 | ); 136 | 137 | render() { 138 | const { children, onFocus, onBlur, value } = this.props; 139 | const $self = this; 140 | 141 | if (!isFunction(children)) { 142 | return null; 143 | } 144 | 145 | const $baseutil = { 146 | $insert: this.insert, 147 | $remove: this.remove, 148 | $swap: this.swap, 149 | $push: (values, callback) => this.insert(values, callback), 150 | $pop: callback => this.remove(callback), 151 | $shift: callback => this.remove(0, callback), 152 | $unshift: (values, callback) => this.insert(0, values, callback), 153 | onFocus, 154 | onBlur 155 | }; 156 | 157 | return ( 158 |
{ 165 | this.$formutil = $formutil; 166 | 167 | return ( 168 | 169 | {this.state.items.map(({ id, values }, index) => ( 170 | { 177 | return ( 178 | 181 | $formutil.$onValidates($formutil => { 182 | const { $invalid, $params } = $formutil; 183 | 184 | if (!isEqual($fieldutil.$viewValue, $params)) { 185 | $fieldutil.$setState({ 186 | $viewValue: $params, 187 | $value: $invalid ? null : $params 188 | }); 189 | } 190 | }) 191 | } 192 | children={$innerFormutil => 193 | children( 194 | { 195 | get $length() { 196 | return $self.state.items.length; 197 | }, 198 | $index: index, 199 | $isLast: () => index === this.state.items.length - 1, 200 | $isFirst: () => index === 0, 201 | ...$baseutil, 202 | ...$innerFormutil 203 | }, 204 | $formutil 205 | ) 206 | } 207 | /> 208 | ); 209 | }} 210 | /> 211 | ))} 212 | 213 | ); 214 | }} 215 | /> 216 | ); 217 | } 218 | } 219 | 220 | export default EasyFieldList; 221 | -------------------------------------------------------------------------------- /src/EasyField/Native.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class EasyFieldNative extends Component { 5 | static displayName = 'React.Formutil.EasyField.Native'; 6 | 7 | static propTypes = { 8 | onChange: PropTypes.func, 9 | onFocus: PropTypes.func, 10 | onBlur: PropTypes.func, 11 | 12 | value: PropTypes.any, 13 | name: PropTypes.string, 14 | type: PropTypes.string, 15 | 16 | checked: PropTypes.any, 17 | unchekced: PropTypes.any 18 | }; 19 | 20 | static defaultProps = { 21 | value: '', 22 | type: 'text', 23 | checked: true, 24 | unchecked: false 25 | }; 26 | 27 | render() { 28 | const { $fieldutil, value: htmlValue, onChange, onFocus, onBlur, checked, unchecked, ...others } = this.props; 29 | const htmlType = this.props.type; 30 | 31 | let htmlProps = { 32 | value: 'compositionValue' in this ? this.compositionValue : htmlValue, 33 | onCompositionEnd: ev => { 34 | this.isComposing = false; 35 | delete this.compositionValue; 36 | htmlProps.onChange(ev); 37 | }, 38 | onCompositionStart: () => (this.isComposing = true), 39 | onChange: ev => { 40 | const { value } = ev.target; 41 | 42 | if (this.isComposing) { 43 | this.compositionValue = value; 44 | this.forceUpdate(); 45 | } else { 46 | onChange(value, ev); 47 | } 48 | }, 49 | onFocus, 50 | onBlur: ev => { 51 | if (this.isComposing) { 52 | this.isComposing = false; 53 | delete this.compositionValue; 54 | htmlProps.onChange(ev); 55 | } 56 | 57 | return onBlur(ev); 58 | } 59 | }; 60 | let Element = 'input'; 61 | 62 | switch (htmlType) { 63 | case 'select': 64 | Element = htmlType; 65 | 66 | htmlProps.onChange = ev => { 67 | const node = ev.target; 68 | const value = node.multiple 69 | ? [].slice 70 | .call(node.options) 71 | .filter(option => option.selected) 72 | .map(option => option.value) 73 | : node.value; 74 | 75 | onChange(value, ev); 76 | }; 77 | 78 | delete others.type; 79 | 80 | break; 81 | case 'textarea': 82 | Element = htmlType; 83 | delete others.type; 84 | break; 85 | 86 | case 'checkbox': 87 | case 'radio': 88 | htmlProps = { 89 | checked: htmlValue === checked, 90 | onChange: ev => { 91 | onChange(ev.target.checked ? checked : unchecked, ev); 92 | }, 93 | onFocus, 94 | onBlur 95 | }; 96 | 97 | break; 98 | 99 | default: 100 | break; 101 | } 102 | 103 | return ; 104 | } 105 | } 106 | 107 | export default EasyFieldNative; 108 | -------------------------------------------------------------------------------- /src/EasyField/easyFieldHandler.js: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, createElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Native from './Native'; 4 | import Group from './Group'; 5 | import List from './List'; 6 | import { isEmpty, isUndefined, isFunction, isValidProp, checkComponentPropType } from '../utils'; 7 | 8 | export const TYPE = '__TYPE__'; 9 | export const defaultValidators = [ 10 | [ 11 | 'required', 12 | ($value, check, { __TYPE__, checked = true }) => 13 | check === false || (__TYPE__ === 'checked' ? $value === checked : !isEmpty($value)) 14 | ], 15 | ['maxLength', ($value, len) => isEmpty($value) || $value.length <= len * 1], 16 | ['minLength', ($value, len) => isEmpty($value) || $value.length >= len * 1], 17 | ['max', ($value, limit) => isEmpty($value) || $value * 1 <= limit * 1], 18 | ['min', ($value, limit) => isEmpty($value) || $value * 1 >= limit * 1], 19 | ['pattern', ($value, regexp) => isEmpty($value) || regexp.test($value)], 20 | ['enum', ($value, enumeration) => isEmpty($value) || enumeration.indexOf($value) > -1], 21 | ['checker', ($value, checker, props) => checker($value, props)] 22 | ].reduce(($validators, item) => { 23 | const [validKey, validate] = item; 24 | 25 | $validators[validKey] = function validator($value, propValue, { validMessage = {} }) { 26 | return validate(...arguments) || validMessage[validKey] || `Error input: ${validKey}`; 27 | }; 28 | 29 | return $validators; 30 | }, {}); 31 | 32 | export const propTypes = 33 | process.env.NODE_ENV !== 'production' 34 | ? { 35 | type: PropTypes.string, 36 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), 37 | component: checkComponentPropType, 38 | render: PropTypes.func, 39 | 40 | defaultValue: PropTypes.any, 41 | validMessage: PropTypes.object, 42 | 43 | valuePropName: PropTypes.string, 44 | changePropName: PropTypes.string, 45 | focusPropName: PropTypes.string, 46 | blurPropName: PropTypes.string, 47 | getValueFromEvent: PropTypes.func, 48 | 49 | passUtil: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) 50 | } 51 | : undefined; 52 | 53 | export const displayName = 'React.Formutil.EasyField'; 54 | 55 | export const defaultProps = { 56 | validMessage: {}, 57 | valuePropName: 'value', 58 | changePropName: 'onChange', 59 | focusPropName: 'onFocus', 60 | blurPropName: 'onBlur', 61 | $parser: value => (typeof value === 'string' ? value.trim() : value) 62 | }; 63 | 64 | export function createHandler($fieldutil, fieldProps, childProps) { 65 | const { valuePropName, changePropName, focusPropName, blurPropName, getValueFromEvent, passUtil } = fieldProps; 66 | 67 | const fetchValueFromEvent = function (ev) { 68 | return ev && ev.target ? ev.target[valuePropName] : ev; 69 | }; 70 | 71 | const $handleProps = { 72 | ...childProps, 73 | 74 | [valuePropName]: $fieldutil.$viewValue, 75 | [changePropName]: (value, ...events) => { 76 | if (events[0]?.nativeEvent instanceof Event) { 77 | events.push(value); 78 | } else { 79 | events.unshift(value); 80 | } 81 | 82 | const onChange = fieldProps[changePropName]; 83 | 84 | onChange && onChange(...events); 85 | 86 | const newValue = getValueFromEvent ? getValueFromEvent(...events) : fetchValueFromEvent(value); 87 | 88 | $fieldutil.$render(newValue); 89 | }, 90 | [focusPropName]: (...args) => { 91 | const onFocus = fieldProps[focusPropName]; 92 | 93 | onFocus && onFocus(...args); 94 | 95 | $fieldutil.$setFocused(true); 96 | }, 97 | [blurPropName]: (...args) => { 98 | const onBlur = fieldProps[blurPropName]; 99 | 100 | onBlur && onBlur(...args); 101 | 102 | if ($fieldutil.$untouched) { 103 | $fieldutil.$setTouched(true); 104 | } 105 | 106 | $fieldutil.$setFocused(false); 107 | } 108 | }; 109 | 110 | if (passUtil) { 111 | $handleProps[passUtil === true ? '$fieldutil' : String(passUtil)] = $fieldutil; 112 | } 113 | 114 | return $handleProps; 115 | } 116 | 117 | export function parseProps(props) { 118 | const { 119 | children, 120 | component, 121 | render, 122 | 123 | ...fieldProps 124 | } = props; 125 | 126 | const { 127 | // filter all the props that accept by EasyField 128 | name, 129 | type, 130 | defaultValue, 131 | valuePropName, 132 | changePropName, 133 | focusPropName, 134 | blurPropName, 135 | getValueFromEvent, 136 | validMessage, 137 | checked, 138 | unchecked, 139 | __TYPE__, 140 | __DIFF__, 141 | passUtil, 142 | 143 | // filter all the props that accept by Field 144 | $defaultValue, 145 | $defaultState, 146 | $onFieldChange, 147 | $validators, 148 | $asyncValidators, 149 | $validateLazy, 150 | $memo, 151 | $reserveOnUnmount, 152 | $parser, 153 | $formatter, 154 | $ref, 155 | 156 | ...childProps 157 | } = fieldProps; 158 | 159 | const renderProps = { 160 | children, 161 | component, 162 | render 163 | }; 164 | 165 | if ($memo === true && isUndefined(__DIFF__)) { 166 | fieldProps.__DIFF__ = [children, component, render]; 167 | } 168 | 169 | const isNative = !isUndefined(type) || (isUndefined(children) && isUndefined(component) && isUndefined(render)); 170 | 171 | Object.keys({ 172 | ...(fieldProps.$validators = { 173 | ...defaultValidators, 174 | ...fieldProps.$validators 175 | }), 176 | ...fieldProps.$asyncValidators 177 | }).forEach(prop => { 178 | if (prop in childProps) { 179 | if (!isNative || !isValidProp(prop)) { 180 | delete childProps[prop]; 181 | } 182 | } 183 | }); 184 | 185 | if (isNative) { 186 | const [htmlType = 'text', groupType] = (type || '').split('.'); 187 | 188 | renderProps.component = htmlType === 'group' ? Group : htmlType === 'list' ? List : Native; 189 | 190 | // Native or Group need to pass 'name' | 'type' | 'children' 191 | if (name) { 192 | childProps.name = name; 193 | } 194 | 195 | if (type) { 196 | childProps.type = htmlType; 197 | } 198 | 199 | if (children) { 200 | childProps.children = children; 201 | } 202 | 203 | childProps.checked = checked; 204 | childProps.unchecked = unchecked; 205 | 206 | switch (htmlType) { 207 | case 'select': 208 | case 'textarea': 209 | if (props.multiple) { 210 | fieldProps[TYPE] = 'array'; 211 | } 212 | 213 | break; 214 | 215 | case 'group': 216 | if (groupType === 'checkbox') { 217 | fieldProps[TYPE] = 'array'; 218 | } 219 | 220 | childProps.type = groupType; 221 | break; 222 | 223 | case 'checkbox': 224 | case 'radio': 225 | fieldProps[TYPE] = 'checked'; 226 | break; 227 | 228 | case 'list': 229 | fieldProps[TYPE] = 'array'; 230 | break; 231 | 232 | default: 233 | break; 234 | } 235 | } 236 | 237 | if (!('$defaultValue' in fieldProps) && 'defaultValue' in props) { 238 | fieldProps.$defaultValue = defaultValue; 239 | } 240 | 241 | if (!('$defaultValue' in fieldProps) && TYPE in fieldProps) { 242 | let defaultValue; 243 | 244 | switch (fieldProps[TYPE]) { 245 | case 'checked': 246 | const { unchecked = false } = fieldProps; 247 | 248 | defaultValue = unchecked; 249 | break; 250 | 251 | case 'array': 252 | defaultValue = []; 253 | break; 254 | 255 | case 'object': 256 | defaultValue = {}; 257 | break; 258 | 259 | case 'number': 260 | defaultValue = 0; 261 | break; 262 | 263 | case 'empty': 264 | default: 265 | break; 266 | } 267 | 268 | fieldProps.$defaultValue = defaultValue; 269 | } 270 | 271 | return { 272 | fieldProps, 273 | childProps, 274 | renderProps 275 | }; 276 | } 277 | 278 | export function renderField($handleProps, renderprops) { 279 | let { component, render, children } = renderprops; 280 | 281 | if (component) { 282 | return createElement(component, $handleProps); 283 | } 284 | 285 | if (isFunction(render)) { 286 | return render($handleProps); 287 | } 288 | 289 | if (isFunction(children)) { 290 | return children($handleProps); 291 | } 292 | 293 | return Children.map(children, child => cloneElement(child, $handleProps)); 294 | } 295 | -------------------------------------------------------------------------------- /src/EasyField/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Field from '../Field'; 3 | import { renderField, createHandler, parseProps, displayName, propTypes, defaultProps } from './easyFieldHandler'; 4 | 5 | /** 6 | * 提供对浏览器原生表单控件的封装 7 | * 支持以下类型表单元素: 8 | * - input[type=xx] 9 | * - textarea 10 | * - select 11 | */ 12 | class EasyField extends Component { 13 | static displayName = displayName; 14 | static propTypes = propTypes; 15 | static defaultProps = defaultProps; 16 | 17 | renderChildren = $fieldutil => { 18 | const { fieldProps, childProps, renderProps } = this.parsedProps; 19 | 20 | return renderField(createHandler($fieldutil, fieldProps, childProps), renderProps); 21 | }; 22 | 23 | parsedProps = {}; 24 | 25 | render() { 26 | const { fieldProps } = (this.parsedProps = parseProps(this.props)); 27 | 28 | return ; 29 | } 30 | } 31 | 32 | export default EasyField; 33 | -------------------------------------------------------------------------------- /src/Field.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import warning from 'warning'; 3 | import isEqual from 'react-fast-compare'; 4 | import { createHandler, GET_FIELD_UUID, propTypes, displayName, renderField } from './fieldHelper'; 5 | import FormContext from './context'; 6 | import { runCallback, createRef, isStateEqual } from './utils'; 7 | 8 | class Field extends Component { 9 | static displayName = displayName; 10 | static propTypes = propTypes; 11 | 12 | $$FIELD_UUID = GET_FIELD_UUID(); 13 | 14 | /** @type { any } */ 15 | $formContext; 16 | /** @type { any } */ 17 | $state; 18 | 19 | shouldRendered = false; 20 | 21 | componentDidMount() { 22 | this.isMounting = true; 23 | 24 | const { 25 | props: { name: $name }, 26 | $formContext 27 | } = this; 28 | 29 | warning( 30 | !$name || $formContext.$formutil, 31 | `You should enusre that the with the name '${$name}' must be used underneath a component or withForm() HOC, otherwise it's isolated.` 32 | ); 33 | 34 | warning($name, `You should assign a name to , otherwise it will be isolated!`); 35 | 36 | if ($formContext.$$register) { 37 | $formContext.$$register($name, this.$fieldHandler); 38 | } 39 | 40 | this.$prevState = this.$state; 41 | 42 | createRef(this.props.$ref, this.$fieldutil); 43 | } 44 | 45 | componentWillUnmount() { 46 | if (this.$formContext.$$unregister) { 47 | this.$formContext.$$unregister(this.props.name, this.$fieldHandler, this.props.$reserveOnUnmount); 48 | } 49 | 50 | this.isMounting = false; 51 | 52 | createRef(this.props.$ref, null); 53 | } 54 | 55 | componentDidUpdate(prevProps) { 56 | const $name = this.props.name; 57 | 58 | if ($name !== prevProps.name) { 59 | if (this.$formContext.$$register) { 60 | this.$formContext.$$register($name, this.$fieldHandler, prevProps.name); 61 | } 62 | } 63 | 64 | createRef(this.props.$ref, this.$fieldutil); 65 | 66 | if (this.$state.$value !== this.$prevState.$value) { 67 | if (!($name in (this.$formContext.$$registers || {}))) { 68 | this.$registered.$$triggerChange({ 69 | $newValue: this.$state.$value, 70 | $prevValue: this.$prevState.$value 71 | }); 72 | } 73 | } 74 | 75 | this.$prevState = this.$state; 76 | } 77 | 78 | shouldComponentUpdate(nextProps) { 79 | const { $memo } = nextProps; 80 | 81 | return ( 82 | !$memo || 83 | /** 84 | * 这里不能用isEqual深度比较,避免遇到$value为大数据时导致性能问题 85 | * isStateEqual只比较一层 86 | */ 87 | !isStateEqual(this.$registered.$getState(), this.$prevState) || 88 | !(Array.isArray($memo) ? isEqual($memo, this.props.$memo) : isEqual(this.props, nextProps)) 89 | ); 90 | } 91 | 92 | $setState = ($newState, callback) => 93 | new Promise(resolve => { 94 | const execute = () => resolve(runCallback(callback, this.$fieldutil)); 95 | 96 | if (this.isMounting) { 97 | const $name = this.props.name; 98 | 99 | if ($name in (this.$formContext.$$registers || {})) { 100 | this.shouldRendered = false; 101 | this.$formContext.$$onChange($name, $newState, execute); 102 | 103 | /** 104 | * Ensure Field could rerender if has been cached. In others words, it's vdomEq always true, 105 | * render not trigger Field rerender 106 | */ 107 | if (!this.shouldRendered) { 108 | this.forceUpdate(); 109 | } 110 | } else { 111 | this.$registered.$$merge($newState); 112 | this.$registered.$$detectChange($newState); 113 | 114 | this.forceUpdate(execute); 115 | } 116 | } else { 117 | this.$registered.$$merge($newState); 118 | execute(); 119 | } 120 | }); 121 | 122 | _render() { 123 | const $fieldutil = (this.$fieldutil = { 124 | $name: this.props.name, 125 | ...this.$registered.$getState(), 126 | ...this.$registered, 127 | $$formutil: this.$formContext.$formutil 128 | }); 129 | 130 | return renderField($fieldutil, this.props); 131 | } 132 | 133 | render() { 134 | this.shouldRendered = true; 135 | 136 | return ( 137 | 138 | {getFormContext => { 139 | const shouldInitial = !this.$formContext; 140 | 141 | this.$formContext = getFormContext(); 142 | 143 | if (!this.$fieldHandler) { 144 | this.$fieldHandler = createHandler(this, this); 145 | } 146 | 147 | this.$registered = 148 | (this.$formContext.$$registers || {})[this.$fieldHandler.$name] || this.$fieldHandler; 149 | 150 | if (shouldInitial) { 151 | this.$fieldHandler.$$reset(); 152 | this.$fieldHandler.$validate(); 153 | } 154 | 155 | return this._render(); 156 | }} 157 | 158 | ); 159 | } 160 | } 161 | 162 | export default Field; 163 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import hoistStatics from 'hoist-non-react-statics'; 3 | import FormContext from './context'; 4 | 5 | function connect(WrappedComponent) { 6 | const Connect = forwardRef((props, ref) => { 7 | return ( 8 | 9 | {getFormContext => } 10 | 11 | ); 12 | }); 13 | 14 | Connect.displayName = 15 | 'React.Formutil.connect.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous'); 16 | 17 | return hoistStatics(Connect, WrappedComponent); 18 | } 19 | 20 | export default connect; 21 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(() => ({})); 4 | -------------------------------------------------------------------------------- /src/fieldHelper.js: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, createElement } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import warning from 'warning'; 4 | import * as utils from './utils'; 5 | import { FORM_VALIDATE_RESULT } from './Form'; 6 | 7 | let FIELD_UUID = 0; 8 | const $baseState = { 9 | $valid: true, 10 | $invalid: false, 11 | 12 | $dirty: false, 13 | $pristine: true, 14 | 15 | $touched: false, 16 | $untouched: true, 17 | 18 | $focused: false, 19 | 20 | $pending: false, 21 | 22 | $error: {} 23 | }; 24 | 25 | function isError(result) { 26 | return /*!utils.isUndefined(result) && */ result !== true; 27 | } 28 | 29 | function warningValidatorReturn(result, key, name) { 30 | warning( 31 | !utils.isUndefined(result), 32 | `You should return a string or Error when the validation('${ 33 | name && name + ': ' 34 | }${key}') failed, otherwise return true.` 35 | ); 36 | } 37 | 38 | export const propTypes = 39 | process.env.NODE_ENV !== 'production' 40 | ? { 41 | name: PropTypes.string, 42 | 43 | $defaultValue: PropTypes.any, 44 | $defaultState: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), 45 | $onFieldChange: PropTypes.func, 46 | $validators: PropTypes.object, 47 | $asyncValidators: PropTypes.object, 48 | $validateLazy: PropTypes.bool, 49 | $memo: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), 50 | $reserveOnUnmount: PropTypes.bool, 51 | $ref: PropTypes.oneOfType([ 52 | PropTypes.func, 53 | PropTypes.shape({ 54 | current: PropTypes.any 55 | }) 56 | ]), 57 | $parser: PropTypes.func, 58 | $formatter: PropTypes.func, 59 | 60 | render: PropTypes.func, 61 | component: utils.checkComponentPropType, 62 | children(props, ...args) { 63 | let pt = PropTypes.oneOfType([PropTypes.func, PropTypes.node]); 64 | 65 | if (!props.render && !props.component && props.children !== null) { 66 | pt = pt.isRequired; 67 | } 68 | 69 | return pt(props, ...args); 70 | } 71 | } 72 | : undefined; 73 | 74 | export const displayName = 'React.Formutil.Field'; 75 | 76 | export function GET_FIELD_UUID() { 77 | return FIELD_UUID++; 78 | } 79 | 80 | export function renderField($fieldutil, props) { 81 | let { children, render, component } = props; 82 | 83 | if (component) { 84 | return createElement(component, { $fieldutil }); 85 | } 86 | 87 | if (utils.isFunction(render)) { 88 | return render($fieldutil); 89 | } 90 | 91 | if (utils.isFunction(children)) { 92 | return children($fieldutil); 93 | } 94 | 95 | return Children.map(children, child => 96 | child && utils.isComponent(child.type) 97 | ? cloneElement(child, { 98 | $fieldutil 99 | }) 100 | : child 101 | ); 102 | } 103 | 104 | export function createHandler($this, owner) { 105 | const $fieldHandler = { 106 | $$FIELD_UUID: $this.$$FIELD_UUID, 107 | 108 | $$reset, 109 | $$merge, 110 | $$detectChange, 111 | $$triggerChange, 112 | $onValidate, 113 | 114 | $new() { 115 | return $this.$fieldutil; 116 | }, 117 | $picker: $getState, 118 | $getState, 119 | // not support in Hooks 120 | $getComponent() { 121 | return owner; 122 | }, 123 | 124 | $reset($state, callback) { 125 | return $this.$setState($$reset($state), callback); 126 | }, 127 | $getFirstError, 128 | $validate, 129 | $setState: $this.$setState, 130 | $render, 131 | $setValue, 132 | $setTouched, 133 | $setDirty, 134 | $setFocused, 135 | $setValidity, 136 | $setError, 137 | $setPending 138 | }; 139 | 140 | let $$validatePromise; 141 | 142 | function $$detectChange($newState) { 143 | if ('$value' in $newState || '$viewValue' in $newState) { 144 | $validate(); 145 | } 146 | } 147 | 148 | function $$triggerChange({ $newValue, $prevValue }) { 149 | const { $onFieldChange } = $this.props; 150 | 151 | if (utils.isFunction($onFieldChange)) { 152 | $onFieldChange($newValue, $prevValue, $this.$formContext.$formutil); 153 | } 154 | } 155 | 156 | function $onValidate(callback) { 157 | $$validatePromise.then(callback); 158 | 159 | return $$validatePromise; 160 | } 161 | 162 | function $$reset($newState) { 163 | let $initialState; 164 | 165 | const { props, $formContext } = $this; 166 | 167 | if ($formContext.$$getDefault) { 168 | const $name = props.name; 169 | const { $$defaultStates, $$defaultValues } = $formContext.$$getDefault(); 170 | 171 | if ($name && $$defaultValues) { 172 | const $initialValue = utils.parsePath($$defaultValues, $name); 173 | 174 | $initialState = utils.parsePath($$defaultStates, $name) || {}; 175 | 176 | if (!utils.isUndefined($initialValue)) { 177 | $initialState.$value = $initialValue; 178 | } 179 | } 180 | } 181 | 182 | const { $defaultValue, $defaultState } = props; 183 | 184 | return $$merge({ 185 | ...$baseState, // the base state 186 | ...(utils.isFunction($defaultState) ? $defaultState(props) : $defaultState), // self default state 187 | $value: utils.isFunction($defaultValue) 188 | ? $defaultValue(props) 189 | : '$defaultValue' in props 190 | ? $defaultValue 191 | : '', 192 | ...$initialState, // the default state from Form 193 | ...$newState 194 | }); 195 | } 196 | 197 | function $getState() { 198 | return { ...$this.$state }; 199 | } 200 | 201 | function $validate(callback) { 202 | return ($$validatePromise = new Promise(resolve => { 203 | const { props, $formContext } = $this; 204 | const $validators = { ...props.$validators, ...props.$asyncValidators }; 205 | const { 206 | $value, 207 | $pending, 208 | $error: { ...$newError } 209 | } = $this.$state; 210 | const { $formutil } = $formContext; 211 | const $validError = {}; 212 | let $skipRestValidate = false; 213 | let $breakAsyncHandler; 214 | let $shouldCancelPrevAsyncValidate; 215 | let prevCallback; 216 | let validation; 217 | 218 | delete $newError[FORM_VALIDATE_RESULT]; 219 | 220 | const $validatePromises = Object.keys($validators).reduce((promises, key) => { 221 | delete $newError[key]; 222 | 223 | if (!$skipRestValidate && props[key] != null) { 224 | const result = $validators[key]($value, props[key], { 225 | ...props, 226 | $formutil, 227 | $fieldutil: $this.$fieldutil, 228 | $validError 229 | }); 230 | 231 | if (utils.isPromise(result)) { 232 | promises.push( 233 | // @ts-ignore 234 | result.catch(reason => { 235 | if (!$breakAsyncHandler) { 236 | $setValidity(key, reason || key); 237 | } 238 | }) 239 | ); 240 | } else if (isError(result)) { 241 | $validError[key] = result || key; 242 | 243 | warningValidatorReturn(result, key, props.name); 244 | 245 | if (props.$validateLazy) { 246 | $skipRestValidate = true; 247 | } 248 | } 249 | } 250 | 251 | return promises; 252 | }, []); 253 | const execCallback = $fieldutil => 254 | resolve(utils.runCallback(callback, utils.runCallback(prevCallback, $fieldutil))); 255 | 256 | if ($validatePromises.length) { 257 | if (!$pending) { 258 | $setPending(true); 259 | } 260 | 261 | $shouldCancelPrevAsyncValidate = setCallback => ($breakAsyncHandler = setCallback(execCallback)); 262 | 263 | $validatePromises.push( 264 | $setError({ 265 | ...$newError, 266 | ...$validError 267 | }) 268 | ); 269 | 270 | validation = Promise.all($validatePromises).then(() => { 271 | if ($breakAsyncHandler) { 272 | return $breakAsyncHandler; 273 | } 274 | 275 | $this.$shouldCancelPrevAsyncValidate = null; 276 | 277 | return $setPending(false, execCallback); 278 | }); 279 | } else { 280 | if ($pending) { 281 | $setPending(false); 282 | } 283 | 284 | validation = $setError( 285 | { 286 | ...$newError, 287 | ...$validError 288 | }, 289 | execCallback 290 | ); 291 | } 292 | 293 | if ($this.$shouldCancelPrevAsyncValidate) { 294 | $this.$shouldCancelPrevAsyncValidate(callback => { 295 | prevCallback = callback; 296 | 297 | return validation; 298 | }); 299 | } 300 | 301 | $this.$shouldCancelPrevAsyncValidate = $shouldCancelPrevAsyncValidate; 302 | })); 303 | } 304 | 305 | function $render($viewValue, callback) { 306 | return $this.$setState( 307 | { 308 | $viewValue, 309 | $dirty: true 310 | }, 311 | callback 312 | ); 313 | } 314 | 315 | function $setValue($value, callback) { 316 | return $this.$setState( 317 | { 318 | $value 319 | }, 320 | callback 321 | ); 322 | } 323 | 324 | function $setTouched($touched, callback) { 325 | return $this.$setState( 326 | { 327 | $touched 328 | }, 329 | callback 330 | ); 331 | } 332 | 333 | function $setDirty($dirty, callback) { 334 | return $this.$setState( 335 | { 336 | $dirty 337 | }, 338 | callback 339 | ); 340 | } 341 | 342 | function $setFocused($focused, callback) { 343 | return $this.$setState( 344 | { 345 | $focused 346 | }, 347 | callback 348 | ); 349 | } 350 | 351 | function $setError($error, callback) { 352 | return $this.$setState( 353 | { 354 | $error 355 | }, 356 | callback 357 | ); 358 | } 359 | 360 | function $setValidity(key, result = true, callback) { 361 | const { 362 | $error: { ...$newError } 363 | } = $this.$state; 364 | 365 | if (isError(result)) { 366 | $newError[key] = result || key; 367 | 368 | warningValidatorReturn(result, key, $this.props.name); 369 | } else { 370 | delete $newError[key]; 371 | } 372 | 373 | return $setError($newError, callback); 374 | } 375 | 376 | function $setPending($pending, callback) { 377 | return $this.$setState( 378 | { 379 | $pending 380 | }, 381 | callback 382 | ); 383 | } 384 | 385 | function $getFirstError() { 386 | const { $error = {} } = $this.$state; 387 | 388 | for (let name in $error) { 389 | if ($error.hasOwnProperty(name)) { 390 | return $error[name] instanceof Error ? $error[name].message : $error[name]; 391 | } 392 | } 393 | } 394 | 395 | function $$merge({ ...$newState }) { 396 | if ('$error' in $newState) { 397 | if (!$newState.$error) { 398 | $newState.$error = {}; 399 | } 400 | 401 | $newState.$valid = Object.keys($newState.$error).length === 0; 402 | } 403 | 404 | // process $value 405 | const { $parser, $formatter } = $this.props; 406 | 407 | if ('$viewValue' in $newState && !('$value' in $newState)) { 408 | const $setViewValue = $value => ($newState.$viewValue = $value); 409 | 410 | $newState.$value = $parser ? $parser($newState.$viewValue, $setViewValue) : $newState.$viewValue; 411 | } else if ('$value' in $newState && !('$viewValue' in $newState)) { 412 | const $setModelValue = $value => ($newState.$value = $value); 413 | 414 | $newState.$viewValue = $formatter ? $formatter($newState.$value, $setModelValue) : $newState.$value; 415 | } 416 | 417 | // process $valid/$invalid 418 | if ('$valid' in $newState) { 419 | $newState.$invalid = !$newState.$valid; 420 | } else if ('$invalid' in $newState) { 421 | $newState.$valid = !$newState.$invalid; 422 | } 423 | 424 | // process $dirty/$pristine 425 | if ('$dirty' in $newState) { 426 | $newState.$pristine = !$newState.$dirty; 427 | } else if ('$pristine' in $newState) { 428 | $newState.$dirty = !$newState.$pristine; 429 | } 430 | 431 | // process $touched/$untouched 432 | if ('$touched' in $newState) { 433 | $newState.$untouched = !$newState.$touched; 434 | } else if ('$untouched' in $newState) { 435 | $newState.$touched = !$newState.$untouched; 436 | } 437 | 438 | $this.$state = { ...$this.$state, ...$newState }; 439 | 440 | return $getState(); 441 | } 442 | 443 | return $fieldHandler; 444 | } 445 | -------------------------------------------------------------------------------- /src/hooks/Field.js: -------------------------------------------------------------------------------- 1 | import useField from './useField'; 2 | import { propTypes, displayName, renderField } from '../fieldHelper'; 3 | 4 | function Field(props) { 5 | const $fieldutil = useField(props); 6 | 7 | return renderField($fieldutil, props); 8 | } 9 | 10 | Field.FieldDisplayName = displayName; 11 | Field.propTypes = propTypes; 12 | 13 | export default Field; 14 | -------------------------------------------------------------------------------- /src/hooks/useField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import warning from 'warning'; 3 | import useFormContext from './useFormContext'; 4 | import { runCallback, createRef } from '../utils'; 5 | import { createHandler, GET_FIELD_UUID } from '../fieldHelper'; 6 | 7 | /** 8 | * @description 9 | * The custom hook for Field 10 | * 11 | * @param {string | object} [name] 12 | * @param {object} [props] 13 | * 14 | * @return {object} $Fieldutil 15 | */ 16 | function useField(name, props = {}) { 17 | if (!React.useState) { 18 | throw new Error(`Hooks api need react@>=16.8, Please upgrade your reactjs.`); 19 | } 20 | 21 | const { useState, useLayoutEffect, useEffect, useRef } = React; 22 | const _useEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; 23 | 24 | let $name; 25 | 26 | if (name) { 27 | if (typeof name === 'string') { 28 | $name = name; 29 | 30 | props.name = $name; 31 | } else { 32 | props = name; 33 | 34 | $name = props.name; 35 | } 36 | } 37 | 38 | const $formContext = useFormContext(); 39 | /** @type {any} */ 40 | const $this = useRef({}).current; 41 | /** @type {React.MutableRefObject} */ 42 | const callbackRef = useRef([]); 43 | 44 | let $registered; 45 | 46 | $this.$formContext = $formContext; 47 | $this.props = props; 48 | $this.$setState = $setState; 49 | $this.shouldRendered = true; 50 | 51 | // we not directly use this $state, just from $this.$state 52 | const [, setState] = useState(() => { 53 | $this.$$FIELD_UUID = GET_FIELD_UUID(); 54 | $this.$fieldHandler = $registered = createHandler($this); 55 | 56 | $this.$fieldHandler.$$reset(); 57 | $this.$fieldHandler.$validate(); 58 | }); 59 | 60 | if (!$registered) { 61 | $registered = ($formContext.$$registers || {})[$this.$fieldHandler.$name] || $this.$fieldHandler; 62 | } 63 | 64 | _useEffect(() => { 65 | const { $state } = $this; 66 | 67 | if ($this.isMounting) { 68 | if (!($name in ($formContext.$$registers || {}))) { 69 | $registered.$$triggerChange({ 70 | $newValue: $state.$value, 71 | $prevValue: $this.$prevState.$value 72 | }); 73 | } 74 | } 75 | 76 | $this.$prevState = $state; 77 | // eslint-disable-next-line 78 | }, [$this.$state.$value]); 79 | 80 | _useEffect(() => { 81 | $this.isMounting = true; 82 | 83 | warning( 84 | !$name || $formContext.$formutil, 85 | `You should enusre that the useField() with the name '${$name}' must be used underneath a component or withForm() HOC, otherwise it's isolated.` 86 | ); 87 | 88 | warning($name, `You should pass a name argument to useField(), otherwise it will be isolated!`); 89 | 90 | return () => { 91 | $this.isMounting = false; 92 | 93 | createRef(props.$ref, null); 94 | }; 95 | // eslint-disable-next-line 96 | }, []); 97 | 98 | _useEffect(() => { 99 | if ($formContext.$$register) { 100 | $formContext.$$register($name, $this.$fieldHandler); 101 | } 102 | 103 | return () => { 104 | if ($formContext.$$unregister) { 105 | $formContext.$$unregister($name, $this.$fieldHandler, !$this.isMounting && props.$reserveOnUnmount); 106 | } 107 | }; 108 | // eslint-disable-next-line 109 | }, [$name]); 110 | 111 | // trigger ref callback 112 | _useEffect(() => { 113 | createRef(props.$ref, $this.$fieldutil); 114 | }); 115 | 116 | _useEffect(() => { 117 | if (callbackRef.current.length > 0) { 118 | const callbackQueue = [...callbackRef.current]; 119 | 120 | callbackRef.current.length = 0; 121 | 122 | while (callbackQueue.length) { 123 | callbackQueue.pop()($this.$fieldutil); 124 | } 125 | } 126 | }); 127 | 128 | function $setState($newState, callback) { 129 | return new Promise(resolve => { 130 | const execute = () => resolve(runCallback(callback, $this.$fieldutil)); 131 | 132 | if ($this.isMounting) { 133 | if ($name in ($formContext.$$registers || {})) { 134 | $this.shouldRendered = false; 135 | $formContext.$$onChange($name, $newState, execute); 136 | 137 | if (!$this.shouldRendered) { 138 | setState({}); 139 | } 140 | } else { 141 | $registered.$$merge($newState); 142 | $registered.$$detectChange($newState); 143 | 144 | setState({}); 145 | 146 | callbackRef.current.push(execute); 147 | } 148 | } else { 149 | $registered.$$merge($newState); 150 | execute(); 151 | } 152 | }); 153 | } 154 | 155 | return ($this.$fieldutil = { 156 | $name, 157 | ...$registered.$getState(), 158 | ...$registered, 159 | $$formutil: $formContext.$formutil 160 | }); 161 | } 162 | 163 | export default useField; 164 | -------------------------------------------------------------------------------- /src/hooks/useForm.js: -------------------------------------------------------------------------------- 1 | import useFormContext from './useFormContext'; 2 | 3 | function useForm() { 4 | const { $formutil } = useFormContext(); 5 | 6 | return $formutil; 7 | } 8 | 9 | export default useForm; 10 | -------------------------------------------------------------------------------- /src/hooks/useFormContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormContext from '../context'; 3 | 4 | function useFormContext() { 5 | if (!React.useState) { 6 | throw new Error(`Hooks api need react@>=16.8, Please upgrade your reactjs.`); 7 | } 8 | 9 | const { useContext } = React; 10 | const getFormContext = useContext(FormContext); 11 | 12 | return getFormContext(); 13 | } 14 | 15 | export default useFormContext; 16 | -------------------------------------------------------------------------------- /src/hooks/useHandler.js: -------------------------------------------------------------------------------- 1 | import { createHandler, parseProps, defaultProps } from '../EasyField/easyFieldHandler'; 2 | import useField from './useField'; 3 | 4 | function useHandler(props) { 5 | props = { ...defaultProps, ...props, children: null }; 6 | 7 | const { fieldProps, childProps } = parseProps(props); 8 | const $fieldutil = useField(fieldProps); 9 | 10 | return createHandler($fieldutil, fieldProps, childProps); 11 | } 12 | 13 | export default useHandler; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Form } from './Form'; 2 | export { default as withForm } from './withForm'; 3 | export { default as Field } from './Field'; 4 | export { default as withField } from './withField'; 5 | export { default as EasyField } from './EasyField'; 6 | export { default as connect } from './connect'; 7 | export { default as formContext } from './context'; 8 | 9 | // hooks 10 | export { default as useField } from './hooks/useField'; 11 | export { default as useForm } from './hooks/useForm'; 12 | export { default as useHandler } from './hooks/useHandler'; 13 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning'; 2 | import reactIs from 'react-is'; 3 | 4 | const { isValidElementType } = reactIs; 5 | 6 | const OBJECT_PROTO = Object.getPrototypeOf({}); 7 | const PATH_REGEXP = /\s*(?:\]\s*\.|\]\s*\[|\.|\[|\])\s*/g; 8 | const Root = typeof window === 'undefined' ? global : window; 9 | 10 | export function isUndefined(arg) { 11 | return typeof arg === 'undefined'; 12 | } 13 | 14 | export function isFunction(arg) { 15 | return typeof arg === 'function'; 16 | } 17 | 18 | export function isEmpty(arg) { 19 | return isUndefined(arg) || arg === null || arg + '' === ''; 20 | } 21 | 22 | export function isPromise(promise) { 23 | return !!promise && isFunction(promise.then); 24 | } 25 | 26 | export function isObject(obj) { 27 | return Object.prototype.toString.call(obj) === '[object Object]'; 28 | } 29 | 30 | export function isPlainObj(obj) { 31 | if (!isObject(obj)) return false; 32 | if (null === Object.getPrototypeOf(obj)) return true; 33 | if (!isFunction(obj.constructor)) return false; 34 | 35 | return obj.constructor.prototype === OBJECT_PROTO; 36 | } 37 | 38 | export function isComponent(obj) { 39 | return isValidElementType(obj) && typeof obj !== 'string'; 40 | } 41 | 42 | export function checkComponentPropType(props, propName, componentName) { 43 | if (props[propName] && !isComponent(props[propName])) { 44 | return new Error( 45 | `Invalid prop 'component' supplied to '${componentName}': the prop is not a valid React component` 46 | ); 47 | } 48 | } 49 | 50 | // quick clone deeply 51 | export function deepClone(obj) { 52 | if (Array.isArray(obj)) { 53 | const newObj = []; 54 | 55 | for (let i = 0, j = obj.length; i < j; i++) { 56 | newObj[i] = deepClone(obj[i]); 57 | } 58 | 59 | return newObj; 60 | } else if (isPlainObj(obj)) { 61 | const newObj = {}; 62 | 63 | for (let i in obj) { 64 | if (obj.hasOwnProperty(i)) newObj[i] = deepClone(obj[i]); 65 | } 66 | 67 | return newObj; 68 | } 69 | 70 | return obj; 71 | } 72 | 73 | export const runCallback = function(callback, ...args) { 74 | if (isFunction(callback)) { 75 | callback(...args); 76 | } 77 | 78 | return args[0]; 79 | }; 80 | 81 | export function createHOC(withHOC) { 82 | return function(...args) { 83 | if (isComponent(args[0])) { 84 | return withHOC(...args); 85 | } 86 | 87 | return function(WrappedComponent) { 88 | return withHOC(WrappedComponent, args[0]); 89 | }; 90 | }; 91 | } 92 | 93 | const VALID_PROPS = ['minlength', 'maxlength', 'max', 'min', 'required', 'pattern', 'step']; 94 | 95 | export function isValidProp(prop) { 96 | return VALID_PROPS.indexOf(prop.toLowerCase()) > -1; 97 | } 98 | 99 | /* eslint-disable */ 100 | const executeWord = function(word) { 101 | try { 102 | const exec = new Function( 103 | 'origin', 104 | 'global', 105 | `return typeof ${word} === 'number' || (typeof ${word} !== 'undefined' && !(origin in global)) ? ${word} : origin` 106 | ); 107 | return exec(word, Root); 108 | } catch (err) { 109 | return word; 110 | } 111 | }; 112 | 113 | /** 114 | * @desc 解析表达式中赋值深路径对象 115 | * 116 | * @param {object} target 要赋值的对象 117 | * @param {string} path 赋值路径,eg:list[0].title 118 | * @param {any} [value] 要赋过去的值,如过不传,则返回解析路径后的值 119 | * 120 | * 使用示例:parsePath({}, 'list[0].authors[1].name', 'Lucy'); 121 | */ 122 | export function parsePath(...args) { 123 | const [target, path, value] = args; 124 | 125 | warning(typeof path === 'string', `The second parameter(${JSON.stringify(path)}) of parsePath() must be a string.`); 126 | 127 | const pathSymbols = (path.match(PATH_REGEXP) || []).map(s => s.replace(/\s/g, '')); 128 | const pathWords = path 129 | .split(PATH_REGEXP) 130 | .map(s => s.trim()) 131 | .filter(item => item !== ''); 132 | let scope = target; 133 | 134 | try { 135 | if (args.length < 3) { 136 | for (let index = 0, len = pathWords.length; index < len; index++) { 137 | const word = executeWord(pathWords[index]); 138 | 139 | if (index + 1 === len) { 140 | return scope[word]; 141 | } 142 | 143 | if (isUndefined(scope[word])) { 144 | break; 145 | } 146 | 147 | scope = scope[word]; 148 | } 149 | } else { 150 | for (let index = 0, length = pathWords.length; index < length; index++) { 151 | const word = executeWord(pathWords[index]); 152 | const nextWord = pathWords[index + 1]; 153 | const symbol = pathSymbols[index]; 154 | 155 | if (isUndefined(nextWord)) { 156 | scope[word] = value; 157 | break; 158 | } 159 | 160 | switch (symbol) { 161 | case '].': 162 | case '.': 163 | scope = scope[word] = isUndefined(scope[word]) ? {} : { ...scope[word] }; 164 | break; 165 | 166 | case '][': 167 | case '[': 168 | const nextVarWord = executeWord(nextWord); 169 | 170 | scope = scope[word] = isUndefined(scope[word]) 171 | ? typeof nextVarWord === 'number' && nextVarWord >= 0 172 | ? [] 173 | : {} 174 | : Array.isArray(scope[word]) 175 | ? [...scope[word]] 176 | : { ...scope[word] }; 177 | break; 178 | 179 | default: 180 | scope[word] = value; 181 | break; 182 | } 183 | } 184 | } 185 | } catch (error) { 186 | warning(false, `The name '%s' of Field seems is not a legal expression.`, path); 187 | } 188 | 189 | if (args.length > 2) { 190 | return target; 191 | } 192 | } 193 | 194 | export function pathExist(scope, path) { 195 | const pathWords = path 196 | .split(PATH_REGEXP) 197 | .map(s => s.trim()) 198 | .filter(item => item !== ''); 199 | 200 | for (let index = 0, len = pathWords.length; index < len; index++) { 201 | const word = executeWord(pathWords[index]); 202 | 203 | if (!(word in scope)) { 204 | break; 205 | } 206 | 207 | if (index + 1 === len) { 208 | return { 209 | data: scope[word] 210 | }; 211 | } 212 | 213 | scope = scope[word]; 214 | } 215 | } 216 | 217 | export function createRef(ref, value) { 218 | if (ref) { 219 | if (isFunction(ref)) { 220 | ref(value); 221 | } else if ('current' in ref) { 222 | ref.current = value; 223 | } 224 | } 225 | } 226 | 227 | export const arrayFind = (array, process) => { 228 | for (let i = 0, j = array.length; i < j; i++) { 229 | if (process(array[i]) === true) { 230 | return array[i]; 231 | } 232 | } 233 | }; 234 | 235 | export const objectMap = (obj, handler) => 236 | Object.keys(obj).reduce((newObj, key) => { 237 | newObj[key] = handler(obj[key], key, obj); 238 | return newObj; 239 | }, {}); 240 | 241 | export const objectEach = (obj, handler) => Object.keys(obj).forEach(key => handler(obj[key], key, obj)); 242 | 243 | export const toObject = (arr, handler, obj = {}) => 244 | arr.reduce((...args) => { 245 | handler(...args); 246 | 247 | return args[0]; 248 | }, obj); 249 | 250 | const TODO_DELETE = undefined; 251 | export function CLEAR(obj, pkey, pobj) { 252 | objectEach(obj, (value, key) => { 253 | if (value === TODO_DELETE) { 254 | delete obj[key]; 255 | } else if (isPlainObj(value) || Array.isArray(value)) { 256 | CLEAR(value, key, obj); 257 | } 258 | }); 259 | 260 | if (pobj && Object.keys(obj).every(key => obj[key] === TODO_DELETE)) { 261 | pobj[pkey] = TODO_DELETE; 262 | CLEAR(pobj); 263 | } 264 | } 265 | export const objectClear = (obj, name) => { 266 | if (!isUndefined(parsePath(obj, name))) { 267 | parsePath(obj, name, TODO_DELETE); 268 | 269 | CLEAR(obj); 270 | } 271 | }; 272 | 273 | export function isStateEqual(prev, next) { 274 | if (prev === next) { 275 | return true; 276 | } 277 | 278 | const keys = Object.keys(prev); 279 | 280 | if (keys.length !== Object.keys(next).length) { 281 | return false; 282 | } 283 | 284 | for (let i = 0; i < keys.length; i++) { 285 | if (prev[keys[i]] !== next[keys[i]]) { 286 | return false; 287 | } 288 | } 289 | 290 | return true; 291 | } 292 | -------------------------------------------------------------------------------- /src/withField.js: -------------------------------------------------------------------------------- 1 | import React, { Component, forwardRef } from 'react'; 2 | import hoistStatics from 'hoist-non-react-statics'; 3 | import Field from './Field'; 4 | import { createHOC } from './utils'; 5 | 6 | const filterProps = [ 7 | 'name', 8 | 9 | '$defaultValue', 10 | '$defaultState', 11 | '$onFieldChange', 12 | '$validators', 13 | '$asyncValidators', 14 | '$validateLazy', 15 | '$memo', 16 | '$reserveOnUnmount', 17 | '$ref', 18 | '$parser', 19 | '$formatter', 20 | 21 | 'render', 22 | 'component', 23 | 'children' 24 | ]; 25 | 26 | function withField(WrappedComponent, config = {}) { 27 | class WithField extends Component { 28 | static displayName = 29 | 'React.Formutil.withField.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous'); 30 | 31 | renderChildren = $fieldutil => ( 32 | 33 | ); 34 | 35 | render() { 36 | const { __forwardRef__, ...others } = this.props; 37 | // component优先级最高,这里排除掉, 避免和render属性冲突 38 | const { component, ...fieldProps } = this.props; 39 | 40 | filterProps 41 | .concat( 42 | Object.keys({ 43 | ...config.$validators, 44 | ...config.$asyncValidators, 45 | ...others.$validators, 46 | ...others.$asyncValidators 47 | }) 48 | ) 49 | .forEach(prop => { 50 | if (prop in others) { 51 | if (prop === '$validators' || prop === '$asyncValidators' || prop === '$defaultState') { 52 | fieldProps[prop] = { ...config[prop], ...others[prop] }; 53 | } 54 | 55 | delete others[prop]; 56 | } 57 | }); 58 | 59 | this.othersProps = others; 60 | 61 | return ; 62 | } 63 | } 64 | 65 | const ForwardRefField = forwardRef((props, ref) => ); 66 | 67 | ForwardRefField.displayName = 68 | 'React.Formutil.withField.ForwardRef.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous'); 69 | 70 | return hoistStatics(ForwardRefField, WrappedComponent); 71 | } 72 | 73 | export default createHOC(withField); 74 | -------------------------------------------------------------------------------- /src/withForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component, forwardRef } from 'react'; 2 | import hoistStatics from 'hoist-non-react-statics'; 3 | import Form from './Form'; 4 | import { createHOC } from './utils'; 5 | 6 | const filterProps = [ 7 | 'render', 8 | 'component', 9 | 'children', 10 | '$defaultValues', 11 | '$defaultStates', 12 | '$onFormChange', 13 | '$validator', 14 | '$processer', 15 | '$ref' 16 | ]; 17 | 18 | function withForm(WrappedComponent, config = {}) { 19 | class WithForm extends Component { 20 | static displayName = 21 | 'React.Formutil.withForm.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous'); 22 | 23 | renderChildren = $formutil => ( 24 | 25 | ); 26 | 27 | render() { 28 | const { __forwardRef__, ...others } = this.props; 29 | // component优先级最高,这里排除掉, 避免和render属性冲突 30 | const { component, ...formProps } = this.props; 31 | 32 | filterProps.forEach(prop => { 33 | if (prop in others) { 34 | if (prop === '$defaultStates' || prop === '$defaultValues') { 35 | formProps[prop] = { ...config[prop], ...others[prop] }; 36 | } 37 | 38 | delete others[prop]; 39 | } 40 | }); 41 | 42 | this.othersProps = others; 43 | 44 | return ; 45 | } 46 | } 47 | 48 | const ForwardRefForm = forwardRef((props, ref) => ); 49 | 50 | ForwardRefForm.displayName = 51 | 'React.Formutil.withForm.ForwardRef.' + (WrappedComponent.displayName || WrappedComponent.name || 'Anonymous'); 52 | 53 | return hoistStatics(ForwardRefForm, WrappedComponent); 54 | } 55 | 56 | export default createHOC(withForm); 57 | -------------------------------------------------------------------------------- /tests/connect.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, EasyField } from '../src'; 3 | import { renderForm } from './helper'; 4 | 5 | const ConnectForm = connect(props => { 6 | props.formRef?.(props.$formutil); 7 | 8 | return ; 9 | }); 10 | 11 | test('should pass $formutil', async () => { 12 | let innerFormutil; 13 | const { getFormutil } = renderForm( (innerFormutil = f)} />); 14 | 15 | expect(getFormutil()).toBe(innerFormutil.$new()); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/dev.dist.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { $Formutil } from '..'; 4 | import { Form as FormCJS, EasyField as EasyFieldCJS } from '../dist/react-formutil.cjs.development'; 5 | import { Form as FormESM, EasyField as EasyFieldESM } from '../dist/react-formutil.esm.development'; 6 | import { Form as FormUMD, EasyField as EasyFieldUMD } from '../dist/react-formutil.umd.development'; 7 | 8 | test('react-formutil.cjs.production should running', async () => { 9 | let formutilRef = React.createRef<$Formutil>(); 10 | 11 | render( 12 | 13 | {() => ( 14 | <> 15 | 16 | 17 | 18 | )} 19 | 20 | ); 21 | 22 | expect(formutilRef.current!.$params).toEqual({ 23 | a: 1, 24 | b: { 25 | c: 2 26 | } 27 | }); 28 | }); 29 | 30 | test('react-formutil.esm.production should running', async () => { 31 | let formutilRef = React.createRef<$Formutil>(); 32 | 33 | render( 34 | 35 | {() => ( 36 | <> 37 | 38 | 39 | 40 | )} 41 | 42 | ); 43 | 44 | expect(formutilRef.current!.$params).toEqual({ 45 | a: 1, 46 | b: { 47 | c: 2 48 | } 49 | }); 50 | }); 51 | 52 | test('react-formutil.umd.production should running', async () => { 53 | let formutilRef = React.createRef<$Formutil>(); 54 | 55 | render( 56 | 57 | {() => ( 58 | <> 59 | 60 | 61 | 62 | )} 63 | 64 | ); 65 | 66 | expect(formutilRef.current!.$params).toEqual({ 67 | a: 1, 68 | b: { 69 | c: 2 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/helper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { $Formutil, FormProps, $Fieldutil, FieldProps } from '../index.d'; 4 | import { Form, Field } from '../src'; 5 | 6 | export function renderForm( 7 | content: React.ReactNode | (($formutil: $Formutil) => React.ReactNode), 8 | formProps?: FormProps 9 | ) { 10 | let formHandler: $Formutil; 11 | const getForm = content => ( 12 | {...formProps}> 13 | {$formutil => { 14 | formHandler = $formutil; 15 | 16 | return typeof content === 'function' ? content($formutil) : content; 17 | }} 18 | 19 | ); 20 | const { rerender, ...rest } = render(getForm(content)); 21 | 22 | return { 23 | getFormutil() { 24 | return formHandler; 25 | }, 26 | ...rest, 27 | rerender: (newContent?: any) => rerender(getForm(newContent === undefined ? content : newContent)) 28 | }; 29 | } 30 | 31 | export function renderField(fieldProps?: FieldProps) { 32 | let fieldHandler: $Fieldutil; 33 | let formHandler: $Formutil; 34 | let instance; 35 | const getForm = (newProps?: FieldProps) => ( 36 |
{ 38 | formHandler = $formutil; 39 | 40 | return ( 41 | (instance = node)}> 42 | {$fieldutil => { 43 | fieldHandler = $fieldutil; 44 | 45 | return ( 46 | { 50 | $fieldutil.$render(ev.target.value); 51 | 52 | if ($fieldutil.$pristine) { 53 | $fieldutil.$setDirty(true); 54 | } 55 | }} 56 | onFocus={() => { 57 | $fieldutil.$setFocused(true); 58 | }} 59 | onBlur={() => { 60 | $fieldutil.$setFocused(false); 61 | 62 | if ($fieldutil.$untouched) { 63 | $fieldutil.$setTouched(true); 64 | } 65 | }} 66 | /> 67 | ); 68 | }} 69 | 70 | ); 71 | }}>
72 | ); 73 | const { rerender, ...rest } = render(getForm()); 74 | 75 | return { 76 | getFieldutil() { 77 | return fieldHandler; 78 | }, 79 | getFormutil() { 80 | return formHandler; 81 | }, 82 | getInstance() { 83 | return instance; 84 | }, 85 | getElement() { 86 | return rest.getByTestId('input') as HTMLInputElement; 87 | }, 88 | rerender(newProps?: FieldProps) { 89 | return rerender(getForm(newProps)); 90 | }, 91 | ...rest 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /tests/prod.dist.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { $Formutil } from '..'; 4 | import { Form as FormCJS, EasyField as EasyFieldCJS } from '../dist/react-formutil.cjs.production'; 5 | import { Form as FormESM, EasyField as EasyFieldESM } from '../dist/react-formutil.esm.production'; 6 | import { Form as FormUMD, EasyField as EasyFieldUMD } from '../dist/react-formutil.umd.production'; 7 | 8 | test('react-formutil.cjs.production should running', async () => { 9 | let formutilRef = React.createRef<$Formutil>(); 10 | 11 | render( 12 | 13 | {() => ( 14 | <> 15 | 16 | 17 | 18 | )} 19 | 20 | ); 21 | 22 | expect(formutilRef.current!.$params).toEqual({ 23 | a: 1, 24 | b: { 25 | c: 2 26 | } 27 | }); 28 | }); 29 | 30 | test('react-formutil.esm.production should running', async () => { 31 | let formutilRef = React.createRef<$Formutil>(); 32 | 33 | render( 34 | 35 | {() => ( 36 | <> 37 | 38 | 39 | 40 | )} 41 | 42 | ); 43 | 44 | expect(formutilRef.current!.$params).toEqual({ 45 | a: 1, 46 | b: { 47 | c: 2 48 | } 49 | }); 50 | }); 51 | 52 | test('react-formutil.umd.production should running', async () => { 53 | let formutilRef = React.createRef<$Formutil>(); 54 | 55 | render( 56 | 57 | {() => ( 58 | <> 59 | 60 | 61 | 62 | )} 63 | 64 | ); 65 | 66 | expect(formutilRef.current!.$params).toEqual({ 67 | a: 1, 68 | b: { 69 | c: 2 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/useField.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { useField } from '../src'; 4 | import { renderForm } from './helper'; 5 | 6 | function UseFieldCompoennt(props) { 7 | const $fieldutil = useField(props); 8 | 9 | return ( 10 | $fieldutil.$render(ev.target.value)} /> 11 | ); 12 | } 13 | 14 | test('should handle input field', () => { 15 | const { getFormutil, getByTestId } = renderForm(); 16 | 17 | expect(getFormutil().$params).toEqual({ 18 | a: '1' 19 | }); 20 | 21 | userEvent.type(getByTestId('input'), '2'); 22 | 23 | expect(getFormutil().$params).toEqual({ 24 | a: '12' 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../src/utils'; 2 | 3 | test('parsePath() set value', () => { 4 | expect(utils.parsePath({}, 'a.b', true)).toEqual({ 5 | a: { b: true } 6 | }); 7 | 8 | expect(utils.parsePath({}, 'a[b]', true)).toEqual({ 9 | a: { b: true } 10 | }); 11 | 12 | expect(utils.parsePath({}, 'a["b"]', true)).toEqual({ 13 | a: { b: true } 14 | }); 15 | 16 | expect(utils.parsePath({}, 'a[1]', true)).toEqual({ 17 | a: [undefined, true] 18 | }); 19 | 20 | expect(utils.parsePath({}, 'a[1 + 1]', true)).toEqual({ 21 | a: [undefined, undefined, true] 22 | }); 23 | 24 | expect(utils.parsePath({}, 'a[1 + "a"]', true)).toEqual({ 25 | a: { 26 | '1a': true 27 | } 28 | }); 29 | 30 | expect(utils.parsePath({}, 'a.b[c][0][0].d', true)).toEqual({ 31 | a: { 32 | b: { 33 | c: [ 34 | [ 35 | { 36 | d: true 37 | } 38 | ] 39 | ] 40 | } 41 | } 42 | }); 43 | }); 44 | 45 | test('parsePath() get value', () => { 46 | expect(utils.parsePath({ a: { b: true } }, 'a.b')).toEqual(true); 47 | 48 | expect(utils.parsePath({ a: { b: true } }, 'a[b]')).toEqual(true); 49 | 50 | expect(utils.parsePath({ a: { b: true } }, 'a["b"]')).toEqual(true); 51 | 52 | expect( 53 | utils.parsePath( 54 | { 55 | a: [undefined, true] 56 | }, 57 | 'a[1]' 58 | ) 59 | ).toEqual(true); 60 | 61 | expect(utils.parsePath({ a: [undefined, undefined, true] }, 'a[1 + 1]')).toEqual(true); 62 | 63 | expect( 64 | utils.parsePath( 65 | { 66 | a: { 67 | '1a': true 68 | } 69 | }, 70 | 'a[1 + "a"]' 71 | ) 72 | ).toEqual(true); 73 | 74 | expect( 75 | utils.parsePath( 76 | { 77 | a: { 78 | b: { 79 | c: [ 80 | [ 81 | { 82 | d: true 83 | } 84 | ] 85 | ] 86 | } 87 | } 88 | }, 89 | 'a.b[c][0][0].d' 90 | ) 91 | ).toEqual(true); 92 | }); 93 | 94 | test('isStateEqual()', () => { 95 | expect( 96 | utils.isStateEqual( 97 | { 98 | a: true 99 | }, 100 | { 101 | a: true 102 | } 103 | ) 104 | ).toBe(true); 105 | 106 | expect( 107 | utils.isStateEqual( 108 | { 109 | a: true 110 | }, 111 | { 112 | a: true, 113 | b: true 114 | } 115 | ) 116 | ).toBe(false); 117 | 118 | expect( 119 | utils.isStateEqual( 120 | { 121 | a: {} 122 | }, 123 | { 124 | a: {} 125 | } 126 | ) 127 | ).toBe(false); 128 | }); 129 | 130 | test('objectClear()', () => { 131 | const obj = { 132 | a: { 133 | b: { 134 | c: [ 135 | [ 136 | { 137 | d: true 138 | } 139 | ] 140 | ] 141 | } 142 | } 143 | }; 144 | 145 | utils.objectClear(obj, 'a.b[c][0][0].d'); 146 | 147 | expect(obj).toEqual({}); 148 | 149 | const obj1 = { 150 | a: { 151 | b: { 152 | c: [ 153 | [ 154 | { 155 | e: true 156 | } 157 | ] 158 | ], 159 | d: true 160 | } 161 | } 162 | }; 163 | 164 | utils.objectClear(obj1, 'a.b[c][0][0].e'); 165 | 166 | expect(obj1).toEqual({ 167 | a: { 168 | b: { 169 | d: true 170 | } 171 | } 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /tests/withField.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { withField } from '../src'; 4 | import { renderForm } from './helper'; 5 | 6 | const CustomField = withField(({ $fieldutil, ...others }) => { 7 | return $fieldutil.$render(ev.target.value)} />; 8 | }); 9 | 10 | test('should pass $fieldutil', async () => { 11 | expect(CustomField.displayName).toBe('React.Formutil.withField.ForwardRef.Anonymous'); 12 | 13 | const { getFormutil, getByTestId } = renderForm(); 14 | 15 | expect(getFormutil().$params).toEqual({ 16 | a: '1' 17 | }); 18 | 19 | userEvent.type(getByTestId('input'), '2'); 20 | 21 | expect(getFormutil().$params).toEqual({ 22 | a: '12' 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/withForm.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { withForm, EasyField } from '../src'; 4 | import { $Formutil } from '../index.d'; 5 | 6 | function FormPage() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | test('withForm', () => { 16 | const formutilRef = React.createRef<$Formutil>(); 17 | const Form = withForm(FormPage); 18 | 19 | expect(Form.displayName).toBe('React.Formutil.withForm.ForwardRef.FormPage'); 20 | 21 | render(
); 22 | 23 | expect(formutilRef.current!.$params).toEqual({ 24 | a: '', 25 | b: '' 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ESNext", 5 | "lib": ["esnext", "dom"], 6 | "jsx": "react", 7 | "resolveJsonModule": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "allowUnreachableCode": false, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "skipLibCheck": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": false, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "importHelpers": true 20 | }, 21 | "exclude": ["node_modules", "scripts", "dist", "build", "buildDev"] 22 | } 23 | --------------------------------------------------------------------------------