├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── README.zh-CN.md ├── package.json ├── pnpm-lock.yaml ├── public ├── ant_plus_logo.svg └── antx_vs_antd.png ├── rollup.config.ts ├── src ├── Watch.tsx ├── WrapperCol.tsx ├── create.tsx └── index.tsx └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'prettier'], 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | example 5 | index.html 6 | vite.config.mts 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | plugins: ['prettier-plugin-organize-imports'], 4 | }; 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nanxiaobei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Link in bio to **widgets**, 4 | your online **home screen**. ➫ [🔗 kee.so](https://kee.so/) 5 | 6 |
7 | 8 | --- 9 | 10 |
11 | 12 | Ant Plus 13 | 14 | Ant Design Form Simplified, build forms in the simplest way. 15 | 16 | [![npm version](https://img.shields.io/npm/v/antx.svg?style=flat-square)](https://www.npmjs.com/package/antx) 17 | [![npm downloads](https://img.shields.io/npm/dt/antx.svg?style=flat-square)](http://www.npmtrends.com/antx) 18 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/antx?style=flat-square)](https://bundlephobia.com/result?p=antx) 19 | [![GitHub](https://img.shields.io/github/license/nanxiaobei/ant-plus.svg?style=flat-square)](https://github.com/nanxiaobei/ant-plus/blob/main/LICENSE) 20 | ![npm peer dependency version](https://img.shields.io/npm/dependency-version/antx/peer/react?style=flat-square) 21 | ![npm peer dependency version](https://img.shields.io/npm/dependency-version/antx/peer/antd?style=flat-square) 22 | 23 | English · [简体中文](./README.zh-CN.md) 24 | 25 |
26 | 27 | --- 28 | 29 | ## Introduction 30 | 31 | `antx` provides a set of `antd` mixed field components: 32 | 33 | **1. Say goodbye to cumbersome `` and `rules`** 34 | Directly write on field components (e.g. `Input`) with `Form.Item` props and field props (**fully TypeScript support**), which greatly simplifies the code. 35 | 36 | **2. String `rules` (only enhanced, original `rules` are also supported)** 37 | `rules` in string, for example `rules={['required', 'max=10']}` represents for `rules={[{ required: true }, { max: 10 }]}`. 38 | 39 | **3. Not adding any new props** 40 | All props are `antd` original props, without add any other new props, reducing mental burden. 41 | 42 | In the same time, `antx` provides 2 helper components (`WrapperCol`, `Watch`), and a tool function `create()` for easily enhancing existing field components. 43 | 44 | ## Installation 45 | 46 | ```sh 47 | pnpm add antx 48 | # or 49 | yarn add antx 50 | # or 51 | npm i antx 52 | ``` 53 | 54 | ## Usage 55 | 56 | ```tsx 57 | import { Button, Form } from 'antd'; 58 | import { Input, Select, WrapperCol } from 'antx'; 59 | 60 | const App = () => { 61 | return ( 62 |
63 | 64 | 143 | 144 | 145 | 146 | {(song) => { 147 | return
Song: {song}
; 148 | }} 149 |
150 | 151 | 152 | {([song, artist]) => { 153 | return ( 154 |
155 | Song: {song}, Artist: {artist} 156 |
157 | ); 158 | }} 159 |
160 |
; 161 | ``` 162 | 163 | 2. **WrapperCol**: simplify the layout code, the same props as `Form.Item`, used when the UI needs to be aligned with the input box. 164 | 165 | ```tsx 166 | // WrapperCol example 167 | import { WrapperCol } from 'antx'; 168 | 169 |
170 | 171 | This is a hint that aligns with the input box 172 |
; 173 | ``` 174 | 175 | ### 3. `create()` function 176 | 177 | - **create()**: convert existing custom field components into components that support `Form.Item` props mix-in. 178 | 179 | ```tsx 180 | import { create } from 'antx'; 181 | 182 | // Before 183 |
184 | 185 | 186 | 187 |
; 188 | 189 | // enhancing with create() 190 | const MyCustomInputPlus = create(MyCustomInput); 191 | 192 | // After 193 |
194 | 195 | ; 196 | ``` 197 | 198 | ### 4. String `rules` 199 | 200 | | String | Equals to | Description | 201 | | --------------- | -------------------------------------- | ------------ | 202 | | `'required'` | `{ required: true }` | | 203 | | `'required=xx'` | `{ required: true, message: 'xx' }` | | 204 | | `'string'` | `{ type: 'string', whitespace: true }` | | 205 | | `'pureString'` | `{ type: 'string' }` | | 206 | | `'number'` | `{ type: 'number' }` | | 207 | | `'array'` | `{ type: 'array' }` | | 208 | | `'boolean'` | `{ type: 'boolean' }` | | 209 | | `'url'` | `{ type: 'url' }` | | 210 | | `'email'` | `{ type: 'email' }` | | 211 | | `'len=20'` | `{ len: 20 }` | `len === 20` | 212 | | `'max=100'` | `{ max: 100 }` | `max <= 100` | 213 | | `'min=10'` | `{ min: 10 }` | `min >= 10` | 214 | 215 | ```tsx 216 | // String rules example 217 | 218 | ``` 219 | 220 | ## Comparison 221 | 222 | Ant Plus and Ant Design form code comparison: 223 | 224 | ![Comparison](public/antx_vs_antd.png) 225 | 226 | ## License 227 | 228 | [MIT License](https://github.com/nanxiaobei/ant-plus/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/) 229 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Link in bio to **widgets**, 4 | your online **home screen**. ➫ [🔗 kee.so](https://kee.so/) 5 | 6 |
7 | 8 | --- 9 | 10 |
11 | 12 | Ant Plus 13 | 14 | Ant Design Form 简化版,以最简便的方式搭建表单。 15 | 16 | [![npm version](https://img.shields.io/npm/v/antx.svg?style=flat-square)](https://www.npmjs.com/package/antx) 17 | [![npm downloads](https://img.shields.io/npm/dt/antx.svg?style=flat-square)](http://www.npmtrends.com/antx) 18 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/antx?style=flat-square)](https://bundlephobia.com/result?p=antx) 19 | [![GitHub](https://img.shields.io/github/license/nanxiaobei/ant-plus.svg?style=flat-square)](https://github.com/nanxiaobei/ant-plus/blob/main/LICENSE) 20 | ![npm peer dependency version](https://img.shields.io/npm/dependency-version/antx/peer/react?style=flat-square) 21 | ![npm peer dependency version](https://img.shields.io/npm/dependency-version/antx/peer/antd?style=flat-square) 22 | 23 | [English](./README.md) · 简体中文 24 | 25 |
26 | 27 | --- 28 | 29 | ## 介绍 30 | 31 | `antx` 提供一套 `antd` 混合表单组件的集合: 32 | 33 | **1. 告别繁琐的 `` 与 `rules`** 34 | 直接在表单组件 (如 `Input`) 上混写 `Form.Item` props 与组件 props (完整 TypeScript 支持),显著简化代码。 35 | 36 | **2. 字符串 rules (仅增强,原 rules 写法同样支持)** 37 | 提供 string 形式 rules,例如 `rules={['required', 'max=10'']}` 即 `rules={[{ required: true }, { max: 10 }]}`。 38 | 39 | **3. 未新增任何 props** 40 | 所有 props 均为 `antd` 组件原有 props,未新增任何其它 props,减少心智负担。 41 | 42 | 同时 `antx` 还提供了 2 个助手组件 (`WrapperCol`、`Watch`) ,以及一个工具函数 `create()` 用于轻松拓展已有表单组件。 43 | 44 | ## 安装 45 | 46 | ```sh 47 | pnpm add antx 48 | # or 49 | yarn add antx 50 | # or 51 | npm i antx 52 | ``` 53 | 54 | ## 使用 55 | 56 | ```tsx 57 | import { Button, Form } from 'antd'; 58 | import { Input, Select, WrapperCol } from 'antx'; 59 | 60 | const App = () => { 61 | return ( 62 |
63 | 64 | 143 | 144 | 145 | 146 | {(song) => { 147 | return
歌曲:{song}
; 148 | }} 149 |
150 | 151 | 152 | {([song, artist]) => { 153 | return ( 154 |
155 | 歌曲:{song},歌手:{artist} 156 |
157 | ); 158 | }} 159 |
160 |
; 161 | ``` 162 | 163 | 2. **WrapperCol**: 简化布局代码,props 与` Form.Item` 完全一致,用于 UI 需与输入框对齐的情况 164 | 165 | ```tsx 166 | // WrapperCol 示例 167 | import { WrapperCol } from 'antx'; 168 | 169 |
170 | 171 | 这是一条与输入框对齐的提示 172 |
; 173 | ``` 174 | 175 | ### 3. `create()` 工具函数 176 | 177 | - **create()**: 将已有表单组件,包装为支持 `Form.Item` props 混写的组件,轻松拓展现有组件 178 | 179 | ```tsx 180 | import { create } from 'antx'; 181 | 182 | // 拓展前 183 |
184 | 185 | 186 | 187 |
; 188 | 189 | // 使用 create() 拓展 190 | const MyCustomInputPlus = create(MyCustomInput); 191 | 192 | // 拓展后 193 |
194 | 195 | ; 196 | ``` 197 | 198 | ### 4. 字符串 `rules` 199 | 200 | | 字符串 | 对应 | 说明 | 201 | | --------------- | -------------------------------------- | ------------ | 202 | | `'required'` | `{ required: true }` | | 203 | | `'required=xx'` | `{ required: true, message: 'xx' }` | | 204 | | `'string'` | `{ type: 'string', whitespace: true }` | | 205 | | `'pureString'` | `{ type: 'string' }` | | 206 | | `'number'` | `{ type: 'number' }` | | 207 | | `'array'` | `{ type: 'array' }` | | 208 | | `'boolean'` | `{ type: 'boolean' }` | | 209 | | `'url'` | `{ type: 'url' }` | | 210 | | `'email'` | `{ type: 'email' }` | | 211 | | `'len=20'` | `{ len: 20 }` | `len === 20` | 212 | | `'max=100'` | `{ max: 100 }` | `max <= 100` | 213 | | `'min=10'` | `{ min: 10 }` | `min >= 10` | 214 | 215 | ```tsx 216 | // 字符串 rules 示例 217 |
218 | 219 |
220 | ``` 221 | 222 | ## 对比 223 | 224 | Ant Plus 与 Ant Design 表单代码对比: 225 | 226 | ![Comparison](public/antx_vs_antd.png) 227 | 228 | ## 协议 229 | 230 | [MIT License](https://github.com/nanxiaobei/ant-plus/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/) 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antx", 3 | "version": "5.8.0", 4 | "description": "Ant Design Form Simplified", 5 | "keywords": [ 6 | "react", 7 | "form", 8 | "antd", 9 | "ant-design", 10 | "ant-plus", 11 | "input", 12 | "select" 13 | ], 14 | "license": "MIT", 15 | "author": "nanxiaobei (https://github.com/nanxiaobei)", 16 | "homepage": "https://github.com/nanxiaobei/ant-plus", 17 | "repository": "github:nanxiaobei/ant-plus", 18 | "bugs": "https://github.com/nanxiaobei/ant-plus/issues", 19 | "main": "dist/ant-plus.cjs.js", 20 | "module": "dist/ant-plus.esm.js", 21 | "types": "dist/ant-plus.d.ts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "rm -rf dist && rollup -c --configPlugin @rollup/plugin-typescript", 27 | "u": "pnpm update --latest && pnpm add eslint@8.57.1" 28 | }, 29 | "peerDependencies": { 30 | "antd": ">=5.0.0", 31 | "react": ">=16.8.0", 32 | "react-dom": ">=16.8.0" 33 | }, 34 | "devDependencies": { 35 | "@rollup/plugin-typescript": "^12.1.1", 36 | "@types/node": "^22.9.1", 37 | "@types/react": "^18.3.12", 38 | "@types/react-dom": "^18.3.1", 39 | "@vitejs/plugin-react": "^4.3.3", 40 | "antd": "^5.22.2", 41 | "eslint": "^8.57.1", 42 | "eslint-config-prettier": "^9.1.0", 43 | "eslint-config-react-app": "^7.0.1", 44 | "prettier": "^3.3.3", 45 | "prettier-plugin-organize-imports": "^4.1.0", 46 | "react": "^18.3.1", 47 | "react-dom": "^18.3.1", 48 | "rollup": "^4.27.3", 49 | "rollup-plugin-dts": "^6.1.1", 50 | "typescript": "^5.6.3", 51 | "vite": "^5.4.11" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/ant_plus_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/antx_vs_antd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanxiaobei/ant-plus/ae945c7a4f433969c54d2d409fd35e610e372376/public/antx_vs_antd.png -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import type { RollupOptions } from 'rollup'; 3 | import dts from 'rollup-plugin-dts'; 4 | 5 | import { readFileSync } from 'fs'; 6 | 7 | const pkg = JSON.parse(readFileSync('./package.json') as unknown as string); 8 | 9 | const input = 'src/index.tsx'; 10 | const cjsOutput = { file: pkg.main, format: 'cjs', exports: 'auto' } as const; 11 | const esmOutput = { file: pkg.module, format: 'es' } as const; 12 | const dtsOutput = { file: pkg.types, format: 'es' } as const; 13 | 14 | const deps = Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }); 15 | const keys = new RegExp(`^(${deps.join('|')})(/|$)`); 16 | const external = (id: string) => keys.test(id); 17 | 18 | const config: RollupOptions[] = [ 19 | { input, output: cjsOutput, plugins: [typescript()], external }, 20 | { input, output: esmOutput, plugins: [typescript()], external }, 21 | { input, output: dtsOutput, plugins: [dts()] }, 22 | ]; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /src/Watch.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from 'antd'; 2 | import type { FormInstance } from 'antd/es/form'; 3 | import type { NamePath, Store } from 'antd/es/form/interface'; 4 | import type { ReactNode } from 'react'; 5 | import { useEffect, useRef } from 'react'; 6 | 7 | const { Item } = Form; 8 | 9 | // https://github.com/react-component/field-form/blob/master/src/utils/typeUtil.ts 10 | function toArray(value?: T | T[] | null): T[] { 11 | if (value === undefined || value === null) { 12 | return []; 13 | } 14 | 15 | return Array.isArray(value) ? value : [value]; 16 | } 17 | 18 | // https://github.com/react-component/util/blob/master/src/utils/get.ts 19 | function get( 20 | entity: any, 21 | path: (string | number)[] | readonly (string | number)[], 22 | ) { 23 | let current = entity; 24 | 25 | for (let i = 0; i < path.length; i += 1) { 26 | if (current === null || current === undefined) { 27 | return undefined; 28 | } 29 | 30 | current = current[path[i]]; 31 | } 32 | 33 | return current; 34 | } 35 | 36 | const valOf = (values: Store, name: NamePath) => { 37 | const namePath = toArray(name); 38 | return get(values, namePath); 39 | }; 40 | 41 | export type WatchProps = { 42 | name?: NamePath; 43 | list?: NamePath[]; 44 | children?: (next: any, prev: any, form: FormInstance) => ReactNode; 45 | onlyValid?: boolean; 46 | onChange?: (next: any, prev: any, form: FormInstance) => void; 47 | }; 48 | 49 | /** 50 | * Watch - 用于监听其它字段的值 51 | */ 52 | const Watch = (props: WatchProps) => { 53 | const { name, list, children, onlyValid = false, onChange } = props; 54 | 55 | const hasName = !Array.isArray(list); 56 | const prev = useRef(hasName ? undefined : list.map(() => undefined)); 57 | 58 | return ( 59 | , next: Record) => { 62 | if (hasName) { 63 | return name ? valOf(prev, name) !== valOf(next, name) : true; 64 | } 65 | return list.some((key) => key && valOf(prev, key) !== valOf(next, key)); 66 | }} 67 | > 68 | {(rawForm) => { 69 | const form = rawForm as FormInstance & Record; 70 | 71 | const next = hasName 72 | ? name && form.getFieldValue(name) 73 | : list.map((key) => key && form.getFieldValue(key)); 74 | 75 | let hasChange; 76 | let hasValidValue = false; 77 | let changeEffect = null; 78 | 79 | if (hasName) { 80 | hasChange = next !== prev.current; 81 | hasValidValue = next !== undefined; 82 | } else { 83 | hasChange = next.some((val: any, index: number) => { 84 | hasValidValue = val !== undefined; 85 | return val !== prev.current?.[index]; 86 | }); 87 | } 88 | 89 | const cachedPrev = prev.current; 90 | 91 | if (hasChange) { 92 | prev.current = next; 93 | 94 | if (onChange) { 95 | const ChangeEffect = () => { 96 | useEffect(() => onChange(next, cachedPrev, form), []); 97 | return null; 98 | }; 99 | changeEffect = ; 100 | } 101 | } 102 | 103 | if (!onlyValid || (onlyValid && hasValidValue)) { 104 | return ( 105 | <> 106 | {children?.(next, cachedPrev, form)} 107 | {changeEffect} 108 | 109 | ); 110 | } 111 | 112 | return changeEffect; 113 | }} 114 | 115 | ); 116 | }; 117 | 118 | export default Watch; 119 | -------------------------------------------------------------------------------- /src/WrapperCol.tsx: -------------------------------------------------------------------------------- 1 | import type { FormItemProps } from 'antd'; 2 | import { Form } from 'antd'; 3 | 4 | const { Item } = Form; 5 | 6 | export type WrapperColProps = Omit; 7 | 8 | const WrapperCol = (props: WrapperColProps) => { 9 | return } colon={false} {...props} />; 10 | }; 11 | 12 | export default WrapperCol; 13 | -------------------------------------------------------------------------------- /src/create.tsx: -------------------------------------------------------------------------------- 1 | import type { FormItemProps, FormRule, SliderSingleProps } from 'antd'; 2 | import { Form } from 'antd'; 3 | import type { 4 | ComponentProps, 5 | ComponentType, 6 | ForwardedRef, 7 | JSXElementConstructor, 8 | PropsWithoutRef, 9 | } from 'react'; 10 | import { forwardRef } from 'react'; 11 | 12 | const { Item } = Form; 13 | 14 | // ─── rules ↓↓↓ ─────────────────────────────────────────────────────────────── 15 | export type PlusShortRule = 16 | | 'required' 17 | | `required=${string}` 18 | | 'string' 19 | | 'pureString' 20 | | 'number' 21 | | 'array' 22 | | 'boolean' 23 | | 'url' 24 | | 'email' 25 | | `len=${number}` // len === val 26 | | `max=${number}` // max <= val 27 | | `min=${number}` // min >= val 28 | | FormRule; 29 | 30 | const miscTypeMap: Record = { 31 | boolean: { type: 'boolean' }, 32 | url: { type: 'url' }, 33 | email: { type: 'email' }, 34 | }; 35 | 36 | const numTypeMap: Record = { 37 | string: { type: 'string', whitespace: true }, 38 | pureString: { type: 'string' }, 39 | number: { type: 'number' }, 40 | array: { type: 'array' }, 41 | }; 42 | 43 | const getRules = (rules: PlusShortRule[]): FormRule[] => { 44 | const ruleList: FormRule[] = []; 45 | let numTypeRule: FormRule | null = null; 46 | let numValRules: FormRule[] = []; 47 | 48 | rules.forEach((rule) => { 49 | if (typeof rule !== 'string') { 50 | ruleList.push(rule); 51 | return; 52 | } 53 | 54 | if (rule === 'required') { 55 | ruleList.push({ required: true }); 56 | return; 57 | } 58 | 59 | if (rule in miscTypeMap) { 60 | ruleList.push(miscTypeMap[rule]); 61 | return; 62 | } 63 | 64 | if (rule in numTypeMap) { 65 | numTypeRule = numTypeMap[rule]; 66 | return; 67 | } 68 | 69 | const [key, val] = rule.split('='); 70 | 71 | if (val === undefined) { 72 | return; 73 | } 74 | 75 | if (key === 'required') { 76 | ruleList.push({ required: true, message: val }); 77 | return; 78 | } 79 | 80 | if (key === 'len' || key === 'min' || key === 'max') { 81 | numValRules.push({ [key]: +val }); 82 | return; 83 | } 84 | }); 85 | 86 | if (numTypeRule && numValRules.length > 0) { 87 | ruleList.push(...numValRules.map((obj) => ({ ...numTypeRule, ...obj }))); 88 | } else if (numTypeRule) { 89 | ruleList.push(numTypeRule); 90 | } else if (numValRules.length > 0) { 91 | ruleList.push(...numValRules); 92 | } 93 | 94 | return ruleList; 95 | }; 96 | 97 | // ─── Form.Item & Field ↓↓↓ ─────────────────────────────────────────────────── 98 | type ConflictProps = 'className' | 'style' | 'name' | 'tooltip'; 99 | 100 | type ReplaceProps = { 101 | selfClass?: SliderSingleProps['className']; 102 | selfStyle?: SliderSingleProps['style']; 103 | selfName?: string; 104 | selfTooltip?: SliderSingleProps['tooltip']; 105 | }; 106 | 107 | export type PlusProps

= Omit & 108 | ReplaceProps & 109 | Omit & { rules?: PlusShortRule[] }; 110 | 111 | const replaceMap: Record = { 112 | selfClass: 'className', 113 | selfStyle: 'style', 114 | selfName: 'name', 115 | selfTooltip: 'tooltip', 116 | }; 117 | 118 | const formItemKeys = [ 119 | 'colon', 120 | 'dependencies', 121 | 'extra', 122 | 'getValueFromEvent', 123 | 'getValueProps', 124 | 'hasFeedback', 125 | 'help', 126 | 'hidden', 127 | 'htmlFor', 128 | 'initialValue', 129 | 'label', 130 | 'labelAlign', 131 | 'labelCol', 132 | 'messageVariables', 133 | 'name', 134 | 'normalize', 135 | 'noStyle', 136 | 'preserve', 137 | 'required', 138 | 'rules', 139 | 'shouldUpdate', 140 | 'tooltip', 141 | 'trigger', 142 | 'validateFirst', 143 | 'validateStatus', 144 | 'validateTrigger', 145 | 'valuePropName', 146 | 'wrapperCol', 147 | 148 | // extra 149 | 'className', 150 | 'style', 151 | ] as (keyof FormItemProps)[]; 152 | 153 | const formItemProps = formItemKeys.reduce( 154 | (obj, key) => { 155 | obj[key] = true; 156 | return obj; 157 | }, 158 | {} as Record, 159 | ); 160 | 161 | const create = >( 162 | Field: T, 163 | getDefaultFieldProps?: (p: ComponentProps) => Partial>, 164 | ) => { 165 | type P = ComponentProps; 166 | type Props = PropsWithoutRef>; 167 | 168 | const ItemField = forwardRef((props: Props, ref: ForwardedRef) => { 169 | const itemProps: FormItemProps = {}; 170 | const fieldProps: P = {} as P; 171 | 172 | // split props 173 | if (props) { 174 | Object.keys(props).forEach((key) => { 175 | const val = props[key as keyof Props]; 176 | 177 | if (key in formItemProps) { 178 | const itemKey = key as keyof FormItemProps; 179 | itemProps[itemKey] = key === 'rules' && val ? getRules(val) : val; 180 | } else { 181 | const fieldKey = replaceMap[key as keyof ReplaceProps] || key; 182 | fieldProps[fieldKey as keyof P] = val; 183 | } 184 | }); 185 | } 186 | 187 | // default field props 188 | if (getDefaultFieldProps) { 189 | const extraFieldProps = getDefaultFieldProps(fieldProps); 190 | 191 | Object.keys(extraFieldProps).forEach((key) => { 192 | if (!(key in fieldProps)) { 193 | fieldProps[key as keyof P] = extraFieldProps[key] as P[keyof P]; 194 | } 195 | }); 196 | } 197 | 198 | const RawField = Field as ComponentType

; 199 | 200 | return ( 201 | 202 | 203 | 204 | ); 205 | }); 206 | 207 | Object.keys(Field).forEach((key) => { 208 | if (!(key in ItemField)) { 209 | (ItemField as any)[key] = (Field as any)[key]; 210 | } 211 | }); 212 | 213 | return ItemField as Omit & typeof ItemField; 214 | }; 215 | 216 | export default create; 217 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | AutoCompleteProps as AntAutoCompleteProps, 3 | CascaderProps as AntCascaderProps, 4 | CheckboxProps as AntCheckboxProps, 5 | DatePickerProps as AntDatePickerProps, 6 | InputNumberProps as AntInputNumberProps, 7 | InputProps as AntInputProps, 8 | MentionProps as AntMentionProps, 9 | RadioGroupProps as AntRadioGroupProps, 10 | RadioProps as AntRadioProps, 11 | RateProps as AntRateProps, 12 | SelectProps as AntSelectProps, 13 | SliderSingleProps as AntSliderSingleProps, 14 | SwitchProps as AntSwitchProps, 15 | TimePickerProps as AntTimePickerProps, 16 | TimeRangePickerProps as AntTimeRangePickerProps, 17 | TransferProps as AntTransferProps, 18 | TreeSelectProps as AntTreeSelectProps, 19 | UploadProps as AntUploadProps, 20 | } from 'antd'; 21 | import { 22 | AutoComplete as AntAutoComplete, 23 | Cascader as AntCascader, 24 | Checkbox as AntCheckbox, 25 | DatePicker as AntDatePicker, 26 | Input as AntInput, 27 | InputNumber as AntInputNumber, 28 | Mentions as AntMentions, 29 | Radio as AntRadio, 30 | Rate as AntRate, 31 | Select as AntSelect, 32 | Slider as AntSlider, 33 | Switch as AntSwitch, 34 | TimePicker as AntTimePicker, 35 | Transfer as AntTransfer, 36 | TreeSelect as AntTreeSelect, 37 | Upload as AntUpload, 38 | } from 'antd'; 39 | import type { CheckboxGroupProps as AntCheckboxGroupProps } from 'antd/es/checkbox'; 40 | import type { RangePickerProps as AntRangePickerProps } from 'antd/es/date-picker'; 41 | import type { 42 | PasswordProps as AntPasswordProps, 43 | SearchProps as AntSearchProps, 44 | TextAreaProps as AntTextAreaProps, 45 | } from 'antd/es/input'; 46 | import type { MentionsRef } from 'antd/es/mentions'; 47 | import type { DraggerProps as AntDraggerProps } from 'antd/es/upload'; 48 | import * as React from 'react'; 49 | import type { PlusProps } from './create'; 50 | import create from './create'; 51 | 52 | // mentions 53 | interface MentionsConfig { 54 | prefix?: string | string[]; 55 | split?: string; 56 | } 57 | interface MentionsEntity { 58 | prefix: string; 59 | value: string; 60 | } 61 | type CompoundedComponent = React.ForwardRefExoticComponent< 62 | AntMentionProps & React.RefAttributes 63 | > & { 64 | Option: typeof Option; 65 | _InternalPanelDoNotUseOrYouWillBeFired: any; 66 | getMentions: (value: string, config?: MentionsConfig) => MentionsEntity[]; 67 | }; 68 | 69 | /* create 70 | ---------------------------------------------------------------------- */ 71 | export { default as create } from './create'; 72 | export type { PlusProps, PlusShortRule } from './create'; 73 | 74 | /* custom 75 | ---------------------------------------------------------------------- */ 76 | export type { WatchProps } from './Watch'; 77 | export type { WrapperColProps } from './WrapperCol'; 78 | 79 | export { default as Watch } from './Watch'; 80 | export { default as WrapperCol } from './WrapperCol'; 81 | 82 | /* 1st 83 | ---------------------------------------------------------------------- */ 84 | export type AutoCompleteProps = PlusProps; 85 | export type CascaderProps = PlusProps; 86 | export type CheckboxProps = PlusProps; 87 | export type DatePickerProps = PlusProps; 88 | export type InputProps = PlusProps; 89 | export type InputNumberProps = PlusProps; 90 | export type MentionProps = PlusProps; 91 | export type RadioProps = PlusProps; 92 | export type RateProps = PlusProps; 93 | export type SelectProps = PlusProps; 94 | export type SliderProps = PlusProps; 95 | export type SwitchProps = PlusProps; 96 | export type TimePickerProps = PlusProps; 97 | export type TransferProps = PlusProps>; 98 | export type TreeSelectProps = PlusProps; 99 | export type UploadProps = PlusProps; 100 | 101 | export const AutoComplete = create(AntAutoComplete); 102 | export const Cascader = create(AntCascader); 103 | export const Checkbox = create(AntCheckbox); 104 | export const DatePicker = create(AntDatePicker); 105 | export const Input = create(AntInput); 106 | export const InputNumber = create(AntInputNumber); 107 | export const Mentions = create(AntMentions as unknown as CompoundedComponent); 108 | export const Radio = create(AntRadio); 109 | export const Rate = create(AntRate); 110 | export const Select = create(AntSelect); 111 | export const Slider = create(AntSlider); 112 | export const Switch = create(AntSwitch); 113 | export const TimePicker = create(AntTimePicker); 114 | export const Transfer = create(AntTransfer); 115 | export const TreeSelect = create(AntTreeSelect); 116 | export const Upload = create(AntUpload); 117 | 118 | /* 2nd 119 | ---------------------------------------------------------------------- */ 120 | export type CheckboxGroupProps = PlusProps; 121 | export type DateRangeProps = PlusProps; 122 | export type TextAreaProps = PlusProps; 123 | export type SearchProps = PlusProps; 124 | export type PasswordProps = PlusProps; 125 | export type RadioGroupProps = PlusProps; 126 | export type TimeRangeProps = PlusProps; 127 | export type DraggerProps = PlusProps; 128 | 129 | export const CheckboxGroup = create(AntCheckbox.Group); 130 | export const DateRange = create(AntDatePicker.RangePicker); 131 | export const TextArea = create(AntInput.TextArea); 132 | export const Search = create(AntInput.Search); 133 | export const Password = create(AntInput.Password); 134 | export const RadioGroup = create(AntRadio.Group); 135 | export const TimeRange = create(AntTimePicker.RangePicker); 136 | export const Dragger = create(AntUpload.Dragger); 137 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noImplicitAny": true, 6 | "noImplicitOverride": true, 7 | "noImplicitReturns": false, 8 | "noImplicitThis": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "strict": true, 12 | "strictBindCallApply": true, 13 | "strictFunctionTypes": true, 14 | "strictNullChecks": true, 15 | "strictPropertyInitialization": true, 16 | 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | 21 | "outDir": "dist", 22 | 23 | "allowSyntheticDefaultImports": true, 24 | "esModuleInterop": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "isolatedModules": true, 27 | 28 | "jsx": "react-jsx", 29 | "target": "es6" 30 | }, 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | --------------------------------------------------------------------------------