├── src ├── baseComponents │ ├── Title │ │ └── index.less │ ├── Hidden │ │ └── index.tsx │ ├── DynamicForm │ │ ├── components │ │ │ └── ExpandView │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── Group.tsx │ │ └── index.less │ ├── TextItem │ │ ├── interface.ts │ │ └── index.less │ ├── Context │ │ └── index.ts │ ├── InputItem │ │ ├── interface.ts │ │ └── index.less │ └── HorizontalTitle │ │ └── index.tsx ├── const │ └── index.ts ├── hooks │ ├── index.ts │ └── useToggle.ts ├── theme │ └── index.less ├── assets │ ├── file.png │ ├── look.png │ ├── QRCode.png │ ├── close.png │ ├── photo.png │ ├── position_ico.png │ └── svg.less ├── components │ ├── NomarImagePicker │ │ ├── index.less │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── single.tsx │ │ │ │ └── basic.tsx │ │ ├── interface.ts │ │ ├── demo │ │ │ ├── single.tsx │ │ │ └── index.tsx │ │ ├── imagePickerGroup.tsx │ │ └── index.tsx │ ├── NomarCustom │ │ ├── index.less │ │ ├── interface.ts │ │ ├── CustomGroup.tsx │ │ ├── tests │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ ├── demo │ │ │ └── index.tsx │ │ └── index.md │ ├── NomarSwitch │ │ ├── index.less │ │ ├── interface.ts │ │ ├── tests │ │ │ ├── demos │ │ │ │ └── basic.tsx │ │ │ └── index.test.tsx │ │ ├── index.md │ │ ├── demo │ │ │ └── index.tsx │ │ └── index.tsx │ ├── NomarSelect │ │ ├── utils.ts │ │ ├── index.less │ │ ├── interface.ts │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── single.tsx │ │ │ │ ├── couplet.tsx │ │ │ │ └── basic.tsx │ │ └── demo │ │ │ └── single.tsx │ ├── NomarDatePicker │ │ ├── index.less │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── single.tsx │ │ │ │ └── basic.tsx │ │ └── interface.ts │ ├── ExtraInput │ │ ├── interface.ts │ │ ├── index.less │ │ ├── index.md │ │ └── tests │ │ │ └── index.test.tsx │ ├── RangeDatePicker │ │ ├── index.less │ │ ├── interface.ts │ │ ├── tests │ │ │ ├── demos │ │ │ │ └── basic.tsx │ │ │ └── index.test.tsx │ │ └── demo │ │ │ └── index.tsx │ ├── NomarTextArea │ │ ├── interface.ts │ │ ├── demo │ │ │ ├── single.tsx │ │ │ └── index.tsx │ │ ├── index.less │ │ └── tests │ │ │ └── demos │ │ │ ├── single.tsx │ │ │ └── basic.tsx │ ├── NomarCheckBox │ │ ├── interface.ts │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── formtest.tsx │ │ │ │ ├── single.tsx │ │ │ │ ├── basic.tsx │ │ │ │ └── couplet.tsx │ │ └── demo │ │ │ ├── single.tsx │ │ │ └── index.tsx │ ├── NomarInput │ │ ├── interface.ts │ │ ├── tests │ │ │ ├── demos │ │ │ │ └── single.tsx │ │ │ └── index.test.tsx │ │ └── demo │ │ │ └── single.tsx │ ├── NomarRadio │ │ ├── interface.ts │ │ ├── demo │ │ │ └── single.tsx │ │ ├── tests │ │ │ ├── demos │ │ │ │ └── single.tsx │ │ │ └── index.test.tsx │ │ └── index.less │ ├── NomarFile │ │ ├── index.less │ │ ├── interface.ts │ │ ├── demo │ │ │ ├── single.tsx │ │ │ └── index.tsx │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── single.tsx │ │ │ │ └── basic.tsx │ │ └── fileGroup.tsx │ ├── NomarPicker │ │ ├── interface.ts │ │ ├── tests │ │ │ ├── demos │ │ │ │ └── basic.tsx │ │ │ └── index.test.tsx │ │ └── demo │ │ │ └── index.tsx │ ├── index.tsx │ ├── MultiplePicker │ │ ├── interface.ts │ │ ├── index.less │ │ ├── demo │ │ │ └── single.tsx │ │ └── tests │ │ │ └── demos │ │ │ ├── single.tsx │ │ │ └── basic.tsx │ ├── AddressPicker │ │ ├── interface.ts │ │ ├── index.less │ │ ├── demo │ │ │ └── single.tsx │ │ └── tests │ │ │ └── demos │ │ │ └── single.tsx │ ├── CoverRadio │ │ ├── index.less │ │ ├── tests │ │ │ └── demos │ │ │ │ ├── couplet.tsx │ │ │ │ └── basic.tsx │ │ └── demo │ │ │ └── index.tsx │ └── Field │ │ └── index.tsx ├── index.ts ├── tests │ ├── deepUsage │ │ └── deepUsageTest.tsx │ ├── relativesUsage │ │ └── index.test.tsx │ └── groupUsage │ │ └── index.test.tsx ├── demo │ ├── jsonGroup.md │ ├── groupUsage.md │ ├── relativesUsage.md │ ├── api.md │ ├── jsonUsage.tsx │ └── api.tsx ├── docs │ └── update.md ├── index.less └── utils │ ├── menu.ts │ └── tool.ts ├── .prettierignore ├── typings.d.ts ├── .prettierrc ├── now.json ├── .editorconfig ├── .gitignore ├── .fatherrc.ts ├── jest.config.ts ├── .github ├── FUNDING.yml ├── workflows │ └── deploy.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── scripts └── verifyCommit.js ├── docs ├── index.md └── setting.md ├── change.md ├── .umirc.ts ├── package.json └── README.md /src/baseComponents/Title/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/const/index.ts: -------------------------------------------------------------------------------- 1 | export const allPrefixCls = 'alitajs-dform'; 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useToggle } from './useToggle'; 2 | -------------------------------------------------------------------------------- /src/theme/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd-mobile-v2/es/style/index.less'; 2 | // 3 | -------------------------------------------------------------------------------- /src/assets/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/file.png -------------------------------------------------------------------------------- /src/assets/look.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/look.png -------------------------------------------------------------------------------- /src/assets/QRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/QRCode.png -------------------------------------------------------------------------------- /src/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/close.png -------------------------------------------------------------------------------- /src/assets/photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/photo.png -------------------------------------------------------------------------------- /src/assets/position_ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alitajs/DynamicForm/HEAD/src/assets/position_ico.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | .umi-test 8 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '@bang88/china-city-data'; 5 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | .@{prefixCls}-image { 3 | .am-image-picker-list { 4 | padding: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/NomarCustom/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-dom { 4 | font-size: 17 * @hd; 5 | display: flex; 6 | & > div:last-child { 7 | flex: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-switch { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/NomarSelect/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否是二维数组,用来兼容原先数据 3 | * @param v 数组值 4 | * @returns boolean 5 | */ 6 | export const is2Dimensionals = (v: Array) => { 7 | if (Array.isArray(v) && v.length > 0 && Array.isArray(v[0])) { 8 | return true; 9 | } 10 | return false; 11 | }; 12 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dform", 3 | "version": 2, 4 | "builds": [ 5 | { 6 | "src": "build.sh", 7 | "use": "@now/static-build", 8 | "config": { 9 | "distDir": "docs-dist" 10 | } 11 | } 12 | ], 13 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/components/NomarSelect/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-select { 4 | .am-list-item { 5 | padding-left: 0; 6 | // min-height: @alita-dform-height; 7 | } 8 | 9 | .am-list-item .am-list-line { 10 | padding-right: 0; 11 | } 12 | } 13 | 14 | .@{prefixCls}-vertical-select { 15 | .alitajs-dform-select(); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/NomarDatePicker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-date-picker { 4 | width: 100%; 5 | } 6 | 7 | .alitajs-dform-vertical-date-picker { 8 | width: 100%; 9 | 10 | .am-list-content { 11 | display: none; 12 | } 13 | 14 | .am-list-extra { 15 | flex-basis: 100% !important; 16 | text-align: left !important; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/baseComponents/Hidden/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | interface HiddenProps { 4 | hidden: boolean; 5 | children: any; 6 | } 7 | 8 | const Hidden: FC = ({ children, hidden = false }) => { 9 | if (hidden) { 10 | return
{children}
; 11 | } 12 | return {children}; 13 | }; 14 | export default Hidden; 15 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/interface.ts: -------------------------------------------------------------------------------- 1 | import { SwitchPropsType } from 'antd-mobile-v2/es/switch/PropsType'; 2 | import { BaseComponentProps } from '../../PropsType'; 3 | 4 | type SwitchType = SwitchPropsType & BaseComponentProps; 5 | export interface INomarSwitchProps extends SwitchType { 6 | coverStyle?: React.CSSProperties; 7 | placeholder?: string; 8 | defaultValue?: boolean; 9 | labelNumber?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DynamicForm, { useForm } from './baseComponents/DynamicForm'; 2 | import './index.less'; 3 | 4 | export * from 'rc-field-form/lib/interface'; 5 | export * from './components'; 6 | export * from './utils'; 7 | export * from './PropsType'; 8 | 9 | export { default as FieldForm } from 'rc-field-form'; 10 | 11 | export { useForm }; 12 | // rename 13 | export { DynamicForm as Form }; 14 | 15 | export default DynamicForm; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /package-lock.json 8 | /coverage 9 | 10 | # production 11 | /dist 12 | /docs-dist 13 | /es 14 | /lib 15 | 16 | # misc 17 | .DS_Store 18 | 19 | # umi 20 | .umi 21 | .umi-production 22 | .umi-test 23 | .env.local 24 | 25 | # ide 26 | /.vscode 27 | /.idea 28 | 29 | umd 30 | -------------------------------------------------------------------------------- /src/components/ExtraInput/interface.ts: -------------------------------------------------------------------------------- 1 | import { InputItemPropsType } from 'antd-mobile-v2/es/input-item/PropsType'; 2 | import { BaseComponentProps } from '../../PropsType'; 3 | 4 | export interface IExtraInputProps extends BaseComponentProps { 5 | fieldProps2: string; 6 | coverStyle?: React.CSSProperties; 7 | extraType?: 'input' | 'select'; 8 | firstProps?: InputItemPropsType; 9 | secondProps?: any; 10 | labelNumber?: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ExtraInput/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-extra-input { 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | justify-content: space-between; 8 | 9 | &-begin-input { 10 | width: 65%; 11 | } 12 | 13 | &-begin-vertical-input { 14 | width: 48%; 15 | } 16 | 17 | &-end-input { 18 | width: 30%; 19 | } 20 | 21 | &-end-vertical-input { 22 | width: 47%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | // true false 切换hook 3 | const useToggle = (initialState: boolean): [boolean, () => void, (val: boolean) => void] => { 4 | const [value, setValue] = useState(initialState); 5 | const toggleValue = useCallback(() => setValue((prev) => !prev), []); 6 | const toggleSetValue = useCallback((val) => setValue(val), []); 7 | 8 | return [value, toggleValue, toggleSetValue]; 9 | }; 10 | 11 | export default useToggle; 12 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls} { 4 | &-range-date-picker { 5 | display: flex; 6 | flex-direction: row; 7 | align-items: center; 8 | justify-content: center; 9 | width: 100%; 10 | } 11 | &-begin-picker { 12 | width: 65%; 13 | } 14 | &-begin-vertical-picker { 15 | width: 48%; 16 | } 17 | &-end-picker { 18 | width: 30%; 19 | } 20 | &-end-vertical-picker { 21 | width: 47%; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/interface.ts: -------------------------------------------------------------------------------- 1 | import { TextAreaItemPropsType } from 'antd-mobile-v2/es/textarea-item/PropsType'; 2 | import { BaseComponentProps } from '../../PropsType'; 3 | 4 | type TextAreaType = TextAreaItemPropsType & BaseComponentProps; 5 | export interface INomarTextAreaProps extends TextAreaType { 6 | coverStyle?: React.CSSProperties; 7 | placeholder?: string; 8 | extra?: React.ReactNode | string; 9 | onClick?: (e: React.MouseEvent) => void; 10 | defaultValue?: string; 11 | errorValue?: any; 12 | } 13 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | esm: { 5 | output: 'es', 6 | }, 7 | cjs: { 8 | output: 'lib', 9 | platform: 'browser', 10 | }, 11 | umd: { 12 | name: 'DynamicForm', 13 | output: 'dist', 14 | }, 15 | // extraBabelPlugins: [ 16 | // [ 17 | // 'babel-plugin-import', 18 | // { 19 | // libraryName: 'antd-mobile-v2', 20 | // libraryDirectory: 'es', 21 | // style: true, 22 | // }, 23 | // ], 24 | // ], 25 | }); 26 | -------------------------------------------------------------------------------- /src/baseComponents/DynamicForm/components/ExpandView/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/index.less'; 2 | 3 | .@{prefixCls}-expand-view { 4 | position: relative; 5 | overflow: hidden; 6 | margin-top: 9 * @hd; 7 | height: 15 * @hd; 8 | &-center { 9 | position: absolute; 10 | top: 0; 11 | left: 50%; 12 | width: 81 * @hd; 13 | height: 25 * @hd; 14 | text-align: center; 15 | background-color: #eee; 16 | transform: translateX(-50%); 17 | border-radius: 17 * @hd; 18 | font-size: 10 * @hd; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/interface.ts: -------------------------------------------------------------------------------- 1 | import { IDataItem } from './checkBoxgroup'; 2 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 3 | 4 | export interface INomarCheckBoxProps extends BaseComponentProps { 5 | data?: Record[]; 6 | coverStyle?: React.CSSProperties; 7 | className?: string; 8 | onChange?: (currentActiveLink: (string | number)[]) => void; 9 | disableItem?: (items: IDataItem) => boolean; 10 | chunk?: number; 11 | alias?: IAliasProps; 12 | defaultValue?: (string | number)[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/baseComponents/DynamicForm/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Group from './Group'; 3 | import DForm, { useForm } from './Dform'; 4 | import { IDynamicFormProps } from '../../PropsType'; 5 | 6 | interface CompoundedComponent 7 | extends React.FC< 8 | IDynamicFormProps & React.RefAttributes 9 | > { 10 | Group: typeof Group; 11 | __DYNAMICFORM: boolean; 12 | } 13 | 14 | const Form = DForm as CompoundedComponent; 15 | 16 | Form.Group = Group; 17 | 18 | export { useForm }; 19 | 20 | export default Form; 21 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { PropsType } from 'antd-mobile-v2/es/date-picker/index'; 2 | import { INomarDatePickerProps } from '../NomarDatePicker/interface'; 3 | 4 | export interface DateProps extends PropsType { 5 | defaultValue?: Date | string; 6 | } 7 | 8 | export interface IRangeDatePickerProps extends INomarDatePickerProps { 9 | fieldProps2: string; 10 | placeholder2?: string; 11 | minDate?: Date; 12 | maxDate?: Date; 13 | positionType?: 'vertical' | 'horizontal'; 14 | secondProps?: DateProps; 15 | firstProps?: DateProps; 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | // alita-test is @umijs/test@4 6 | import { createJestConfig } from 'alita-test' 7 | 8 | const config = createJestConfig({ 9 | collectCoverageFrom(memo = []) { 10 | return memo.concat(['src/**','!src/.umi/**','!**/assets/*', '!**/demo/*', '!**/tests/demos/*']) 11 | }, 12 | }, { useEsbuild: false }); 13 | // useEsbuild: true 会快3倍左右,但是不完全检测类型,所以在类型全部正确的情况下,可默认开启 14 | // console.log(config) 15 | export default config; -------------------------------------------------------------------------------- /src/baseComponents/TextItem/interface.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface ITextItemProps { 4 | isVertical?: boolean; 5 | value?: string; 6 | placeholder?: string; 7 | onClick?: (val: string) => void; 8 | labelNumber?: number; 9 | coverStyle?: React.CSSProperties; 10 | extra?: string | React.ReactNode; 11 | disabled?: boolean; 12 | maxLine?: number; 13 | fieldProps?: string; 14 | className?: string; 15 | arrow?: boolean; 16 | ellipsis?: boolean; 17 | clear?: boolean; 18 | clearClick?: () => void; 19 | children?: React.ReactNode; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/NomarInput/interface.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InputItemPropsType } from 'antd-mobile-v2/es/input-item/PropsType'; 3 | import { ClickEvent, BaseComponentProps } from '../../PropsType'; 4 | 5 | export type InputType = 6 | | React.InputHTMLAttributes & 7 | InputItemPropsType & 8 | BaseComponentProps; 9 | 10 | export interface INomarInputProps extends InputType { 11 | inputType?: InputItemPropsType['type']; 12 | coverStyle?: React.CSSProperties; 13 | onClick?: (e: ClickEvent) => void; 14 | maxLine?: number; 15 | unit?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/NomarRadio/interface.ts: -------------------------------------------------------------------------------- 1 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 2 | 3 | export interface RadioItem { 4 | [key: string]: string | number; 5 | } 6 | 7 | export interface INomarRadioProps extends BaseComponentProps { 8 | placeholder?: string; 9 | data?: RadioItem[] | []; 10 | coverStyle?: React.CSSProperties; 11 | radioType?: 'horizontal' | 'vertical'; 12 | onChange?: (currentActiveLink: string | number | undefined) => void; 13 | alias?: IAliasProps; 14 | allowUnChecked?: boolean; 15 | labelNumber?: number; 16 | extra?: string | React.ReactNode; 17 | defaultValue?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/NomarCustom/interface.ts: -------------------------------------------------------------------------------- 1 | import { BaseComponentProps } from '../../PropsType'; 2 | export interface INomarCustomPorps extends BaseComponentProps { 3 | onChange?: (currentActiveLink: any) => void; 4 | customDomProps?: any; 5 | CustomDom?: any; 6 | extra?: string | React.ReactNode; 7 | defaultValue?: string; 8 | children: React.ReactElement; 9 | labelNumber?: number; 10 | } 11 | 12 | export interface CustomGroupProps { 13 | value?: any; 14 | onChange?: (res: any) => any; 15 | customDomProps?: any; 16 | CustomDom?: any; 17 | isVertical: boolean; 18 | cutomTitle: React.ReactNode; 19 | children?: any; 20 | } 21 | -------------------------------------------------------------------------------- /src/baseComponents/Context/index.ts: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | 3 | export interface DformContextProps { 4 | errorValue: Record; 5 | changeForm: Record; 6 | formFlag: boolean; 7 | updateErrorValue: (res: string) => void; 8 | } 9 | 10 | export const DformContext = createContext({ 11 | errorValue: {}, 12 | changeForm: {}, 13 | formFlag: false, 14 | updateErrorValue: () => {}, 15 | }); 16 | 17 | export interface CardContextProps { 18 | cDisabled: boolean; 19 | } 20 | 21 | export const CardContext = createContext({ 22 | cDisabled: false, 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/NomarCustom/CustomGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { CustomGroupProps } from './interface'; 3 | 4 | const CustomGroup: FC = (props) => { 5 | const { children, value, onChange, CustomDom, customDomProps } = props; 6 | 7 | const dom = () => { 8 | if (React.isValidElement(children)) { 9 | return React.cloneElement(children, { 10 | // @ts-ignore 11 | ...children.props, 12 | value, 13 | onChange, 14 | }); 15 | } 16 | return ; 17 | }; 18 | return <>{dom()}; 19 | }; 20 | 21 | export default CustomGroup; 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: alita 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/components/NomarFile/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-file { 4 | &-content { 5 | padding-left: 6 * @hd; 6 | padding-top: 6 * @hd; 7 | } 8 | 9 | &-item { 10 | display: flex; 11 | align-items: center; 12 | padding-bottom: 8 * @hd; 13 | padding-right: 15 * @hd; 14 | } 15 | 16 | &-title { 17 | font-size: @alita-dform-value-font-size; 18 | font-family: PingFangSC-Regular, PingFang SC; 19 | font-weight: 400; 20 | color: rgba(51, 51, 51, 1); 21 | padding-right: 8 * @hd; 22 | overflow: hidden; 23 | white-space: nowrap; 24 | text-overflow: ellipsis; 25 | word-break: break-all; 26 | // max-width: calc(100% - 18 * @hd); 27 | flex: 1; 28 | } 29 | &-item-extra { 30 | font-size: 18 * @hd; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/NomarPicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'rc-field-form/es/interface'; 2 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 3 | 4 | export interface IDataItem { 5 | [key: string]: string | number; 6 | } 7 | 8 | export interface INomarPickerProps extends BaseComponentProps { 9 | data: IDataItem[]; 10 | onChange?: (currentActiveLink: string | number) => void; 11 | coverStyle?: React.CSSProperties; 12 | placeholder?: string; 13 | initValue?: string | number; 14 | labelNumber?: number; 15 | onClick?: (val: string | number | undefined) => void; 16 | alias?: IAliasProps; 17 | extra?: string | React.ReactNode; 18 | defaultValue?: string; 19 | clear?: boolean; 20 | customTitle?: React.ReactNode; 21 | insistOpenModal?: boolean; 22 | arrow?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/NomarInput/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 输入框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DformInput from '../../'; 8 | 9 | const Page: FC = () => { 10 | const [inputValue, setInputValue] = useState('1'); 11 | return ( 12 | <> 13 | setInputValue(e)} 20 | defaultValue={inputValue} 21 | /> 22 | 23 | 26 | 27 | ); 28 | }; 29 | 30 | export default Page; 31 | -------------------------------------------------------------------------------- /src/components/NomarInput/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 输入框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { DformInput } from '@alitajs/dform'; 8 | 9 | const Page: FC = () => { 10 | const [inputValue, setInputValue] = useState('1'); 11 | return ( 12 | <> 13 | setInputValue(e)} 20 | defaultValue={inputValue} 21 | /> 22 | 23 | 26 | 27 | ); 28 | }; 29 | 30 | export default Page; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react-jsx", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "declaration": true, 13 | "skipLibCheck": true, 14 | "paths": { 15 | "@/*": ["src/*"], 16 | "@@/*": ["src/.umi/*"], 17 | "@alitajs/dform": ["src/index.ts"], 18 | "@alitajs/dform/src/*": ["src/*"] 19 | }, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "include": ["typings.d","./src"], 23 | "exclude": [ 24 | "node_modules", 25 | "lib", 26 | "es", 27 | "dist", 28 | "typings", 29 | "**/__test__", 30 | "test", 31 | "docs", 32 | "tests" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | 3 | env: 4 | NODE_OPTIONS: --max-old-space-size=6144 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 'Checkout' 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: 'Setup Node.js' 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 14 24 | 25 | - name: install 26 | run: yarn 27 | 28 | - name: Build 29 | run: yarn run docs:build 30 | 31 | - name: Deploy 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./docs-dist 36 | cname: dform.alitajs.com 37 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 输入框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { DformTextArea } from '@alitajs/dform'; 8 | 9 | const Page: FC = () => { 10 | const [value, setValue] = useState('123'); 11 | return ( 12 | <> 13 | { 20 | setValue(str); 21 | }} 22 | /> 23 | 24 | 27 | 28 | ); 29 | }; 30 | 31 | export default Page; 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | 15 | - [ ] `npm test` passes 16 | - [ ] tests are included 17 | - [ ] documentation is changed or added 18 | - [ ] commit message follows commit guidelines 19 | 20 | ##### Description of change 21 | 22 | 23 | 24 | - any feature? 25 | - close https://github.com/alitajs/DynamicForm/ISSUE_URL 26 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import NomarImagePicker from '../..'; 3 | 4 | interface BasicProps {} 5 | 6 | const fileList = [ 7 | { 8 | url: 'https://zos.alipayobjects.com/rmsportal/PZUUCKTRIHWiZSY.jpeg', 9 | id: '2121', 10 | }, 11 | { 12 | url: 'https://zos.alipayobjects.com/rmsportal/hqQWgTXdrlmVVYi.jpeg', 13 | id: '2122', 14 | }, 15 | ]; 16 | 17 | const Page: React.FC = ({}) => { 18 | const [value, setValue] = useState(fileList); 19 | 20 | return ( 21 | <> 22 | { 28 | setValue(files); 29 | }} 30 | /> 31 | 32 | ); 33 | }; 34 | export default Page; 35 | -------------------------------------------------------------------------------- /src/components/NomarFile/interface.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'rc-field-form/es/interface'; 2 | import { BaseComponentProps } from '../../PropsType'; 3 | 4 | export interface INomarFileItemProps { 5 | [key: string]: any; 6 | } 7 | 8 | export interface INomarFileProps extends BaseComponentProps { 9 | extra?: string | React.ReactNode; 10 | uploadExtra?: string | React.ReactNode; 11 | onClick?: (val: INomarFileItemProps) => void; 12 | onChange?: ( 13 | val: INomarFileItemProps[], 14 | item: INomarFileItemProps, 15 | type: 'add' | 'delete', 16 | ) => void; 17 | upload: (res: any) => void; 18 | alias?: { 19 | id: string | number; 20 | title: string; 21 | }; 22 | defaultValue?: INomarFileItemProps[]; 23 | fileProps?: any; 24 | maxLength?: number; 25 | valueStyle?: React.CSSProperties; 26 | itemExtra?: (item: INomarFileItemProps, index: number) => React.ReactNode; 27 | } 28 | -------------------------------------------------------------------------------- /src/baseComponents/DynamicForm/Group.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Fragment } from 'react'; 2 | import { WingBlank, WhiteSpace } from 'antd-mobile-v2'; 3 | import Card from './Card'; 4 | import { GroupProps } from '../../PropsType'; 5 | import { allPrefixCls } from '../../const'; 6 | 7 | const Group: FC = (props) => { 8 | const { children, type = 'empty', ...otherProps } = props; 9 | switch (type) { 10 | case 'empty': 11 | return {children}; 12 | case 'card': 13 | // 这里嵌套一层div 用来书写定义样式 14 | return ( 15 |
16 | 17 | 18 | {children} 19 | 20 |
21 | ); 22 | default: 23 | return <>; 24 | } 25 | }; 26 | 27 | Group.displayName = 'group'; 28 | export default Group; 29 | -------------------------------------------------------------------------------- /src/components/NomarSelect/interface.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'rc-field-form/es/interface'; 2 | import { PickerData } from 'antd-mobile-v2/lib/picker/PropsType'; 3 | import { PickerPropsType } from 'antd-mobile-v2/es/picker/PropsType'; 4 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 5 | 6 | type SelectType = Omit & 7 | BaseComponentProps; 8 | 9 | export interface INomarSelectProps extends SelectType { 10 | cols?: number; 11 | type?: string; 12 | data: PickerData[] | any; 13 | placeholder?: string; 14 | onClick?: (val: string | number | undefined) => void; 15 | extra?: string | React.ReactNode; 16 | coverStyle?: React.CSSProperties; 17 | onChange?: (event: (string | number)[]) => void; 18 | labelNumber?: number; 19 | alias?: IAliasProps; 20 | defaultValue?: any; 21 | maxLine?: number | undefined; 22 | arrow?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/tests/deepUsage/deepUsageTest.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 输入框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | DformInput, 12 | DformRadio, 13 | DformPicker, 14 | DformDatePicker, 15 | RangeDatePicker, 16 | DformCheckBox, 17 | MultiplePicker, 18 | } from '../..'; 19 | 20 | const Page: FC = () => { 21 | const [form] = useForm(); 22 | const onFinish = (values: Store) => {}; 23 | 24 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => {}; 25 | 26 | const formsValues = {}; 27 | const formProps = { 28 | onFinish, 29 | onFinishFailed, 30 | formsValues, 31 | form, 32 | }; 33 | 34 | return ( 35 |
36 | 37 |
38 | ); 39 | }; 40 | 41 | export default Page; 42 | -------------------------------------------------------------------------------- /src/baseComponents/DynamicForm/components/ExpandView/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Icon } from 'antd-mobile-v2'; 3 | import './index.less'; 4 | 5 | interface ExpandViewProps { 6 | /** 7 | * @description 是否展开 8 | */ 9 | isExtand: boolean; 10 | 11 | /** 12 | * @description 展开回调 13 | */ 14 | onChange: (isExtand: boolean) => void; 15 | } 16 | 17 | const prefixCls = 'alitajs-dform'; 18 | const ExpandView: FC = ({ isExtand = false, onChange }) => { 19 | return ( 20 |
21 |
{ 24 | onChange && onChange(!isExtand); 25 | }} 26 | > 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | ExpandView.displayName = 'ExpandView'; 34 | export default ExpandView; 35 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-area { 4 | .am-textarea-control textarea { 5 | color: @alita-dform-value-color; 6 | font-size: @alita-dform-value-font-size; 7 | } 8 | .am-list-item { 9 | padding-left: 0px; 10 | } 11 | 12 | .am-textarea-item { 13 | background-color: transparent; 14 | } 15 | 16 | .am-textarea-focus textarea { 17 | outline: none; 18 | outline-color: transparent; 19 | } 20 | .am-textarea-control { 21 | padding: 0px; 22 | } 23 | 24 | .am-list-item { 25 | min-height: 0 !important; 26 | } 27 | .am-textarea-label { 28 | min-height: 0 !important; 29 | line-height: 24 * @hd !important; 30 | } 31 | } 32 | 33 | .@{prefixCls}-vertical-area { 34 | .am-textarea-label { 35 | display: none; 36 | } 37 | 38 | .am-list-item { 39 | padding-left: 0px; 40 | background-color: transparent; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import NomarTextArea from '../../'; 3 | 4 | interface BasicProps { 5 | onChange: (val: string | undefined) => void; 6 | onBlur: (val: string | undefined) => void; 7 | } 8 | 9 | const Page: React.FC = ({ onChange, onBlur }) => { 10 | const [value, setValue] = useState('123'); 11 | 12 | return ( 13 | <> 14 | { 22 | onChange(str); 23 | setValue(str); 24 | }} 25 | onBlur={(str: string | undefined) => { 26 | onBlur(str); 27 | setValue(str); 28 | }} 29 | /> 30 | 31 | ); 32 | }; 33 | export default Page; 34 | -------------------------------------------------------------------------------- /scripts/verifyCommit.js: -------------------------------------------------------------------------------- 1 | // Invoked on the commit-msg git hook by yorkie. 2 | 3 | const chalk = require('chalk'); 4 | const msgPath = process.env.GIT_PARAMS; 5 | const msg = require('fs').readFileSync(msgPath, 'utf-8').trim(); 6 | 7 | const commitRE = /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|dep)(\(.+\))?: .{1,50}/; 8 | 9 | if (!commitRE.test(msg)) { 10 | console.log(); 11 | console.error( 12 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`invalid commit message format.`)}\n\n` + 13 | chalk.red( 14 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n`, 15 | ) + 16 | ` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` + 17 | ` ${chalk.green(`fix(v-model): handle events on blur (close #28)`)}\n\n` + 18 | chalk.red(` See .github/commit-convention.md for more details.\n`), 19 | ); 20 | process.exit(1); 21 | } 22 | -------------------------------------------------------------------------------- /src/baseComponents/InputItem/interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InputEventHandler, 3 | StringAndUdfEvent, 4 | StringEvent, 5 | } from '../../PropsType'; 6 | 7 | export interface IInputItemProps { 8 | children?: any; 9 | fieldProps: string; 10 | isVertical?: boolean; 11 | value?: string; 12 | placeholder?: string; 13 | editable?: boolean; 14 | onClick?: (e: React.MouseEvent) => void; 15 | labelNumber?: number; 16 | onChange?: (e: StringEvent) => void; 17 | coverStyle?: React.CSSProperties; 18 | disabled?: boolean; 19 | extra?: string | React.ReactNode; 20 | className?: string; 21 | onBlur?: InputEventHandler; 22 | onFocus?: InputEventHandler; 23 | clear?: boolean; 24 | maxLength?: number; 25 | unit?: string; 26 | min?: string | number; 27 | max?: string | number; 28 | type?: 29 | | 'text' 30 | | 'bankCard' 31 | | 'phone' 32 | | 'password' 33 | | 'number' 34 | | 'digit' 35 | | 'money'; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/NomarDatePicker/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 时间选择框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button } from 'antd-mobile-v2'; 7 | import { DformDatePicker } from '../../../../index'; 8 | 9 | interface SingleProps {} 10 | 11 | const Page: FC = () => { 12 | const [value, setValue] = useState( 13 | '2022年01月01日', 14 | ); 15 | return ( 16 |
17 | setValue(e)} 25 | replaceName={{ 26 | [`年`]: '-', 27 | [`月`]: '-', 28 | [`日`]: '', 29 | }} 30 | /> 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default Page; 37 | -------------------------------------------------------------------------------- /src/components/NomarRadio/demo/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import { DformRadio } from '@alitajs/dform'; 4 | 5 | const Page: React.FC = () => { 6 | const dataSource = [ 7 | { 8 | label: '男', 9 | value: '1', 10 | }, 11 | { 12 | label: '女', 13 | value: '0', 14 | }, 15 | ]; 16 | const [value, setValue] = useState('0'); 17 | 18 | return ( 19 | <> 20 | { 28 | setValue(gender as string); 29 | }} 30 | /> 31 | 32 | 35 | 36 | ); 37 | }; 38 | export default Page; 39 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { ImagePickerPropTypes } from 'antd-mobile-v2/es/image-picker/PropsType'; 2 | import { Rule } from 'rc-field-form/es/interface'; 3 | import { BaseComponentProps } from '../../PropsType'; 4 | 5 | export interface ImageFile { 6 | url: string; 7 | [key: string]: any; 8 | } 9 | 10 | type ImageType = ImagePickerPropTypes & BaseComponentProps; 11 | 12 | export interface INomarImagePickerProps extends ImageType { 13 | coverStyle?: React.CSSProperties; 14 | limitSize?: number; 15 | hidden?: boolean; 16 | extra?: string | React.ReactNode; 17 | compressRatio?: number; 18 | defaultValue?: any[] | undefined; 19 | maxLength?: number; 20 | } 21 | 22 | export interface ImagePickerGroupProps extends INomarImagePickerProps { 23 | value?: any[] | undefined; 24 | onChange: ( 25 | files: ImageFile[], 26 | operationType: string, 27 | index: number | undefined, 28 | ) => void; 29 | limitSize?: number | undefined; 30 | compressRatio?: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/tests/demos/formtest.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../baseComponents/DynamicForm'; 4 | import { useForm } from 'rc-field-form'; 5 | import DformCheckBox from '../../'; 6 | 7 | const CoupletText: FC = () => { 8 | const [form] = useForm(); 9 | const [formsValues, setFormsValues] = React.useState({ fruit: [] }); 10 | React.useEffect(() => { 11 | setFormsValues({ 12 | fruit: [...formsValues.fruit, 'milk', 'fruitJuice'], 13 | }); 14 | }, []); 15 | 16 | const formProps = { 17 | formsValues, 18 | form, 19 | failScroll: false, 20 | isDev: true, 21 | }; 22 | return ( 23 | <> 24 | 25 | 26 | 34 | 35 | ); 36 | }; 37 | export default CoupletText; 38 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as AddressPicker } from './AddressPicker'; 2 | export { default as CoverRadio } from './CoverRadio'; 3 | export { default as ExtraInput } from './ExtraInput'; 4 | export { default as MultiplePicker } from './MultiplePicker'; 5 | export { default as DformInput } from './NomarInput'; 6 | export { default as DformSelect } from './NomarSelect'; 7 | export { default as DformPicker } from './NomarPicker'; 8 | export { default as DformSwitch } from './NomarSwitch'; 9 | export { default as DformTextArea } from './NomarTextArea'; 10 | export { default as DformDatePicker } from './NomarDatePicker'; 11 | export { default as DformRadio } from './NomarRadio'; 12 | export { default as DformCheckBox } from './NomarCheckBox'; 13 | export { default as DformImagePicker } from './NomarImagePicker'; 14 | export { default as DformCustom } from './NomarCustom'; 15 | export { default as DformText } from './NomarText'; 16 | export { default as DformFile } from './NomarFile'; 17 | export { default as RangeDatePicker } from './RangeDatePicker'; 18 | export { default as Field } from './Field'; 19 | -------------------------------------------------------------------------------- /src/demo/jsonGroup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JSON分组 3 | group: 4 | title: JSON分组 5 | path: /jso 6 | nav: 7 | title: 分组 8 | path: /groupUsage 9 | --- 10 | 11 | # JSON 分组 12 | 13 | ## 代码演示 14 | 15 | 16 | 17 | ## API 18 | 19 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 20 | | ---------- | ---------- | ------------ | ------ | -------- | 21 | | groupProps | 分组的属性 | `GroupProps` | `-` | 是 | 22 | 23 | ## GroupProps API 24 | 25 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 26 | | --------- | ---------------------- | --------------------- | ------- | -------- | 27 | | type | 分组类型 | `empty` \| `card` | `empty` | 否 | 28 | | title | 标题 | `string` \| `node` | `-` | 否 | 29 | | required | 是否必填(只做样式显示) | `boolean` | `false` | 否 | 30 | | classname | 自定义样式名 | `React.CSSProperties` | `-` | 否 | 31 | | leftView | 左侧样式 | `string` \| `node` | `-` | 否 | 32 | | rightView | 左侧样式 | `string` \| `node` | `-` | 否 | 33 | -------------------------------------------------------------------------------- /src/components/MultiplePicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'rc-field-form/es/interface'; 2 | import React from 'react'; 3 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 4 | 5 | export interface IDataItem { 6 | [key: string]: string | number; 7 | } 8 | 9 | export interface ChangeValLink { 10 | linkVals?: Array; 11 | unLlinkVals?: Array; 12 | } 13 | 14 | export interface ValueLinks { 15 | [key: string]: ChangeValLink; 16 | } 17 | export interface IMultiplePickerProps extends BaseComponentProps { 18 | data: IDataItem[]; 19 | onChange?: (currentActiveLink: (string | number)[]) => void; 20 | coverStyle?: React.CSSProperties; 21 | placeholder?: string; 22 | maxValueLength?: number; 23 | labelNumber?: number; 24 | onClick?: () => void; 25 | leftContent?: React.ReactNode | string; 26 | rightContent?: React.ReactNode | string; 27 | height?: number | string; 28 | alias?: IAliasProps; 29 | extra?: string | React.ReactNode; 30 | defaultValue?: (string | number)[] | undefined; 31 | clear?: boolean; 32 | arrow?: boolean; 33 | valueLinks?: ValueLinks; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/demo/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import { DformImagePicker } from '@alitajs/dform'; 4 | 5 | interface BasicProps { 6 | onFinish: any; 7 | onFinishFailed: any; 8 | onChange: any; 9 | onImageClick: any; 10 | } 11 | 12 | const fileList = [ 13 | { 14 | url: 'https://zos.alipayobjects.com/rmsportal/PZUUCKTRIHWiZSY.jpeg', 15 | id: '2121', 16 | }, 17 | { 18 | url: 'https://zos.alipayobjects.com/rmsportal/hqQWgTXdrlmVVYi.jpeg', 19 | id: '2122', 20 | }, 21 | ]; 22 | 23 | const Page: React.FC = ({}) => { 24 | const [value, setValue] = useState(fileList); 25 | 26 | return ( 27 | <> 28 | { 34 | setValue(files); 35 | }} 36 | /> 37 | 38 | 39 | 40 | ); 41 | }; 42 | export default Page; 43 | -------------------------------------------------------------------------------- /src/components/NomarRadio/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import NomarRadio from '../../'; 4 | 5 | interface BasicProps { 6 | onChange: (val: string) => void; 7 | } 8 | 9 | const Page: React.FC = ({ onChange }) => { 10 | const dataSource = [ 11 | { 12 | label: '男', 13 | value: '1', 14 | }, 15 | { 16 | label: '女', 17 | value: '0', 18 | }, 19 | ]; 20 | const [value, setValue] = useState('0'); 21 | 22 | return ( 23 | <> 24 | { 32 | onChange(gender as string); 33 | setValue(gender as string); 34 | }} 35 | /> 36 | 37 | 40 | 41 | ); 42 | }; 43 | export default Page; 44 | -------------------------------------------------------------------------------- /src/components/AddressPicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'rc-field-form/es/interface'; 2 | import React from 'react'; 3 | import { IAliasProps, BaseComponentProps } from '../../PropsType'; 4 | 5 | export interface IModalData { 6 | [key: string]: string | number; 7 | } 8 | 9 | export interface IAddressPickerProps extends BaseComponentProps { 10 | onChange?: (currentActiveLink: any) => void; 11 | placeholder?: string; 12 | extra?: string | React.ReactNode; 13 | // level: number; 14 | data?: IModalData[]; 15 | onChangeLevel: (value: (string | number)[]) => void; 16 | placeholderList: string[]; 17 | labelNumber?: number; 18 | coverStyle?: React.CSSProperties; 19 | onClick?: () => void; 20 | leftContent?: string | React.ReactNode; 21 | rightContent?: string | React.ReactNode; 22 | height?: number | string; 23 | noData?: string | React.ReactNode; 24 | loading?: boolean; 25 | className?: string; 26 | alias?: IAliasProps; 27 | defaultValue?: valueProps; 28 | onChangeVerifies?: (value: any) => boolean; 29 | arrow?: boolean; 30 | } 31 | 32 | export interface valueProps { 33 | label: (string | number)[]; 34 | value: (string | number)[]; 35 | } 36 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: alitajs/dform 4 | desc: dynamicForm3 site example 5 | actions: 6 | - text: Getting Started 7 | link: /components 8 | features: 9 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png 10 | title: 组件丰富 11 | desc: 涵盖 17 种表单,每个表单组件都有丰富的示例供您体验。 12 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png 13 | title: 便捷样式配置 14 | desc: 一行代码配置整个项目的表单样式。 15 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d1ee0c6f-5aed-4a45-a507-339a4bfe076c/k7bjsocq_w144_h144.png 16 | title: 使用快捷 17 | desc: UI 快速实现,实现一次性全部赋值,表单提交取值,支持动态表单。 18 | footer: Open-source MIT Licensed | Copyright © 2020
Powered by [dumi](https://d.umijs.org) 19 | --- 20 | 21 | # DynamicForm 22 | 23 | ## 特性 24 | 25 | 借鉴了 `antd@4` 的 `Form` 组件,针对表单使用的 `react-component/field-form` 库进行二次封装。 26 | 27 | - 🚀 UI 的快速实现。 28 | - 🎉 实现一次性全部赋值。 29 | - 🍁 表单提交取值。 30 | - 💄 融合多类型组件表单。 31 | - 🌈 支持动态表单。 32 | - 🐠 公司内部数十个项目中得到锤炼,不断优化完善。 33 | 34 | ## 快速上手 35 | 36 | ```bash 37 | npm install @alitajs/dform 38 | 39 | or 40 | 41 | yarn add @alitajs/dform 42 | ``` 43 | -------------------------------------------------------------------------------- /src/components/NomarDatePicker/interface.ts: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { PropsType } from 'antd-mobile-v2/es/date-picker/index'; 3 | import { ErrorValueProps, BaseComponentProps } from '../../PropsType'; 4 | 5 | export interface DateProps extends Omit { 6 | defaultValue?: Date | string; 7 | arrow?: boolean; 8 | extra?: string | React.ReactNode; 9 | } 10 | 11 | export type DatePickerType = PropsType & BaseComponentProps; 12 | 13 | export interface INomarDatePickerProps extends Omit { 14 | modeType?: PropsType['mode']; 15 | fieldProps2?: string; 16 | secondProps?: DateProps; 17 | placeholder?: string; 18 | labelNumber?: number; 19 | coverStyle?: CSSProperties; 20 | errorValue?: ErrorValueProps; 21 | defaultValue?: Date | undefined | string; 22 | arrow?: boolean; 23 | extra?: string | React.ReactNode; 24 | replaceName?: Record; 25 | } 26 | 27 | export interface INomarDatePickerGroupProps 28 | extends Omit { 29 | onChange: (e: any) => void; 30 | value?: Date | string | undefined; 31 | arrow?: boolean; 32 | children?: React.ReactNode; 33 | } 34 | -------------------------------------------------------------------------------- /src/baseComponents/InputItem/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-input-item { 4 | // height: @alita-dform-height; 5 | display: flex; 6 | flex-direction: row; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | &-value { 11 | font-size: @alita-dform-value-font-size; 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | justify-content: center; 16 | flex: 1; 17 | } 18 | 19 | &-text { 20 | width: 100%; 21 | border: 0 * @hd; 22 | color: @alita-dform-value-color; 23 | background-color: transparent; 24 | font-size: @alita-dform-value-font-size; 25 | } 26 | &-unit { 27 | padding: 0 8 * @hd; 28 | color: #1c242e; 29 | font-size: 14 * @hd; 30 | text-align: center; 31 | white-space: nowrap; 32 | vertical-align: middle; 33 | background-color: #fff; 34 | border: none; 35 | } 36 | &-text:focus { 37 | outline: none; 38 | outline-color: transparent; 39 | } 40 | 41 | &-value input::placeholder { 42 | color: @alita-dform-placeholder !important; 43 | } 44 | 45 | &-focus { 46 | .@{prefixCls}-clear { 47 | display: block; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/MultiplePicker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .alitajs-dform-multiple-picker { 4 | .am-modal-header { 5 | padding: 0 * @hd !important; 6 | } 7 | 8 | .alitajs-dform-multiple-picker-item { 9 | width: 100%; 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | justify-content: space-between; 14 | 15 | .alitajs-dform-multiple-picker-label { 16 | font-size: @alita-dform-value-font-size; 17 | color: #000; 18 | } 19 | 20 | .alitajs-dform-multiple-picker-checked { 21 | color: @alita-dform-radio-color; 22 | } 23 | .alitajs-dform-box-wrapper-disabled, 24 | .alitajs-dform-box-wrapper-disabled + div { 25 | color: @color-text-disabled; 26 | filter: grayscale(100%); 27 | } 28 | 29 | .alitajs-dform-multiple-picker-right { 30 | width: 40 * @hd; 31 | height: 40 * @hd; 32 | // border: 1 * @hd solid red; 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | justify-content: center; 37 | } 38 | } 39 | } 40 | 41 | .alitajs-dform-modal-content { 42 | overflow-y: auto; 43 | max-height: 800 * @hd; 44 | min-height: 300 * @hd; 45 | } 46 | -------------------------------------------------------------------------------- /change.md: -------------------------------------------------------------------------------- 1 | # 版本升级内容 2 | 3 | ## 3.4.16 4 | 5 | picker 组件的数据源为 `undeined` 时,页面容错。 6 | 7 | ## 3.4.13 && 3.4.14 8 | 9 | 统一类型定义文件的归口,统一到 interface.ts 下。 10 | 11 | ## 3.4.12 12 | 13 | 修复 3.4.11 的小问题。 14 | 15 | ## 3.4.11 16 | 17 | hidden > required、rules 并且通过 defaultValue 和 formValues 设置的值不会消失。会在 submit 的时候带出来。 18 | 19 | ## 3.4.10 20 | 21 | 删除 input 组件内多余的 console。 22 | 23 | ## 3.4.9 24 | 25 | 修复 input 组件赋值 0 失效的问题。 26 | 27 | ## 3.4.8 28 | 29 | 修复 ExtraInput 第二个属性的值获取到是 `undefined` 30 | 31 | ## 3.4.0 32 | 33 | - 1、分组增加卡片收缩功能。-------书航 34 | - 2、image 和 file 组件增加 maxLength 字段。用于设置上传文件最大数量的限制。----书航 35 | - 3、优化 addressPicker 组件的逻辑和性能,删除 level 字段(剩余内容不变)。提高用户使用体验。----DIYC 36 | - 4、组件增加 renderHeader 和 renderFooter 用于渲染表单字段头部和尾部。----书航 37 | - 5、修复 checkbox 组件单独使用时,defaultValue 不生效。------传龙 38 | 39 | ## 3.2.2 40 | 41 | - 1、组件允许单独使用,可以不在 `` 的包裹下才能使用---------传龙 42 | - 2、合并 input 和 text ,后续 text 将放弃维护。--------书航 43 | - 3、合并 datePicker 和 rangeDatePicker, 后续 rangeDatePicker 将放弃维护。------DIYC 44 | - 4、date 的赋值允许传递 string 的类型,不一定要传 Date 类型。-------书航 45 | 46 | ## 3.1.11 47 | 48 | - 1、rules 规则兼容 required ,不需要重写 required 49 | - 2、修复 DformFile 上传文件组件 上传图片消失导致无法上传的问题 50 | - 3、picker 和 multiplePicker 增加 clear 图标,可以清空已选中的数据 51 | 52 | ## 3.1.0 53 | 54 | - 1、表单提交报错时增加红色字体的错误提示。 55 | - 2、表单增加及联操作。 56 | - 3、增加可配置分组。 57 | - 4、优化了整个表单的样式。 58 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | title: '@alitajs/dform', 5 | favicon: 'https://avatars.githubusercontent.com/u/49136103?s=200&v=4', 6 | logo: 'https://avatars.githubusercontent.com/u/49136103?s=200&v=4', 7 | outputPath: 'docs-dist', 8 | mode: 'site', 9 | scripts: [ 10 | `setTimeout(function () { 11 | var menu = document.getElementsByClassName('__dumi-default-menu')[0]; 12 | var navbar = document.getElementsByClassName('__dumi-default-navbar')[0]; 13 | if(navbar && navbar?.offsetHeight) { 14 | const isMobile = navbar?.offsetHeight == 50; 15 | if (!isMobile) { 16 | var github = document.createElement('p'); 17 | github.className = 'github'; 18 | github.style.position = 'absolute'; 19 | github.style.top = '8px'; 20 | github.style.left = '280px'; 21 | github.innerHTML = ''; 22 | navbar.appendChild(github); 23 | } 24 | } 25 | }, 300)`, 26 | ], 27 | theme: { 28 | '@hd': '0.02rem', 29 | }, 30 | navs: [ 31 | null, 32 | , 33 | { title: 'v2', path: 'https://d.alitajs.com/' }, 34 | { title: 'GitHub', path: 'https://github.com/alitajs/DynamicForm' }, 35 | ], 36 | }); 37 | -------------------------------------------------------------------------------- /src/docs/update.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 升级说明 3 | group: 4 | title: 升级说明 5 | nav: 6 | title: 升级说明 7 | path: /upgrade 8 | order: 5 9 | --- 10 | 11 | # 升级说明 12 | 13 | ## 3.10.x 14 | 15 | ## 3.10.4 16 | 17 | 修复时间区间组件,设置起始时间会改动到结束时间的问题。 18 | 19 | ### 3.10.2 20 | 21 | 修复图片组件 `disabled` 失效的问题。 22 | 23 | ## 3.9.X 24 | 25 | ### 3.9.5 26 | 27 | 修复`Radio`组件 `radioType`为`vertical`且`positionType`为`horizontal`时,效果不生效的问题 28 | 29 | ### 3.9.4 30 | 31 | 修复`Radio`组件 `radioType`为`vertical`且`positionType`为`vertical`时,标题不展示的问题 32 | 33 | ### 3.9.3 34 | 35 | 缩小包体积 36 | 37 | ### 3.9.2 38 | 39 | 新增 `Group` 组件属性 `boxStyle`, `titleStyle`, 用于设置分组样式和分组 `title` 样式。 40 | 41 | ### 3.9.1 42 | 43 | 新增 `Card` 组件属性 `disabled`,支持 Card 整体禁用,新增 `hideStar` 属性,用于隐藏必填 `*` 号 44 | 45 | ### 3.9.0 46 | 47 | 完善 `Date` 组件,支持在非表单状态下单独使用。 48 | 49 | ## 3.8.X 50 | 51 | ### 3.8.9 52 | 53 | 修复 `Date` 组件 `replaceName` 属性,在 `Date` 弹框不生效的问题。 54 | 55 | ### 3.8.8 56 | 57 | `Date` 组件 `replaceName` 组件。支持替换中文的 `value` 格式。 58 | 59 | ### 3.8.7 60 | 61 | `Date` 组件新增 format 处理 62 | 63 | ### 3.8.6 64 | 65 | `Input` 组件新增 unit、min、max 属性 66 | 67 | ### 3.8.5 68 | 69 | 修复 `Card` 组件 `className` 无效的问题。 70 | 71 | ### 3.8.4 72 | 73 | - `select`、`date` 增加 `arrow` 的入参,允许隐藏右侧箭头。 74 | 75 | ### 3.8.3 76 | 77 | - `picker`、`addressPicker`、`multiplePicker` 增加 `arrow` 的入参,允许隐藏右侧箭头。 78 | - `picker`、`addressPicker`、`multiplePicker` 允许在横向模式下支持 `extra`。 79 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm, { useForm } from '../../../../index'; 4 | import RangeDatePicker from '../../'; 5 | 6 | interface BasicProps { 7 | onFinish: any; 8 | onFinishFailed: any; 9 | } 10 | 11 | const page: FC = ({ onFinish, onFinishFailed }) => { 12 | const [form] = useForm(); 13 | 14 | const formsValues = { 15 | rangeTime1: new Date(), 16 | }; 17 | 18 | const formProps = { 19 | onFinish, 20 | onFinishFailed, 21 | formsValues, 22 | form, 23 | isDev: false, 24 | }; 25 | 26 | return ( 27 | <> 28 | 29 | {}, 37 | }} 38 | /> 39 | 45 | 46 | 47 | 50 | 51 | ); 52 | }; 53 | 54 | export default page; 55 | -------------------------------------------------------------------------------- /src/components/NomarCustom/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, sleep } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import NomarCustom from '../index'; 5 | 6 | interface IDemoPage { 7 | name: string; 8 | age: number; 9 | onChange?: (currentActiveLink: string) => void; 10 | value?: string; 11 | } 12 | 13 | const demoPage: React.FC = (props) => { 14 | const { name, onChange, value } = props; 15 | return ( 16 |
17 |

name: {name}

18 |

19 | age: 20 | { 25 | if (onChange) onChange(e.target.value); 26 | }} 27 | /> 28 |

29 |
30 | ); 31 | }; 32 | const myProps = { 33 | title: '自定义组件(受控)', 34 | required: true, 35 | fieldProps: '', 36 | fieldName: 'age', 37 | CustomDom: demoPage, 38 | customDomProps: { 39 | name: 'owen', 40 | }, 41 | defaultValue: '17', 42 | }; 43 | 44 | it('passes picker a11y test', async () => { 45 | const { container, getByLabelText } = render( 46 |
47 |
48 | 49 | 50 |
, 51 | ); 52 | await sleep(500); 53 | expect(getByLabelText('input')).toBeDefined(); 54 | await testA11y(container); 55 | }); 56 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd-mobile-v2/lib/style/themes/default.less'; 2 | 3 | @import '~antd-mobile-v2/lib/button/style/index'; 4 | @import '~antd-mobile-v2/lib/input-item/style/index'; 5 | @import '~antd-mobile-v2/lib/list/style/index'; 6 | @import '~antd-mobile-v2/lib/switch/style/index'; 7 | @import '~antd-mobile-v2/lib/white-space/style/index'; 8 | @import '~antd-mobile-v2/lib/wing-blank/style/index'; 9 | @import '~antd-mobile-v2/lib/modal/style/index'; 10 | @import '~antd-mobile-v2/lib/image-picker/style/index.less'; 11 | @import '~antd-mobile-v2/lib/textarea-item/style/index'; 12 | @import '~antd-mobile-v2/lib/picker/style/index.less'; 13 | @import '~antd-mobile-v2/lib/picker-view/style/index'; 14 | @import '~antd-mobile-v2/lib/picker-view/style/picker'; 15 | @import '~antd-mobile-v2/lib/flex/style/index.less'; 16 | @import '~antd-mobile-v2/lib/list/style/index.less'; 17 | @import '~antd-mobile-v2/lib/toast/style/index.less'; 18 | @import '~antd-mobile-v2/lib/icon/style/index.less'; 19 | 20 | @alita-dform-title-font-size: 15 * @hd; // 标题大小 21 | @alita-dform-title-color: #333; // 标题颜色 22 | @alita-dform-value-color: rgba(0, 0, 0, 0.8); // 内容颜色 23 | @alita-dform-value-font-size: 14 * @hd; // 内容大小 24 | @alita-dform-radio-color: #1677ff; // 按钮样式 25 | @alita-dform-radio-font-color: #fff; // 按钮文字样式 26 | @alita-dform-placeholder: rgba(0, 0, 0, 0.2); // placeholder 样式 27 | @color-text-disabled: #999; // 不可编辑样式 28 | @alita-dform-padding: 12 * @hd 0; // 表单上下间距 29 | -------------------------------------------------------------------------------- /src/demo/groupUsage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 组件化分组 3 | group: 4 | title: 组件化分组 5 | path: /usa 6 | order: 1 7 | nav: 8 | title: 分组 9 | path: /groupUsage 10 | order: 3 11 | --- 12 | 13 | # 组件化分组 14 | 15 | ## 代码演示 16 | 17 | 18 | 19 | ## API 20 | 21 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 22 | | ------------- | ---------------------- | --------------------- | ------- | -------- | 23 | | type | 分组类型 | `empty` \| `card` | `empty` | 否 | 24 | | title | 标题 | `string` \| `node` | `-` | 否 | 25 | | required | 是否必填(只做样式显示) | `boolean` | `false` | 否 | 26 | | classname | 自定义样式名 | `React.CSSProperties` | `-` | 否 | 27 | | leftView | 左侧样式 | `string` \| `node` | `-` | 否 | 28 | | rightView | 左侧样式 | `string` \| `node` | `-` | 否 | 29 | | extandPostion | 开关位置 | `top` \| `bottom` | `-` | 否 | 30 | | defaultExtand | 默认是面板开关 | `boolean` | true | 否 | 31 | | disabled | 是否禁用 | `boolean` | false | 否 | 32 | | hideStar | 是否隐藏必填`*` | `boolean` | false | 否 | 33 | | boxStyle | 分组样式 | `React.CSSProperties` | - | 否 | 34 | | titleStyle | 分组 `title` 样式 | `React.CSSProperties` | - | 否 | 35 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../'; 4 | import { useForm } from 'rc-field-form'; 5 | import DformSwitch from '../../'; 6 | 7 | interface BasicProps { 8 | onFinish: any; 9 | onFinishFailed: any; 10 | } 11 | 12 | const Page: FC = ({ onFinish, onFinishFailed }) => { 13 | const [form] = useForm(); 14 | const formsValues = {}; 15 | const formsProps = { 16 | form, 17 | onFinish, 18 | onFinishFailed, 19 | formsValues, 20 | isDev: true, 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 33 | 39 | 47 | 48 | 49 | 52 | 53 | ); 54 | }; 55 | 56 | export default Page; 57 | -------------------------------------------------------------------------------- /src/demo/relativesUsage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 级联规则使用 3 | group: 4 | title: 级联规则使用 5 | nav: 6 | title: 级联规则使用 7 | path: /relativesUsage 8 | order: 4 9 | --- 10 | 11 | # 级联规则使用 12 | 13 | ## 代码演示 14 | 15 | 16 | 17 | ## 级联配置 API 18 | 19 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 20 | | --------- | ------------ | -------------------------------- | ------ | -------- | 21 | | relatives | 表单字段编码 | [key:string]: RelativesItemProps | - | true | 22 | 23 | ### RelativesItemProps 24 | 25 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 26 | | ----------- | -------------- | --------------------------------------------------------------------- | ------ | -------- | 27 | | type | 级联类型 | `changeFormValue` or `required` or `hidden` or `disabled` or `custom` | - | true | 28 | | targetValue | 触发条件数据集 | any[] | [] | true | 29 | | targetSet | 级联内容 | `TargetProps`[] | [] | true | 30 | 31 | ### TargetProps 32 | 33 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 34 | | ------------- | ------------------ | ------ | ------ | -------- | 35 | | targetField | 联动目标字段 | string | - | true | 36 | | targetValue | 修改内容数据 | any | - | false | 37 | | targetContent | 自定义变更表单内容 | any | {} | false | 38 | -------------------------------------------------------------------------------- /src/components/MultiplePicker/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 单独使用 多选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { MultiplePicker } from '@alitajs/dform'; 8 | 9 | const foodList = [ 10 | { 11 | foodName: '宫保鸡丁', 12 | foodId: '宫保鸡丁', 13 | }, 14 | { 15 | foodName: '可乐鸡翅', 16 | foodId: '可乐鸡翅', 17 | }, 18 | { 19 | foodName: '爆炒虾仁', 20 | foodId: '爆炒虾仁', 21 | }, 22 | { 23 | foodName: '清蒸小黄鱼', 24 | foodId: '清蒸小黄鱼', 25 | }, 26 | { 27 | foodName: '红烧肉', 28 | foodId: '红烧肉', 29 | }, 30 | ]; 31 | 32 | const Page: FC = () => { 33 | const [mulValue, setMulValue] = useState<(string | number)[]>(['红烧肉']); 34 | return ( 35 | <> 36 | setMulValue(e)} 51 | /> 52 | 53 | 61 | 62 | ); 63 | }; 64 | 65 | export default Page; 66 | -------------------------------------------------------------------------------- /src/components/CoverRadio/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-cover-radio { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | // min-height: @alita-dform-height; 8 | 9 | &-group { 10 | display: flex; 11 | align-items: center; 12 | width: 100%; 13 | } 14 | 15 | &-field { 16 | flex: 1; 17 | overflow: auto; 18 | } 19 | 20 | .alitajs-dform-cover-radio-wrapper { 21 | font-size: @alita-dform-value-font-size; 22 | padding: 3 * @hd 15 * @hd; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | margin-left: 20 * @hd; 27 | color: @alita-dform-value-color; 28 | // margin-bottom: 7 * @hd; 29 | // margin-top: 7 * @hd; 30 | } 31 | 32 | &-wrapper-checked { 33 | .alitajs-dform-cover-radio-wrapper(); 34 | background-color: @alita-dform-radio-color; 35 | color: @alita-dform-radio-font-color !important; 36 | animation-name: colorful; 37 | animation-duration: 1s; 38 | } 39 | 40 | &-wrapper-margin { 41 | margin-left: 0 * @hd; 42 | margin-right: 20 * @hd; 43 | } 44 | 45 | &-wrapper-cover { 46 | // border: 1 * @hd solid @alita-dform-radio-color; 47 | color: @alita-dform-radio-color; 48 | } 49 | 50 | &-item-vertical { 51 | flex-direction: column; 52 | align-items: flex-start; 53 | } 54 | 55 | &-position { 56 | justify-content: flex-end !important; 57 | } 58 | } 59 | 60 | .alitajs-dform-vertical-cover-radio { 61 | .alitajs-dform-cover-radio(); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DformCheckBox from '../../'; 8 | interface pageProps { 9 | onSubmit?: () => void; 10 | } 11 | 12 | const Page: FC = (props) => { 13 | const { onSubmit } = props; 14 | const [value, setValue] = useState(['huawei', 'apple']); 15 | const dataList = [ 16 | { telId: 'vivo', telName: 'vivo', desc: '这是vivo手机' }, 17 | { telId: 'oppo', telName: 'oppo', desc: '这是oppo手机' }, 18 | { telId: 'honor', telName: '荣耀', desc: '这是荣耀手机' }, 19 | { telId: 'xiaomi', telName: '小米', desc: '这是小米手机' }, 20 | { telId: 'huawei', telName: '华为', desc: '这是华为手机' }, 21 | { telId: 'apple', telName: 'apple', desc: '这是苹果手机' }, 22 | ]; 23 | return ( 24 | <> 25 | setValue(val)} 37 | disableItem={(item) => { 38 | if (item.value === 'huawei') return true; 39 | return false; 40 | }} 41 | /> 42 | 43 | 52 | 53 | ); 54 | }; 55 | 56 | export default Page; 57 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { DformCheckBox } from '@alitajs/dform'; 8 | interface pageProps { 9 | onSubmit?: () => void; 10 | } 11 | 12 | const Page: FC = (props) => { 13 | const { onSubmit } = props; 14 | const [value, setValue] = useState(['huawei', 'apple']); 15 | const dataList = [ 16 | { telId: 'vivo', telName: 'vivo', desc: '这是vivo手机' }, 17 | { telId: 'oppo', telName: 'oppo', desc: '这是oppo手机' }, 18 | { telId: 'honor', telName: '荣耀', desc: '这是荣耀手机' }, 19 | { telId: 'xiaomi', telName: '小米', desc: '这是小米手机' }, 20 | { telId: 'huawei', telName: '华为', desc: '这是华为手机' }, 21 | { telId: 'apple', telName: 'apple', desc: '这是苹果手机' }, 22 | ]; 23 | return ( 24 | <> 25 | setValue(val)} 37 | disableItem={(item) => { 38 | if (item.value === 'huawei') return true; 39 | return false; 40 | }} 41 | /> 42 | 43 | 52 | 53 | ); 54 | }; 55 | 56 | export default Page; 57 | -------------------------------------------------------------------------------- /src/components/NomarFile/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /** 3 | * title: 基础 文件上传 4 | * desc: 表单使用 demo 5 | */ 6 | import React, { FC, useState } from 'react'; 7 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 8 | import { getRandom, DformFile } from '@alitajs/dform'; 9 | 10 | const contractList = [ 11 | { title: '合约模板2020.pdf', fileId: '1' }, 12 | { title: '电子协议模板2020.pdf', fileId: '2' }, 13 | ]; 14 | 15 | const Page: FC = () => { 16 | const [fileValue, setFileValue] = useState(contractList); 17 | return ( 18 | <> 19 | { 24 | console.log(res); 25 | }} 26 | defaultValue={fileValue} 27 | maxLength={3} 28 | onChange={(res: any, delItem: any) => { 29 | console.log(res, delItem); 30 | setFileValue(res); 31 | }} 32 | alias={{ 33 | id: 'fileId', 34 | title: 'title', 35 | }} 36 | upload={(res: any) => { 37 | const list = [...fileValue]; 38 | if (res && res.length) { 39 | res.map((item: any) => { 40 | list.push({ 41 | title: item.name, 42 | fileId: getRandom(), 43 | }); 44 | }); 45 | } 46 | setFileValue(list); 47 | }} 48 | /> 49 | 50 | 58 | 59 | ); 60 | }; 61 | 62 | export default Page; 63 | -------------------------------------------------------------------------------- /src/baseComponents/TextItem/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | @textPrefixCls: alitajs-dform-text-item; 4 | 5 | .@{textPrefixCls} { 6 | // min-height: @alita-dform-height; 7 | display: flex; 8 | flex-direction: row; 9 | // align-items: center; 10 | justify-content: center; 11 | 12 | // .alitajs-dform-text-tltle { 13 | // font-size: @alita-dform-title-font-size; 14 | // color: @alita-dform-title-color; 15 | // // width: 40%; 16 | // } 17 | 18 | .text-item { 19 | word-break: break-all; 20 | -webkit-box-orient: vertical; 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | line-height: 24 * @hd; 24 | } 25 | 26 | .@{prefixCls}-placeholder { 27 | .@{textPrefixCls}-text { 28 | color: @alita-dform-placeholder; 29 | .text-item(); 30 | } 31 | } 32 | 33 | &-min-width { 34 | min-width: 25%; 35 | } 36 | 37 | &-content { 38 | width: 100%; 39 | border: 0 * @hd; 40 | display: flex; 41 | flex-direction: column; 42 | .@{textPrefixCls}-text { 43 | color: @alita-dform-value-color; 44 | .text-item(); 45 | } 46 | } 47 | 48 | &-content-padding { 49 | padding-left: 10 * @hd; 50 | } 51 | 52 | &-value { 53 | font-size: @alita-dform-value-font-size; 54 | display: flex; 55 | flex-direction: row; 56 | align-items: center; 57 | justify-content: center; 58 | flex: 1; 59 | // overflow: hidden; 60 | // white-space: nowrap; 61 | // text-overflow: ellipsis; 62 | } 63 | 64 | &-overflow { 65 | text-align: right; 66 | color: @alita-dform-radio-color; 67 | } 68 | 69 | .am-input-extra { 70 | max-height: 33 * @hd; 71 | display: flex; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import NomarSwitch from '../index'; 5 | import BasicTest from './demos/basic'; 6 | 7 | const myProps = { 8 | fieldProps: 'on', 9 | placeholder: '选择', 10 | title: 'On', 11 | } 12 | 13 | it('passes picker a11y test', async () => { 14 | const { container, getByText } = render( 15 |
16 |
17 | 18 | 19 |
20 | ); 21 | fireEvent.click(getByText('On')); 22 | // await testA11y(container); 23 | }); 24 | 25 | test('renders Basic', async () => { 26 | const onFinish = jest.fn(); 27 | const onFinishFailed = jest.fn(); 28 | const { getByText, getAllByText } = render( 29 | 30 | ); 31 | expect(getByText('Off')).toBeDefined(); 32 | fireEvent.click(getByText("Submit")); 33 | await waitFor(() => { 34 | expect(onFinish).toBeCalled(); 35 | }); 36 | expect(getByText("Off").parentNode?.firstChild).toHaveClass('alitajs-dform-redStar'); 37 | //判断是否是必选 38 | const OnClick: any = getByText("On").parentNode?.parentNode?.lastChild?.firstChild; 39 | expect(OnClick.firstChild.getAttribute('value')).toBe('on'); 40 | fireEvent.click(OnClick); 41 | await waitFor(() => { 42 | expect(OnClick.firstChild.getAttribute('value')).toBe("off"); 43 | }); 44 | //判断是否可点击 45 | const disabledOn: any = getByText('Disabled On').parentNode?.parentNode?.lastChild?.firstChild; 46 | fireEvent.click(disabledOn) 47 | await waitFor(() => { 48 | expect(disabledOn.firstChild.getAttribute('value')).toBe("on"); 49 | }) 50 | }) -------------------------------------------------------------------------------- /src/components/NomarSelect/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 下拉单选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DformSelect from '../../'; 8 | interface pageProps { 9 | sbumit?: () => void; 10 | } 11 | 12 | const Page: FC = (props) => { 13 | const { sbumit } = props; 14 | const [inputValue, setInputValue] = useState(); 15 | 16 | const data = [ 17 | [ 18 | { 19 | label: '2013', 20 | value: '2013', 21 | }, 22 | { 23 | label: '2014', 24 | value: '2014', 25 | }, 26 | { 27 | label: '2015', 28 | value: '2015', 29 | }, 30 | { 31 | label: '2016', 32 | value: '2016', 33 | }, 34 | ], 35 | [ 36 | { 37 | label: '春', 38 | value: '春', 39 | }, 40 | { 41 | label: '夏', 42 | value: '夏', 43 | }, 44 | { 45 | label: '秋', 46 | value: '秋', 47 | }, 48 | { 49 | label: '冬', 50 | value: '冬', 51 | }, 52 | ], 53 | ]; 54 | return ( 55 | <> 56 | setInputValue(e)} 64 | defaultValue={inputValue} 65 | /> 66 | 67 | 76 | 77 | ); 78 | }; 79 | 80 | export default Page; 81 | -------------------------------------------------------------------------------- /src/components/NomarSelect/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 下拉单选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { DformSelect } from '@alitajs/dform'; 8 | interface pageProps { 9 | sbumit?: () => void; 10 | } 11 | 12 | const Page: FC = (props) => { 13 | const { sbumit } = props; 14 | const [inputValue, setInputValue] = useState(); 15 | 16 | const data = [ 17 | [ 18 | { 19 | label: '2013', 20 | value: '2013', 21 | }, 22 | { 23 | label: '2014', 24 | value: '2014', 25 | }, 26 | { 27 | label: '2015', 28 | value: '2015', 29 | }, 30 | { 31 | label: '2016', 32 | value: '2016', 33 | }, 34 | ], 35 | [ 36 | { 37 | label: '春', 38 | value: '春', 39 | }, 40 | { 41 | label: '夏', 42 | value: '夏', 43 | }, 44 | { 45 | label: '秋', 46 | value: '秋', 47 | }, 48 | { 49 | label: '冬', 50 | value: '冬', 51 | }, 52 | ], 53 | ]; 54 | return ( 55 | <> 56 | setInputValue(e)} 64 | defaultValue={inputValue} 65 | /> 66 | 67 | 76 | 77 | ); 78 | }; 79 | 80 | export default Page; 81 | -------------------------------------------------------------------------------- /src/components/NomarFile/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /** 3 | * title: 基础 文件上传 4 | * desc: 表单使用 demo 5 | */ 6 | import React, { FC, useState } from 'react'; 7 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 8 | import getRandom from '../../'; 9 | import DformFile from '../../'; 10 | 11 | const contractList = [{ title: '个人简历模板2020.pdf', fileId: '1' }]; 12 | interface pageProps { 13 | onSubmit?: () => void; 14 | } 15 | 16 | const Page: FC = (props) => { 17 | const { onSubmit } = props; 18 | const [fileValue, setFileValue] = useState(contractList); 19 | return ( 20 | <> 21 | { 26 | console.log(res); 27 | }} 28 | defaultValue={fileValue} 29 | maxLength={3} 30 | onChange={(res: any, delItem: any) => { 31 | console.log(res, delItem); 32 | setFileValue(res); 33 | }} 34 | alias={{ 35 | id: 'fileId', 36 | title: 'title', 37 | }} 38 | upload={(res: any) => { 39 | const list = [...fileValue]; 40 | if (res && res.length) { 41 | res.map((item: any) => { 42 | list.push({ 43 | title: item.name, 44 | fileId: getRandom(), 45 | }); 46 | }); 47 | } 48 | setFileValue(list); 49 | }} 50 | /> 51 | 52 | 61 | 62 | ); 63 | }; 64 | 65 | export default Page; 66 | -------------------------------------------------------------------------------- /src/components/MultiplePicker/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 单独使用 多选框 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import MultiplePicker from '../../'; 8 | 9 | const foodList = [ 10 | { 11 | foodName: '宫保鸡丁', 12 | foodId: '宫保鸡丁', 13 | }, 14 | { 15 | foodName: '可乐鸡翅', 16 | foodId: '可乐鸡翅', 17 | }, 18 | { 19 | foodName: '爆炒虾仁', 20 | foodId: '爆炒虾仁', 21 | }, 22 | { 23 | foodName: '清蒸小黄鱼', 24 | foodId: '清蒸小黄鱼', 25 | }, 26 | { 27 | foodName: '红烧肉', 28 | foodId: '红烧肉', 29 | }, 30 | ]; 31 | 32 | interface pageProps { 33 | onSbumit?: () => void; 34 | } 35 | const Page: FC = (props) => { 36 | const { onSbumit } = props; 37 | const [mulValue, setMulValue] = useState<(string | number)[]>(['宫保鸡丁']); 38 | return ( 39 | <> 40 | setMulValue(e)} 64 | /> 65 | 66 | 75 | 76 | ); 77 | }; 78 | 79 | export default Page; 80 | -------------------------------------------------------------------------------- /src/demo/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | group: 4 | title: API 5 | path: /api 6 | order: 1 7 | nav: 8 | title: API 9 | path: /api 10 | order: 2 11 | --- 12 | 13 | # API 14 | 15 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 16 | | -------------- | ---------------------------------------------------- | ----------------------------------------------------- | ------ | -------- | 17 | | data | `json` 格式的数据源 | `DFormData` | 无 | 否 | 18 | | form | 表单对象 | FormInstance, 可通过 `const [form] = useForm();` 创建 | 无 | 是 | 19 | | formsValues | 表单值 | `Store` | {} | 否 | 20 | | onFinish | 表单提交事件 | `(values: Store) => void` | 无 | 否 | 21 | | onFinishFailed | 表单提交失败事件 | `(errorInfo: ValidateErrorEntity) => void;` | 无 | 否 | 22 | | autoLineFeed | 当 `title` 过长自动增加 `positionType` 为 `vertical` | `boolean` | false | 否 | 23 | | allDisabled | 全部不可交互,展示状态 | `boolean` | false | 否 | 24 | | onValuesChange | 字段改变时抛出事件 | `(values: any) => void;` | 无 | 否 | 25 | | isDev | 手动声明开发模式 | `boolean` | false | 否 | 26 | | failScroll | 表单提交失败,滚动到错误的字段位置 | `boolean` | true | 否 | 27 | | errorFlag | 表单提交失败,是否显示错误的提示 | `boolean` | true | 否 | 28 | -------------------------------------------------------------------------------- /src/components/AddressPicker/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .alitajs-dform-address { 4 | .am-modal-header { 5 | padding: 0 * @hd !important; 6 | } 7 | 8 | .alitajs-dform-address-content { 9 | height: 100%; 10 | max-height: 800 * @hd; 11 | min-height: 300 * @hd; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .alitajs-dform-address-value { 16 | display: flex; 17 | // align-items: center; 18 | justify-content: flex-start; 19 | padding: 15 * @hd 15 * @hd 1 * @hd 15 * @hd; 20 | overflow-x: auto; 21 | 22 | .alitajs-dform-address-label { 23 | padding-right: 12 * @hd; 24 | 25 | .alitajs-dform-address-underline { 26 | background-color: @alita-dform-radio-color; 27 | height: 3 * @hd; 28 | width: 100%; 29 | border-radius: 10 * @hd; 30 | } 31 | 32 | .alitajs-dform-address-value-item-label { 33 | text-align: left; 34 | color: @alita-dform-title-color; 35 | font-size: @alita-dform-value-font-size; 36 | padding-bottom: 8 * @hd; 37 | overflow: hidden; 38 | white-space: nowrap; 39 | text-overflow: ellipsis; 40 | } 41 | 42 | .alitajs-dform-address-value-select { 43 | color: @alita-dform-radio-color; 44 | } 45 | } 46 | } 47 | 48 | .alitajs-dform-address-list { 49 | flex: 1; 50 | overflow: auto; 51 | padding-bottom: 100px; 52 | 53 | .alitajs-dform-address-list-item { 54 | display: flex; 55 | flex-direction: row; 56 | align-items: center; 57 | justify-content: space-between; 58 | 59 | .alitajs-dform-address-list-item-common { 60 | overflow: hidden; 61 | white-space: nowrap; 62 | text-overflow: ellipsis; 63 | } 64 | .alitajs-dform-address-list-item-tick { 65 | color: @alita-dform-radio-color; 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/NomarRadio/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | .@{prefixCls}-radio { 4 | display: flex; 5 | justify-content: space-between; 6 | 7 | &-field { 8 | flex: 1; 9 | overflow: auto; 10 | } 11 | 12 | &-group { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | justify-content: flex-start; 17 | } 18 | 19 | &-wrapper { 20 | display: flex; 21 | flex-direction: row; 22 | // align-items: center; 23 | justify-content: center; 24 | padding-right: 16 * @hd; 25 | padding-left: 6 * @hd; 26 | } 27 | &-wrapper-last { 28 | padding-right: 0 * @hd; 29 | } 30 | 31 | &-button { 32 | width: 15 * @hd; 33 | height: 15 * @hd; 34 | border: 1 * @hd solid #d9d9d9; 35 | border-radius: 50%; 36 | padding: 3 * @hd; 37 | flex-shrink: 0; 38 | margin-top: 3 * @hd; 39 | } 40 | 41 | &-inner-button { 42 | width: 100%; 43 | height: 100%; 44 | border-radius: 50%; 45 | background-color: @alita-dform-radio-color; 46 | } 47 | 48 | &-checked { 49 | border: 1 * @hd solid @alita-dform-radio-color; 50 | box-shadow: 0 0 1 * @hd; 51 | } 52 | 53 | &-label { 54 | font-size: @alita-dform-value-font-size; 55 | color: @alita-dform-value-color; 56 | padding-left: 4 * @hd; 57 | padding-right: 4 * @hd; 58 | // padding-top: 1 * @hd; 59 | display: flex; 60 | align-items: center; 61 | word-break: break-all; 62 | // white-space: nowrap; 63 | } 64 | 65 | &-wrapper-item-vertical { 66 | padding-bottom: 8 * @hd; 67 | padding-top: 8 * @hd; 68 | } 69 | 70 | &-wrapper-item-vertical:first-child { 71 | padding-bottom: 8 * @hd; 72 | padding-top: 0; 73 | } 74 | 75 | &-position { 76 | justify-content: flex-end; 77 | } 78 | 79 | &-item-vertical { 80 | flex-direction: column; 81 | align-items: flex-start; 82 | } 83 | } 84 | 85 | .alitajs-dform-vertical-radio { 86 | .alitajs-dform-radio(); 87 | flex-direction: column; 88 | align-items: flex-start; 89 | } 90 | -------------------------------------------------------------------------------- /docs/setting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 配置项 3 | group: 4 | title: 配置项 5 | nav: 6 | title: 配置项 7 | path: /setting 8 | order: 2 9 | --- 10 | 11 | # 可配置项 12 | 13 | ## 一、使用说明 14 | 15 | 安装 `dform` 会自动安装上 `rc-field-form`,若用户自行安装 `rc-field-form`,**可能会导致 `rc-field-form` 版本不一致而报错。** 16 | 17 | **demo 中所有需要从 `rc-field-form` 导出使用的字段都可以在 `dform` 中导出使用。** 18 | 19 | ## 二、自定义属性 20 | 21 | 下方提供几个自定义属性,用户可以在 `config/config.ts` 文件下进行编辑: 22 | 23 | ```js 24 | theme: { 25 | '@alita-dform-title-font-size': '28', 26 | '@alita-dform-title-color': 'blue', 27 | ... 28 | } 29 | ``` 30 | 31 | | 参数 | 说明 | 默认值 | 32 | | ------------------------------- | ---------------------------------------------------------------- | ----------- | 33 | | `@alita-dform-title-font-size` | 标题大小 | `0.3rem` | 34 | | `@alita-dform-title-color` | 标题颜色 | `#000` | 35 | | `@alita-dform-value-font-size` | 选中项和输入框的值大小 | `0.3rem` | 36 | | `@alita-dform-value-color` | 选中项和输入框的值颜色 | `#000` | 37 | | `@alita-dform-placeholder` | `placeholder` 的颜色 | `#888` | 38 | | `@color-text-disabled` | 不可编辑的文字颜色 | `#000` | 39 | | `@alita-dform-radio-color` | CoverRadio, Radio, CheckBox, MultiplePicker 选中时的颜色 | `#1677ff` | 40 | | `@alita-dform-radio-font-color` | CoverRadio, Radio, CheckBox, MultiplePicker 选中时的文字颜色颜色 | `#fff` | 41 | | `@alita-dform-padding` | 表单项间距 | `0.24rem 0` | 42 | 43 | ## 三、时间类型赋值 44 | 45 | **日期字符串在不同浏览器有不同的实现,例如 new Date('2017-1-1') 在 Safari 上是 Invalid Date,而在 Chrome 上是能正常解析的。** 46 | 47 | **在设值时,如果是日期字符串请先用 `dateChange(val)` 进行转化下,`dateChange` 可以在 `@alitajs/dform` 中导出。** 48 | 49 | 例子请参考 `NomarDatePicker` 组件。 50 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../baseComponents/DynamicForm'; 4 | import { useForm } from 'rc-field-form'; 5 | import DformCheckBox from '../../'; 6 | 7 | interface BasicProps { 8 | onFinish: any; 9 | onFinishFailed: any; 10 | } 11 | const fruitsList = [ 12 | { foodId: 'apple', foodName: '苹果' }, 13 | { foodId: 'banana', foodName: '香蕉' }, 14 | { foodId: 'orange', foodName: '橙子' }, 15 | { foodId: 'watermelon', foodName: '西瓜' }, 16 | { foodId: 'hami', foodName: '哈密瓜' }, 17 | { foodId: 'pineapple', foodName: '菠萝' }, 18 | { foodId: 'pear', foodName: '香梨' }, 19 | ]; 20 | 21 | const Page: FC = ({ onFinish, onFinishFailed }) => { 22 | const [form] = useForm(); 23 | // const formsData = [ 24 | // { 25 | // type: 'checkbox', 26 | // title: '喜欢的水果', 27 | // required: true, 28 | // data: fruitsList, 29 | // fieldProps: 'fruit', 30 | // chunk: 2, 31 | // alias: { 32 | // label: 'foodName', 33 | // value: 'foodId', 34 | // }, 35 | // }, 36 | // ] as IFormItemProps[]; 37 | 38 | const formsValues = { 39 | fruit: ['orange'], 40 | }; 41 | 42 | const formProps = { 43 | form, 44 | onFinish, 45 | onFinishFailed, 46 | formsValues, 47 | isDev: true, 48 | }; 49 | return ( 50 | <> 51 | 52 | 63 | 64 | 65 | 73 | 74 | ); 75 | }; 76 | 77 | export default Page; 78 | -------------------------------------------------------------------------------- /src/tests/relativesUsage/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor, sleep } from '@alita/test'; 3 | import BasicTest from './basic'; 4 | import dayjs from 'dayjs'; 5 | 6 | test('render Basic', async () => { 7 | const onFinish = jest.fn(); 8 | const onFinishFailed = jest.fn(); 9 | const onChange = jest.fn(); 10 | const { getByText, getAllByText, getByLabelText } = render( 11 | , 16 | ); 17 | expect(getByLabelText('username')).toHaveValue('小红'); 18 | // 判断是否选择 19 | fireEvent.click(getAllByText('是')[0]); 20 | await waitFor(() => { 21 | expect(getAllByText('是')[0].parentNode?.firstChild).toHaveClass( 22 | 'alitajs-dform-radio-checked', 23 | ); 24 | }); 25 | expect(getByText('J用户名')).toBeDefined(); 26 | // 判断是否选择 27 | fireEvent.click(getAllByText('否')[0]); 28 | await waitFor(() => { 29 | expect(getAllByText('否')[0].parentNode?.firstChild).toHaveClass( 30 | 'alitajs-dform-radio-checked', 31 | ); 32 | }); 33 | // 判断是否选择 34 | fireEvent.click(getAllByText('女')[0]); 35 | await waitFor(() => { 36 | expect(getAllByText('女')[0].parentNode?.firstChild).toHaveClass( 37 | 'alitajs-dform-radio-checked', 38 | ); 39 | }); 40 | await waitFor(() => { 41 | fireEvent.click(getByText('submit')); 42 | }); 43 | expect(onFinishFailed).toBeCalled(); 44 | await waitFor(() => { 45 | expect(getByText('出生年月').parentNode?.firstChild).toHaveClass( 46 | 'alitajs-dform-redStar', 47 | ); 48 | expect(getByText('美食').parentNode?.firstChild).toHaveClass( 49 | 'alitajs-dform-redStar', 50 | ); 51 | }); 52 | fireEvent.click(getByText('请选择出生年月日')); 53 | fireEvent.click(getByText('取消')); 54 | fireEvent.click(getByText('请选择出生年月日')); 55 | fireEvent.click(getByText('确认')); 56 | expect(onChange).toBeCalled(); 57 | expect(getByText(dayjs(new Date()).format('YYYY-MM-DD'))); 58 | await waitFor(() => { 59 | fireEvent.click(getByText('submit')); 60 | }); 61 | expect(onFinish).toBeCalled(); 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/NomarFile/fileGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import { INomarFileProps, INomarFileItemProps } from './interface'; 3 | // @ts-ignore 4 | import ClosePng from '../../assets/close.png'; 5 | import './index.less'; 6 | 7 | const prefixCls = 'alitajs-dform-file'; 8 | 9 | interface IFileGroupProps extends INomarFileProps { 10 | value?: INomarFileItemProps[]; 11 | valueChange: (res: any[]) => void; 12 | valueStyle?: React.CSSProperties; 13 | } 14 | 15 | const FileGroup: FC = (props) => { 16 | const { 17 | value = [], 18 | onChange, 19 | onClick, 20 | alias = { id: 'id', title: 'title' }, 21 | disabled = false, 22 | valueChange, 23 | valueStyle, 24 | itemExtra 25 | } = props; 26 | 27 | useEffect(() => { 28 | valueChange(value); 29 | }, [JSON.stringify(value || [])]); 30 | 31 | const del = (index: number) => { 32 | const newData = Array.from(value); 33 | newData.splice(index, 1); 34 | if (onChange) onChange(newData, value[index], 'delete'); 35 | }; 36 | 37 | const itemClick = (item: INomarFileItemProps) => { 38 | if (onClick) onClick(item); 39 | }; 40 | 41 | return ( 42 |
43 | {value.map((item: INomarFileItemProps, index: number) => ( 44 |
45 | { 49 | itemClick(item); 50 | }} 51 | > 52 | {item[alias.title || 'title']} 53 | 54 | {!disabled && ( 55 | { 57 | del(index); 58 | }} 59 | src={ClosePng} 60 | alt="" 61 | className="alitajs-dform-close" 62 | /> 63 | )} 64 | {typeof itemExtra === "function" &&
{itemExtra(item, index)}
} 65 |
66 | ))} 67 |
68 | ); 69 | }; 70 | 71 | export default FileGroup; 72 | -------------------------------------------------------------------------------- /src/components/NomarFile/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm, { getRandom } from '../../../../index'; 4 | import { useForm } from 'rc-field-form'; 5 | import NomarFile from '../../'; 6 | 7 | interface BasicProps { 8 | onFinish: any; 9 | onFinishFailed: any; 10 | onMyClick: any; 11 | } 12 | let forms: any; 13 | 14 | const NomarFileTestPage: React.FC = ({ 15 | onFinish, 16 | onFinishFailed, 17 | onMyClick, 18 | }) => { 19 | const [form] = useForm(); 20 | forms = form; 21 | const contractList = [ 22 | { title: '房子买卖协议.pdf', fileId: '1' }, 23 | { title: '房屋租赁合同说明书.pdf', fileId: '2' }, 24 | ]; 25 | const formProps = { 26 | form, 27 | onFinish, 28 | onFinishFailed, 29 | formsValues: { 30 | contract: contractList, 31 | }, 32 | isDev: false, 33 | }; 34 | return ( 35 | <> 36 | 37 | { 44 | onMyClick(); 45 | }} 46 | onChange={(res: any, delItem: any) => {}} 47 | alias={{ 48 | id: 'fileId', 49 | title: 'title', 50 | }} 51 | maxLength={2} 52 | upload={(res: any) => { 53 | const list = form.getFieldsValue().contract || []; 54 | if (res && res.length) { 55 | res.map((item: any) => { 56 | list.push({ 57 | title: item.name, 58 | fileId: getRandom(), 59 | }); 60 | }); 61 | } 62 | form.setFieldsValue({ 63 | contract: list, 64 | }); 65 | }} 66 | /> 67 | 68 | 69 | 77 | 78 | ); 79 | }; 80 | export default { NomarFileTestPage, forms }; 81 | -------------------------------------------------------------------------------- /src/components/CoverRadio/tests/demos/couplet.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../index'; 4 | import { useForm } from 'rc-field-form'; 5 | import CoverRadio from '../../'; 6 | 7 | const drinkList = [ 8 | { foodId: 'cola', foodName: '可乐' }, 9 | { foodId: 'milk', foodName: '牛奶' }, 10 | { foodId: 'fruitJuice', foodName: '果汁' }, 11 | ]; 12 | const selectList = [ 13 | { label: 'onlyCola', value: '要可乐' }, 14 | { label: 'onlyMilk', value: '要牛奶' }, 15 | { label: 'onlyFruitJuice', value: '要果汁' }, 16 | ]; 17 | 18 | const Couplet: FC = () => { 19 | const [form] = useForm(); 20 | const [formsValues, setFormsValues] = React.useState({ drink: 'milk' }); 21 | const formProps = { 22 | formsValues, 23 | form, 24 | isDev: false, 25 | }; 26 | function onChange(e: any) { 27 | if (e === 'onlyMilk') { 28 | setFormsValues({ drink: 'milk' }); 29 | } else if (e === 'onlyCola') { 30 | setFormsValues({ drink: 'cola' }); 31 | } else if (e === 'onlyFruitJuice') { 32 | setFormsValues({ drink: 'fruitJuice' }); 33 | } 34 | } 35 | return ( 36 | <> 37 | 38 | { 44 | // eslint-disable-next-line no-console 45 | setFormsValues({ drink: val }); 46 | }} 47 | alias={{ 48 | label: 'foodName', 49 | value: 'foodId', 50 | }} 51 | /> 52 | { 63 | onChange(e); 64 | }} 65 | /> 66 | 67 | 68 | 71 | 72 | ); 73 | }; 74 | export default Couplet; 75 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Switch 3 | group: 4 | title: Switch 5 | nav: 6 | title: 组件 7 | path: /components 8 | --- 9 | 10 | # Switch 11 | 12 | ## 代码展示 13 | 14 | 15 | 16 | ## API 17 | 18 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 19 | | ------------ | ------------------------------------------------ | ----------------------------- | ------ | -------- | 20 | | type | 表单类型 | string | '' | 是 | 21 | | coverStyle | 自定义选择框样式 | object | {} | 否 | 22 | | title | 标题 | string | '' | 是 | 23 | | fieldProps | 文本属性 | string | - | 是 | 24 | | placeholder | placeholder | string | '' | 否 | 25 | | required | 必填判断 | boolean | false | 否 | 26 | | disabled | 是否可编辑 | boolean | false | 否 | 27 | | hasStar | 必填项红\*展示与否的判断 | boolean | true | 否 | 28 | | hasStar | 必填项红\*展示与否的判断 | boolean | true | 否 | 29 | | rules | 规则校验(如需用到该字段,请重写 `required` 校验) | array | [] | 否 | 30 | | hidden | 字段展示与否的判断 | boolean | false | 否 | 31 | | renderHeader | 组件头部 | `number` or `string` | - | 否 | 32 | | className | 类名 | string | - | 否 | 33 | | defaultValue | 设置初始取值 | boolean | false | 否 | 34 | | renderHeader | 组件头部 | `string` or `React.ReactNode` | '' | 否 | 35 | | renderFooter | 组件尾部 | `string` or `React.ReactNode` | '' | 否 | 36 | -------------------------------------------------------------------------------- /src/components/MultiplePicker/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多选框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | MultiplePicker, 12 | } from '../../../../index'; 13 | interface BasicProps { 14 | onFinish: any; 15 | onFinishFailed: any; 16 | onChange: any; 17 | } 18 | 19 | const Page: FC = ({ onFinish, onFinishFailed, onChange }) => { 20 | const [form] = useForm(); 21 | 22 | const foodList = [ 23 | { 24 | foodName: '宫保鸡丁', 25 | foodId: '宫保鸡丁', 26 | }, 27 | { 28 | foodName: '可乐鸡翅', 29 | foodId: '可乐鸡翅', 30 | }, 31 | { 32 | foodName: '爆炒虾仁', 33 | foodId: '爆炒虾仁', 34 | }, 35 | { 36 | foodName: '清蒸小黄鱼', 37 | foodId: '清蒸小黄鱼', 38 | }, 39 | { 40 | foodName: '红烧肉', 41 | foodId: '红烧肉', 42 | }, 43 | ]; 44 | const formsValues = { 45 | youFood: ['红烧肉', '清蒸小黄鱼'], 46 | }; 47 | const formProps = { 48 | onFinish, 49 | onFinishFailed, 50 | formsValues, 51 | form, 52 | autoLineFeed: false, 53 | isDev: false, 54 | }; 55 | return ( 56 | <> 57 | 58 | 72 | 87 | 88 | 89 | 92 | 93 | ); 94 | }; 95 | 96 | export default Page; 97 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/imagePickerGroup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useEffect } from 'react'; 2 | import { ImagePicker, Toast } from 'antd-mobile-v2'; 3 | import { ImageFile, ImagePickerGroupProps } from './interface'; 4 | import { transformFile } from '../../utils'; 5 | 6 | const ImagePickerGroup: FC = (props) => { 7 | const { 8 | onChange, 9 | limitSize, 10 | compressRatio, 11 | value = [], 12 | maxLength, 13 | disabled = false, 14 | ...otherProps 15 | } = props; 16 | 17 | const [selectable, setSelectable] = useState(true); 18 | 19 | useEffect(() => { 20 | if (disabled) { 21 | setSelectable(false); 22 | return; 23 | } 24 | if (maxLength && value && value.length && value.length >= maxLength) { 25 | setSelectable(false); 26 | } else { 27 | setSelectable(true); 28 | } 29 | }, [disabled, JSON.stringify(value || [])]); 30 | 31 | const checkFileLimit = (file: ImageFile) => { 32 | if (limitSize && file && file.size && file.size > limitSize) { 33 | Toast.fail('图片过大', 1); 34 | return false; 35 | } 36 | return true; 37 | }; 38 | 39 | const imageChange = ( 40 | files: ImageFile[] | any, 41 | operationType: string, 42 | index: number | undefined, 43 | ) => { 44 | if (files && files.length > value.length) { 45 | const lastFile = files[files.length - 1]; 46 | const { file = {} } = lastFile; 47 | if (compressRatio && lastFile.url.indexOf('base64,') !== -1) { 48 | transformFile(lastFile.file, compressRatio).then((newFile: any) => { 49 | const reader = new FileReader(); 50 | reader.readAsDataURL(newFile); 51 | reader.onload = function ({ target }) { 52 | if (!checkFileLimit(newFile)) return; 53 | files[files.length - 1] = { 54 | ...files[files.length - 1], 55 | file: newFile, 56 | url: target?.result || '', 57 | }; 58 | }; 59 | }); 60 | } else if (!checkFileLimit(file)) return; 61 | } 62 | onChange(files, operationType, index); 63 | }; 64 | return ( 65 | 71 | ); 72 | }; 73 | 74 | export default ImagePickerGroup; 75 | -------------------------------------------------------------------------------- /src/demo/jsonUsage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useEffect } from 'react'; 2 | import { WhiteSpace, Button } from 'antd-mobile-v2'; 3 | import DynamicForm, { 4 | useForm, 5 | Store, 6 | ValidateErrorEntity, 7 | IFormItemProps, 8 | } from '@alitajs/dform'; 9 | 10 | const sexData = [ 11 | { label: '男', value: 'man' }, 12 | { label: '女', value: 'woman' }, 13 | ]; 14 | const weatherData = [ 15 | { label: '晴', value: '晴' }, 16 | { label: '阴', value: '阴' }, 17 | { label: '雨', value: '雨' }, 18 | ]; 19 | 20 | const motionData = [ 21 | { label: '篮球', value: '篮球' }, 22 | { label: '羽毛球', value: '羽毛球' }, 23 | { label: '乒乓球', value: '乒乓球' }, 24 | ]; 25 | 26 | const data = [ 27 | { 28 | type: 'input', 29 | fieldProps: 'username', 30 | required: true, 31 | placeholder: '请输入', 32 | title: '用户名', 33 | defaultValue: '小红', 34 | }, 35 | { 36 | type: 'radio', 37 | fieldProps: 'sex', 38 | title: '性别', 39 | data: sexData, 40 | }, 41 | { 42 | type: 'date', 43 | fieldProps: 'date', 44 | placeholder: '请选择', 45 | title: '出生年月', 46 | }, 47 | { 48 | type: 'picker', 49 | fieldProps: 'weather', 50 | placeholder: '请选择', 51 | title: '天气', 52 | data: weatherData, 53 | }, 54 | { 55 | type: 'multiplePicker', 56 | fieldProps: 'motion', 57 | placeholder: '请选择', 58 | title: '特长', 59 | data: motionData, 60 | }, 61 | ] as IFormItemProps[]; 62 | 63 | const Page: FC = () => { 64 | const [form] = useForm(); 65 | const [formsValues, setFormsValues] = useState({}); 66 | 67 | useEffect(() => { 68 | setFormsValues({ 69 | sex: 'man', 70 | motion: ['羽毛球', '乒乓球'], 71 | }); 72 | }, []); 73 | 74 | const onFinish = (values: Store) => { 75 | console.log(values); 76 | }; 77 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 78 | console.log('Failed:', errorInfo); 79 | }; 80 | 81 | const formProps = { 82 | form, 83 | data, 84 | onFinish, 85 | onFinishFailed, 86 | formsValues, 87 | }; 88 | 89 | return ( 90 |
91 | 92 | 93 | 96 |
97 | ); 98 | }; 99 | 100 | export default Page; 101 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 时间区间选择框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | dateChange, 12 | RangeDatePicker, 13 | } from '@alitajs/dform'; 14 | 15 | const page: FC = () => { 16 | const [form] = useForm(); 17 | const onFinish = (values: Store) => { 18 | // eslint-disable-next-line no-console 19 | console.log('Success:', values); 20 | }; 21 | 22 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 23 | // eslint-disable-next-line no-console 24 | console.log('Failed:', errorInfo); 25 | }; 26 | 27 | const formsValues = { 28 | rangeTime1: dateChange(new Date('2021-07-08')), 29 | rangeTime2: dateChange(new Date('2021-08-08')), 30 | }; 31 | 32 | const formProps = { 33 | onFinish, 34 | onFinishFailed, 35 | formsValues, 36 | form, 37 | isDev: false, 38 | }; 39 | 40 | return ( 41 | <> 42 | 43 | { 51 | // eslint-disable-next-line no-console 52 | console.log(val); 53 | }, 54 | }} 55 | /> 56 | 64 | 77 | 78 | 79 | 82 | 83 | ); 84 | }; 85 | 86 | export default page; 87 | -------------------------------------------------------------------------------- /src/components/NomarDatePicker/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 时间选择框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | dateChange, 10 | DformDatePicker, 11 | } from '../../../../index'; 12 | 13 | interface BasicProps { 14 | onFinish: any; 15 | onFinishFailed: any; 16 | onChange: any; 17 | } 18 | 19 | const Page: FC = ({ onFinish, onFinishFailed, onChange }) => { 20 | const [form] = useForm(); 21 | const formsValues = { 22 | DateTime: dateChange('2020-02-02 22:22'), 23 | rangeTime1: new Date(), 24 | }; 25 | 26 | const formProps = { 27 | form, 28 | onFinish, 29 | onFinishFailed, 30 | formsValues, 31 | isDev: false, 32 | }; 33 | 34 | return ( 35 | <> 36 | 37 | 47 | 55 | 65 | {}} 71 | /> 72 | 79 | 80 | 81 | 89 | 90 | ); 91 | }; 92 | 93 | export default Page; 94 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm, { getRandom } from '../../../../index'; 4 | import { useForm } from 'rc-field-form'; 5 | import NomarTextArea from '../../'; 6 | import PhotoIcon from '../../../../assets/photo.png'; 7 | 8 | interface BasicProps { 9 | onFinish: any; 10 | onFinishFailed: any; 11 | onBlur: any; 12 | } 13 | 14 | const NomarTextAreaTestPage: React.FC = ({ 15 | onFinish, 16 | onFinishFailed, 17 | onBlur, 18 | }) => { 19 | const [form] = useForm(); 20 | const formsValues = { 21 | textArea2: '只读,不可编辑', 22 | }; 23 | const formProps = { 24 | onFinish, 25 | onFinishFailed, 26 | formsValues, 27 | form, 28 | isDev: false, 29 | }; 30 | const photoImg = () => ( 31 | 32 | ); 33 | 34 | return ( 35 | <> 36 | 37 | 44 | 52 | 67 | 75 | 76 | 77 | 85 | 86 | ); 87 | }; 88 | 89 | export default NomarTextAreaTestPage; 90 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多选框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | DformCheckBox, 12 | } from '@alitajs/dform'; 13 | 14 | const fruitsList = [ 15 | { foodId: 'apple', foodName: '苹果', desc: '这是苹果' }, 16 | { foodId: 'banana', foodName: '香蕉', desc: '这是香蕉' }, 17 | { foodId: 'orange', foodName: '橙子', desc: '这是橙子' }, 18 | { foodId: 'watermelon', foodName: '西瓜', desc: '这是西瓜' }, 19 | { foodId: 'hami', foodName: '哈密瓜', desc: '这是哈密瓜' }, 20 | { foodId: 'pineapple', foodName: '菠萝', desc: '这是菠萝' }, 21 | { foodId: 'pear', foodName: '香梨', desc: '这是香梨' }, 22 | ]; 23 | 24 | const Page: FC = () => { 25 | const [form] = useForm(); 26 | const onFinish = (values: Store) => { 27 | // eslint-disable-next-line no-console 28 | console.log('Success:', values); 29 | }; 30 | 31 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 32 | // eslint-disable-next-line no-console 33 | console.log('Failed:', errorInfo); 34 | }; 35 | const formsValues = { 36 | fruit: ['watermelon', 'orange'], 37 | }; 38 | 39 | const formProps = { 40 | form, 41 | onFinish, 42 | onFinishFailed, 43 | formsValues, 44 | isDev: true, 45 | }; 46 | return ( 47 | <> 48 | 49 | { 60 | if (item.value === 'watermelon') return true; 61 | return false; 62 | }} 63 | /> 64 | 75 | 76 | 77 | 85 | 86 | ); 87 | }; 88 | 89 | export default Page; 90 | -------------------------------------------------------------------------------- /src/utils/menu.ts: -------------------------------------------------------------------------------- 1 | export const DFORM_COMP_NAME = [ 2 | 'addressPicker', 3 | 'coverRadio', 4 | 'extraInput', 5 | 'multiplePicker', 6 | 'dformInput', 7 | 'dformSelect', 8 | 'dformPicker', 9 | 'dformSwitch', 10 | 'dformTextArea', 11 | 'dformDatePicker', 12 | 'dformRadio', 13 | 'dformCheckBox', 14 | 'dformImagePicker', 15 | 'dformCustom', 16 | 'dformText', 17 | 'dformFile', 18 | 'rangeDatePicker', 19 | ]; 20 | 21 | export const NO_SUPPORT_VERTICAL = ['switch', 'NomarSwitch']; 22 | 23 | export const DFORM_COMP_DETAULT = { 24 | addressPicker: { positionType: 'horizontal' }, 25 | coverRadio: { positionType: 'horizontal' }, 26 | extraInput: { positionType: 'vertical' }, 27 | multiplePicker: { positionType: 'horizontal' }, 28 | input: { positionType: 'horizontal' }, 29 | select: { positionType: 'horizontal' }, 30 | picker: { positionType: 'horizontal' }, 31 | switch: { positionType: 'horizontal' }, 32 | area: { positionType: 'horizontal' }, 33 | date: { positionType: 'horizontal' }, 34 | radio: { positionType: 'horizontal' }, 35 | checkbox: { positionType: 'vertical' }, 36 | image: { positionType: 'vertical' }, 37 | custom: { positionType: 'horizontal' }, 38 | text: { positionType: 'horizontal' }, 39 | file: { positionType: 'vertical' }, 40 | rangeDatePicker: { positionType: 'vertical' }, 41 | dformInput: { positionType: 'horizontal' }, 42 | dformSelect: { positionType: 'horizontal' }, 43 | dformPicker: { positionType: 'horizontal' }, 44 | dformSwitch: { positionType: 'horizontal' }, 45 | dformTextArea: { positionType: 'horizontal' }, 46 | dformDatePicker: { positionType: 'horizontal' }, 47 | dformRadio: { positionType: 'horizontal' }, 48 | dformCheckBox: { positionType: 'vertical' }, 49 | dformImagePicker: { positionType: 'vertical' }, 50 | dformCustom: { positionType: 'vertical' }, 51 | dformText: { positionType: 'horizontal' }, 52 | dformFile: { positionType: 'vertical' }, 53 | } as { [key: string]: { positionType: 'vertical' | 'horizontal' } }; 54 | 55 | export const PLACEHOLDER_MENU = { 56 | addressPicker: '请选择', 57 | coverRadio: '请选择', 58 | extraInput: '请输入', 59 | multiplePicker: '请选择', 60 | input: '请输入', 61 | select: '请选择', 62 | picker: '请选择', 63 | switch: '请选择', 64 | area: '请输入', 65 | date: '请选择', 66 | radio: '请选择', 67 | checkbox: '请选择', 68 | image: '请上传', 69 | custom: '请选择', 70 | text: '暂无数据', 71 | file: '请上传', 72 | rangeDatePicker: '请选择', 73 | } as { [key: string]: '请选择' | '请上传' | '请输入' | '暂无数据' }; 74 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 开关控件 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | IFormItemProps, 9 | useForm, 10 | Store, 11 | ValidateErrorEntity, 12 | DformSwitch, 13 | } from '@alitajs/dform'; 14 | 15 | interface PageProps {} 16 | 17 | const Page: FC = () => { 18 | const [form] = useForm(); 19 | const [val, setVal] = useState(true); 20 | const onFinish = (values: Store) => { 21 | // eslint-disable-next-line no-console 22 | console.log('Success:', values); 23 | }; 24 | 25 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 26 | // eslint-disable-next-line no-console 27 | console.log(errorInfo); 28 | }; 29 | 30 | const formsData = [ 31 | { 32 | type: 'switch', 33 | fieldProps: 'off', 34 | placeholder: '选择', 35 | title: 'Off', 36 | required: true, 37 | }, 38 | { 39 | type: 'switch', 40 | fieldProps: 'on', 41 | placeholder: '选择', 42 | title: 'On', 43 | }, 44 | { 45 | type: 'switch', 46 | fieldProps: 'disabledOn', 47 | placeholder: '选择', 48 | title: 'Disabled On', 49 | required: true, 50 | disabled: true, 51 | }, 52 | ] as IFormItemProps[]; 53 | const formsValues = {}; 54 | const formsProps = { 55 | form, 56 | onFinish, 57 | onFinishFailed, 58 | formsValues, 59 | // data: formsData, 60 | isDev: true, 61 | }; 62 | 63 | return ( 64 | 65 | 66 | 67 | 73 | 81 | 82 | 83 | 86 | setVal(v)} 92 | /> 93 | 94 | ); 95 | }; 96 | 97 | export default Page; 98 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor, sleep } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import RangeDatePicker from '../'; 5 | import BasicText from './demos/basic'; 6 | import dayjs from 'dayjs'; 7 | 8 | const props = { 9 | fieldProps: 'rangeTime1', 10 | fieldProps2: 'rangeTime2', 11 | title: '时间(datetime)', 12 | firstProps: { 13 | onOk: (val: any) => { 14 | // eslint-disable-next-line no-console 15 | console.log(val); 16 | }, 17 | }, 18 | }; 19 | 20 | it('passes picker a11y test', async () => { 21 | const { container, getAllByText } = render( 22 |
23 |
24 | 25 | 26 |
, 27 | ); 28 | // fireEvent.click(getAllByText('请选择')[0]); 29 | await testA11y(container); 30 | }); 31 | 32 | test('render Basic', async () => { 33 | const onFinish = jest.fn(); 34 | const onFinishFailed = jest.fn(); 35 | const { getByText, getAllByText } = render( 36 | , 37 | ); 38 | const newTime = new Date(); 39 | let year: string = newTime.getFullYear() + ''; 40 | let month: string = newTime.getMonth() + 1 + ''; 41 | const dataTime = dayjs(newTime).format('YYYY-MM-DD HH:mm'); 42 | expect(getByText(dataTime)).toBeDefined(); 43 | fireEvent.click(getByText('Submit')); 44 | await waitFor(() => { 45 | expect(onFinishFailed).toBeCalled(); 46 | }); 47 | fireEvent.click(getAllByText('请选择')[0]); 48 | await waitFor(() => { 49 | expect(getByText('取消')).toBeDefined; 50 | }); 51 | fireEvent.click(getAllByText('请选择')[0]); 52 | await waitFor(() => { 53 | expect(getByText('时间(date)')).toBeDefined; 54 | fireEvent.click(getByText('确认')); 55 | }); 56 | await waitFor(() => { 57 | expect(getAllByText(dataTime)[0]).toHaveClass( 58 | 'alitajs-dform-text-item-text', 59 | ); 60 | }); 61 | // fireEvent.click(getAllByText('请选择')[0]); 62 | // await waitFor(() => { 63 | // expect(getByText(parseInt(month) + 1 + '月')).toBeDefined(); 64 | // fireEvent.click(getByText('确认')); 65 | // }); 66 | fireEvent.click(getAllByText('请选择')[0]); 67 | await waitFor(() => { 68 | expect(getByText(parseInt(year) + 1 + '年')).toBeDefined(); 69 | fireEvent.click(getByText('确认')); 70 | }); 71 | fireEvent.click(getByText('Submit')); 72 | await waitFor(() => { 73 | expect(onFinish).toBeCalled(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/CoverRadio/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../index'; 4 | import { useForm } from 'rc-field-form'; 5 | import CoverRadio from '../../'; 6 | 7 | const sexList = [ 8 | { sexName: '男', sexId: 'man' }, 9 | { sexName: '女', sexId: 'woman' }, 10 | ]; 11 | 12 | const foodList = [ 13 | { 14 | label: '宫保鸡丁', 15 | value: '宫保鸡丁', 16 | }, 17 | { 18 | label: '可乐鸡翅', 19 | value: '可乐鸡翅', 20 | }, 21 | { 22 | label: '爆炒虾仁', 23 | value: '爆炒虾仁', 24 | }, 25 | { 26 | label: '清蒸小黄鱼', 27 | value: '清蒸小黄鱼', 28 | }, 29 | { 30 | label: '红烧肉', 31 | value: '红烧肉', 32 | }, 33 | ]; 34 | 35 | interface BasicProps { 36 | onFinish: any; 37 | onFinishFailed: any; 38 | } 39 | 40 | const CoverRadioTestPage: React.FC = ({ 41 | onFinish, 42 | onFinishFailed, 43 | }) => { 44 | const [form] = useForm(); 45 | const formsValues = { 46 | sex2: 'woman', 47 | sex: 'man', 48 | }; 49 | 50 | const formProps = { 51 | onFinish, 52 | onFinishFailed, 53 | formsValues, 54 | form, 55 | isDev: false, 56 | }; 57 | return ( 58 | <> 59 | 60 | { 66 | // eslint-disable-next-line no-console 67 | // console.log(val); 68 | }} 69 | alias={{ 70 | label: 'sexName', 71 | value: 'sexId', 72 | }} 73 | /> 74 | 75 | 86 | 94 | 95 | 96 | 99 | 100 | ); 101 | }; 102 | export default CoverRadioTestPage; 103 | -------------------------------------------------------------------------------- /src/baseComponents/HorizontalTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode, useContext, useState, useMemo } from 'react'; 2 | import classnames from 'classnames'; 3 | import { DformContext, DformContextProps } from '../Context'; 4 | import { allPrefixCls } from '../../const'; 5 | 6 | export interface HorizontalTitleProps { 7 | /** 8 | * 是否是必填项 9 | */ 10 | required?: boolean; 11 | /** 12 | * 13 | */ 14 | hasStar?: boolean; 15 | /** 16 | * fieldProps 17 | */ 18 | fieldProps?: string; 19 | /** 20 | * 标题 21 | */ 22 | title?: string | ReactNode; 23 | /** 24 | * 标题宽度 25 | */ 26 | labelNumber?: number; 27 | /** 28 | * 是否是纵向效果 29 | */ 30 | isVertical?: boolean; 31 | /** 32 | * style 33 | */ 34 | style?: React.CSSProperties; 35 | /** 36 | * 标题 style 37 | */ 38 | titleStyle?: React.CSSProperties; 39 | } 40 | 41 | const HorizontalTitle: FC = (props) => { 42 | const { 43 | required = false, 44 | hasStar, 45 | title, 46 | labelNumber = 7, 47 | isVertical, 48 | fieldProps = '', 49 | titleStyle, 50 | } = props; 51 | const [mregedRequired, setMregedRequired] = useState(required); 52 | 53 | const { changeForm } = useContext(DformContext); 54 | 55 | const labelCls = classnames({ 56 | [`${allPrefixCls}-title-label`]: true, 57 | [`${allPrefixCls}-title-label-0`]: labelNumber === 0, 58 | [`${allPrefixCls}-title-label-2`]: labelNumber === 2, 59 | [`${allPrefixCls}-title-label-3`]: labelNumber === 3, 60 | [`${allPrefixCls}-title-label-4`]: labelNumber === 4, 61 | [`${allPrefixCls}-title-label-5`]: labelNumber === 5, 62 | [`${allPrefixCls}-title-label-6`]: labelNumber === 6, 63 | [`${allPrefixCls}-title-label-7`]: labelNumber === 7, 64 | [`${allPrefixCls}-title-label-auto`]: labelNumber > 7, 65 | }); 66 | 67 | useMemo(() => { 68 | if (changeForm[fieldProps]?.required !== undefined) { 69 | setMregedRequired(changeForm[fieldProps]?.required); 70 | } else { 71 | setMregedRequired(required); 72 | } 73 | }, [changeForm[fieldProps], required]); 74 | 75 | return ( 76 |
82 | {mregedRequired && hasStar && ( 83 |
*
84 | )} 85 |
{title}
86 |
87 | ); 88 | }; 89 | 90 | export default HorizontalTitle; 91 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../index'; 4 | import { useForm } from 'rc-field-form'; 5 | import NomarImagePicker from '../..'; 6 | 7 | interface BasicProps { 8 | onFinish: any; 9 | onFinishFailed: any; 10 | onChange: any; 11 | onImageClick: any; 12 | } 13 | 14 | const Page: React.FC = ({ 15 | onFinish, 16 | onFinishFailed, 17 | onChange, 18 | onImageClick, 19 | }) => { 20 | const fileList = [ 21 | { 22 | url: 'https://zos.alipayobjects.com/rmsportal/PZUUCKTRIHWiZSY.jpeg', 23 | id: '2121', 24 | }, 25 | { 26 | url: 'https://zos.alipayobjects.com/rmsportal/hqQWgTXdrlmVVYi.jpeg', 27 | id: '2122', 28 | }, 29 | ]; 30 | const [form] = useForm(); 31 | const formsValues = { 32 | maxLengthImg: fileList, 33 | }; 34 | const formProps = { 35 | onFinish, 36 | onFinishFailed, 37 | form, 38 | isDev: true, 39 | formsValues, 40 | }; 41 | return ( 42 | <> 43 | 44 | { 51 | onChange(); 52 | }} 53 | /> 54 | { 59 | onImageClick(); 60 | }} 61 | limitSize={3 * 1024 * 1024} 62 | defaultValue={fileList} 63 | onChange={(files: any, type: string, index: number | undefined) => { 64 | onChange(); 65 | }} 66 | /> 67 | 74 | 80 | 81 | 82 | 85 | 86 | ); 87 | }; 88 | export default Page; 89 | -------------------------------------------------------------------------------- /src/components/ExtraInput/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ExtraInput 3 | group: 4 | title: ExtraInput 5 | nav: 6 | title: 组件 7 | path: /components 8 | --- 9 | 10 | # ExtraInput 11 | 12 | ## 代码演示 13 | 14 | 15 | 16 | ## API 17 | 18 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 19 | | ------------ | ---------------------------------------------------------- | ----------------------------- | ---------- | -------- | 20 | | title | 标题 | string | '' | 是 | 21 | | fieldProps | 文本属性 | string | false | 是 | 22 | | fieldProps2 | 文本属性 | string | false | 是 | 23 | | required | 必填判断 | boolean | false | 否 | 24 | | positionType | 表单方向样式 | `horizontal` or `vertical` | `vertical` | 否 | 25 | | hasStar | 必填项红\*展示与否的判断 | boolean | true | 否 | 26 | | firstProps | `DformInput` api | {} | - | 否 | 27 | | secondProps | `DformInput` api or `DformPicker` api | {} | - | 否 | 28 | | rules | 规则校验(如需用到该字段,请重写 `required` 校验) | array | [] | 否 | 29 | | subTitle | 标题右侧的副标题,仅在 `positionType` 为 `vertical` 时生效 | string or node | '' | 否 | 30 | | hidden | 字段展示与否的判断 | boolean | false | 否 | 31 | | extraType | 表单字段类型 | `input` or `select` | `input` | 否 | 32 | | renderHeader | 组件头部 | `number` or `string` | - | 否 | 33 | | coverStyle | 自定义每个选项的样式,例如高度,内外边距等 | object | - | 否 | 34 | | renderHeader | 组件头部 | `string` or `React.ReactNode` | '' | 否 | 35 | | renderFooter | 组件尾部 | `string` or `React.ReactNode` | '' | 否 | 36 | -------------------------------------------------------------------------------- /src/components/NomarFile/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /** 3 | * title: 基础 文件上传 4 | * desc: 表单使用 demo 5 | */ 6 | import React, { FC } from 'react'; 7 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 8 | import DynamicForm, { 9 | useForm, 10 | Store, 11 | ValidateErrorEntity, 12 | getRandom, 13 | } from '@alitajs/dform'; 14 | import NomarFile from '../'; 15 | 16 | const contractList = [ 17 | { title: '合约模板2020.pdf', fileId: '1' }, 18 | { title: '电子协议模板2020.pdf', fileId: '2' }, 19 | ]; 20 | 21 | const Page: FC = () => { 22 | const [form] = useForm(); 23 | const onFinish = (values: Store) => { 24 | // eslint-disable-next-line no-console 25 | console.log('Success:', values); 26 | }; 27 | 28 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 29 | // eslint-disable-next-line no-console 30 | console.log('Failed:', errorInfo); 31 | }; 32 | 33 | const formProps = { 34 | form, 35 | onFinish, 36 | onFinishFailed, 37 | formsValues: { 38 | contract: contractList, 39 | }, 40 | isDev: false, 41 | }; 42 | 43 | return ( 44 | <> 45 | 46 | { 51 | console.log(res); 52 | }} 53 | valueStyle={{ color: '#f40' }} 54 | itemExtra={(item, index) => { 55 | console.log(item, index); 56 | return
预览
; 57 | }} 58 | maxLength={3} 59 | onChange={(res: any, delItem: any) => { 60 | console.log(res, delItem); 61 | }} 62 | alias={{ 63 | id: 'fileId', 64 | title: 'title', 65 | }} 66 | upload={(res: any) => { 67 | const list = form.getFieldsValue().contract || []; 68 | if (res && res.length) { 69 | res.map((item: any) => { 70 | list.push({ 71 | title: item.name, 72 | fileId: getRandom(), 73 | }); 74 | }); 75 | } 76 | form.setFieldsValue({ 77 | contract: list, 78 | }); 79 | }} 80 | /> 81 |
82 | 83 | 91 | 92 | ); 93 | }; 94 | 95 | export default Page; 96 | -------------------------------------------------------------------------------- /src/components/NomarPicker/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm, { IFormItemProps, useForm } from '../../../../index'; 4 | 5 | interface BasicProps { 6 | onFinish: any; 7 | onFinishFailed: any; 8 | onChange: any; 9 | } 10 | 11 | const Page: FC = ({ onFinish, onFinishFailed, onChange }) => { 12 | const [form] = useForm(); 13 | 14 | const aliasCityList = [ 15 | { 16 | cityId: '深圳', 17 | cityName: 'shenzhen', 18 | }, 19 | { 20 | cityId: '杭州', 21 | cityName: 'hangzhou', 22 | }, 23 | { 24 | cityId: '广州', 25 | cityName: 'guangzhou', 26 | }, 27 | ]; 28 | 29 | const cityList = [ 30 | { 31 | label: '北京', 32 | value: 'beijing', 33 | }, 34 | { 35 | label: '上海', 36 | value: 'shanghai', 37 | }, 38 | { 39 | label: '福州', 40 | value: 'fuzhou', 41 | }, 42 | ]; 43 | 44 | const formsData = [ 45 | { 46 | type: 'picker', 47 | fieldProps: '', 48 | fieldName: 'myCity', 49 | required: true, 50 | data: aliasCityList, 51 | title: '我喜欢的城市', 52 | labelNumber: 7, 53 | placeholder: '请选择我喜欢的城市placeholder', 54 | alias: { 55 | label: 'cityId', 56 | value: 'cityName', 57 | }, 58 | onChange: (e) => onChange(e), 59 | }, 60 | { 61 | type: 'picker', 62 | fieldProps: 'youCity', 63 | data: cityList, 64 | title: '选择你喜欢的城市', 65 | positionType: 'vertical', 66 | }, 67 | { 68 | type: 'picker', 69 | fieldProps: 'disabledClick', 70 | data: cityList, 71 | title: 'disabled点击', 72 | placeholder: '不可点击', 73 | disabled: true, 74 | }, 75 | { 76 | type: 'picker', 77 | fieldProps: 'noData', 78 | data: [], 79 | title: '数据源为空', 80 | placeholder: '数据源为空', 81 | }, 82 | ] as IFormItemProps[]; 83 | const formsValues = { 84 | youCity: 'fuzhou', 85 | }; 86 | const formProps = { 87 | onFinish, 88 | onFinishFailed, 89 | data: formsData, 90 | formsValues, 91 | form, 92 | failScroll: false, 93 | }; 94 | return ( 95 | <> 96 | 97 | 98 | 101 | 102 | ); 103 | }; 104 | 105 | export default Page; 106 | -------------------------------------------------------------------------------- /src/components/NomarPicker/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor, sleep } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import NomarPicker from '..'; 5 | import BasicText from './demos/basic'; 6 | import CoupletText from './demos/couplet'; 7 | 8 | const cityData = [ 9 | { label: '北京', value: 'beijing' }, 10 | { label: '上海', value: 'shanghai' }, 11 | { label: '福州', value: 'fuzhou' }, 12 | ]; 13 | 14 | const props = { 15 | fieldProps: 'myCity', 16 | required: true, 17 | data: cityData, 18 | title: '我喜欢的城市', 19 | labelNumber: 7, 20 | placeholder: '请选择我喜欢的城市', 21 | }; 22 | 23 | it('passes picker a11y test', async () => { 24 | const { container, getByText } = render( 25 |
26 |
27 | 28 | 29 |
, 30 | ); 31 | fireEvent.click(getByText('请选择我喜欢的城市')); 32 | await testA11y(container); 33 | }); 34 | 35 | test('render Basic', async () => { 36 | const onFinish = jest.fn(); 37 | const onFinishFailed = jest.fn(); 38 | const onChange = jest.fn(); 39 | const { getByText } = render( 40 | , 45 | ); 46 | expect(getByText('福州')).toBeDefined(); 47 | await waitFor(() => { 48 | fireEvent.click(getByText('Submit')); 49 | }); 50 | expect(onFinishFailed).toBeCalled(); 51 | fireEvent.click(getByText('请选择我喜欢的城市placeholder')); 52 | fireEvent.click(getByText('取消')); 53 | fireEvent.click(getByText('请选择我喜欢的城市placeholder')); 54 | fireEvent.click(getByText('确定')); 55 | expect(onChange).toBeCalled(); 56 | await waitFor(() => { 57 | expect(getByText('深圳')).toHaveClass('alitajs-dform-text-item-text'); 58 | }); 59 | await waitFor(() => { 60 | fireEvent.click(getByText('Submit')); 61 | }); 62 | expect(onFinish).toBeCalled(); 63 | fireEvent.click(getByText('不可点击')); 64 | }); 65 | 66 | test('render couplet', async () => { 67 | const { getByText } = render(); 68 | expect(getByText('请选择延迟赋值')); 69 | await sleep(1100); 70 | expect(getByText('上海')).toBeDefined(); 71 | fireEvent.click(getByText('delayValue值改为北京')); 72 | expect(getByText('北京')).toBeDefined(); 73 | fireEvent.click(getByText('请选择改值后及联')); 74 | fireEvent.click(getByText('确定')); 75 | expect(getByText('福州')).toBeDefined(); 76 | await sleep(2100); 77 | expect(getByText('杭州')).toBeDefined(); 78 | fireEvent.click(getByText('设不存在的值')); 79 | expect(getByText('请选择延迟赋数据源')); 80 | }); 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alitajs/dform", 3 | "version": "3.10.5", 4 | "scripts": { 5 | "start": "dumi dev", 6 | "docs:build": "dumi build", 7 | "docs:deploy": "gh-pages -d docs-dist", 8 | "build": "father build", 9 | "watch": "father build -w", 10 | "deploy": "npm run docs:build && npm run docs:deploy", 11 | "release": "npm run build && npm publish", 12 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 13 | "test": "jest", 14 | "test:coverage": "jest --coverage", 15 | "publish": "npm publish --access public", 16 | "publish:test": "npm run publish --tag next", 17 | "publish:test-date": "npm run publish --tag date" 18 | }, 19 | "main": "lib/index.js", 20 | "module": "es/index.js", 21 | "typings": "es/index.d.ts", 22 | "files": [ 23 | "dist", 24 | "es", 25 | "lib" 26 | ], 27 | "repository": "https://github.com/alitajs/DynamicForm", 28 | "license": "MIT", 29 | "gitHooks": { 30 | "pre-commit": "lint-staged", 31 | "commit-msg": "node scripts/verifyCommit.js" 32 | }, 33 | "lint-staged": { 34 | "*.{js,jsx,less,md,json}": [ 35 | "prettier --write" 36 | ], 37 | "*.ts?(x)": [ 38 | "prettier --parser=typescript --write" 39 | ] 40 | }, 41 | "dependencies": { 42 | "@bang88/china-city-data": "^1.0.0", 43 | "antd-mobile-v2": "2.3.4", 44 | "classnames": "^2.3.1", 45 | "copy-to-clipboard": "^3.3.1", 46 | "dayjs": "^1.10.6", 47 | "lodash.chunk": "^4.2.0", 48 | "rc-field-form": "^1.27.0", 49 | "react-transition-group": "4.4.1", 50 | "rmc-date-picker": "^6.0.10", 51 | "rmc-feedback": "^2.0.0" 52 | }, 53 | "peerDependencies": { 54 | "react": ">=16.8.6" 55 | }, 56 | "resolutions": { 57 | "@types/react": "16.14.32", 58 | "@types/react-dom": "16.9.16" 59 | }, 60 | "devDependencies": { 61 | "@alita/test": "^0.0.4", 62 | "@babel/core": "^7.16.5", 63 | "@babel/plugin-transform-runtime": "^7.16.5", 64 | "@types/lodash": "^4.14.171", 65 | "@types/lodash.chunk": "^4.2.7", 66 | "@types/react": "16.14.32", 67 | "@types/react-dom": "16.9.16", 68 | "@types/react-transition-group": "4.4.1", 69 | "alita-test": "4.0.0-alpha.6", 70 | "babel-plugin-import": "^1.13.3", 71 | "dumi": "^1.0.16", 72 | "dumi-theme-mobile": "^1.1.6", 73 | "father": "4.1.1", 74 | "gh-pages": "^3.2.3", 75 | "jest": "^27.1.0", 76 | "lint-staged": "^10.0.7", 77 | "postcss-plugin-px2rem": "^0.8.1", 78 | "prettier": "^2.2.1", 79 | "ts-node": "^10.8.1", 80 | "yorkie": "^2.0.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @alitajs/dform 2 | 3 | 零成本上手的移动端动态表单库。 4 | 5 | ## 介绍 6 | 7 | [官网文档](https://dform.alitajs.com/) 8 | 9 | 借鉴了 `antd@4` 的 `Form` 组件,针对表单使用的 `react-component/field-form` 库进行二次封装。 10 | 11 | ## 特性 12 | 13 | - 🚀 UI 的快速实现。 14 | - 🎉 实现一次性全部赋值。 15 | - 🍁 表单提交取值。 16 | - 💄 融合多类型组件表单。 17 | - 🌈 支持动态表单。 18 | - 🐠 公司内部数十个项目中得到锤炼,不断优化完善。 19 | 20 | ## 组件 21 | 22 | `dform` 共提供 `17` 种组件。涵盖: 23 | 24 | - 文本展示类型: `text` 25 | - 输入类型: `input` 和 `area` 26 | - 选择类型: `picker` 和 `select` 27 | - 多选类型: `multiplePicker` 和 `checkbox` 28 | - 开关类型: `switch` 29 | - 时间选择类型: `date` 30 | - 图片选择类型: `image` 31 | - 选择地址类型: `addressPicker` 32 | - Radio 按钮类型: `radio` 和 `coverRadio` 33 | - Check 多选类型: `check` 34 | - 时间区间选择类型: `rangeDatePicker` 35 | - 高阶输入类型: `extraInput` 36 | 37 | 如果这么多的组件还不能满足需求,不着急。我们还提供 `自定义类型: custom` 组件,让用户自己实现,并在文档中提供教程。或者给我们提个 [issues](https://github.com/alitajs/DynamicForm/issues),我们会根据评估结果进行开发和维护。 38 | 39 | ## 快速上手 40 | 41 | ```bash 42 | npm install @alitajs/dform 43 | 44 | or 45 | 46 | yarn add @alitajs/dform 47 | ``` 48 | 49 | ## 提效点 50 | 51 | ### 1、`picker` 组件: 52 | 53 | `antd-mobile-v2` 提供的 `Select` 组件涵盖了及联的类型,所以 `value` 出参以 `[]` 的形式。 54 | 55 | 但是在表单对象走接口时,每个字段的值很大情况下都是 `stirng` 或者 `number` 的形式进行传递,在 `[]` 情况下,还要对数据结构进行处理。 56 | 57 | `dform` 提供了四种选址组件: 58 | 59 | - `picker`: 单选类型,出参为 `string` 或者 `number`,不再需要对数据结构进行多一层的转化。 60 | - `select`: `antd-mobile-v2` 上的 `Select` 组件,出参入参设值保持一致。 61 | - `multiplePicker`: 多选,出参以 list 的形式提供。 62 | - `addressPicker`: 选址,更是帮你大大的提效(**舒服的写业务吧,剩下的事情交给我们**)。 63 | 64 | ### 2、一行代码配置样式 65 | 66 | 不同的项目,不同的 ui 设计师,针对表单的开发样式肯定不一样,比如: 67 | 68 | - 标题的颜色和大小 69 | - 值的颜色和大小 70 | - placeholder 颜色 71 | - ... 72 | 73 | 在 `.umirc.ts` 和 `config.ts` 下配置:`theme` 74 | 75 | ![theme](https://img-blog.csdnimg.cn/20200702171633257.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjI3ODk3OQ==,size_16,color_FFFFFF,t_70) 76 | 77 | 一行代码帮你解决整个项目 `dform` 样式问题。**不香吗?** 78 | 79 | ### 3、不敲一行代码帮你配置 `data` 的 JSON 数据 80 | 81 | 如果你连 `JSON` 格式的 `data` 也懒得写,那么 `isDev` 字段开启开发者模式,让你鼠标点一点就能编辑好一串 `JSON`,视频会告诉你用起来多舒服。 82 | 83 | 视频若打不开请直接点开[@alitajs/dform 可视化开发者模式](https://v.qq.com/x/page/u3108c1l2o8.html)链接。 84 | 85 | 86 | 87 | ## 可视化编辑方案 88 | 89 | 方案灵感来源于 [ava](https://github.com/antvis/ava) 的 `autoChart`,用法和触发条件一致,都是 data 为空且在开发状态的时候,显示编辑表单按钮。用户可以强制设定 `isDev` 来在任意环境中使用。 90 | 91 | ![2020-01-20 17 16 23](https://user-images.githubusercontent.com/11746742/72713840-b37bc900-3ba8-11ea-8a94-d19cdd39be53.gif) 92 | 93 | 更多详情,请点击[dform 官网](https://dform.alitajs.com/) 欢迎交流。感谢! 94 | -------------------------------------------------------------------------------- /src/tests/groupUsage/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, fireEvent, waitFor } from '@alita/test'; 3 | import GroupUsageTest from './groupUsageTest'; 4 | import JsonGroupTest from './jsonGroupTest'; 5 | 6 | test('group usage test', async () => { 7 | const { getByText } = render(); 8 | await waitFor(() => { 9 | fireEvent.click(getByText('Submit')); 10 | }); 11 | expect(getByText('卡片一')).toBeDefined(); 12 | expect(getByText('卡片二')).toBeDefined(); 13 | expect(getByText('卡片三')).toBeDefined(); 14 | expect(getByText('请选择性别')).toBeDefined(); 15 | expect(getByText('请选择喜欢的水果')).toBeDefined(); 16 | await waitFor(() => { 17 | expect(getByText('卡片一').parentNode?.firstChild).toHaveClass( 18 | 'alitajs-dform-card-require', 19 | ); 20 | }); 21 | await waitFor(() => { 22 | expect(getByText('卡片三').parentNode?.firstChild).toHaveStyle( 23 | 'background: rgb(24, 144, 255)', 24 | ); 25 | }); 26 | 27 | fireEvent.click(getByText('男')); 28 | await waitFor(() => { 29 | expect(getByText('男').parentNode?.firstChild).toHaveClass( 30 | 'alitajs-dform-radio-checked', 31 | ); 32 | }); 33 | fireEvent.click(getByText('香梨')); 34 | expect(getByText('请选择喜欢的水果')).toBeDefined(); 35 | fireEvent.click(getByText('菠萝')); 36 | await waitFor(() => { 37 | expect(getByText('菠萝').parentNode?.firstChild).toHaveClass( 38 | 'alitajs-dform-box-botton-checked', 39 | ); 40 | }); 41 | }); 42 | 43 | test('json group test', async () => { 44 | const { getByText } = render(); 45 | await waitFor(() => { 46 | fireEvent.click(getByText('Submit')); 47 | }); 48 | expect(getByText('卡片一')).toBeDefined(); 49 | expect(getByText('卡片二')).toBeDefined(); 50 | expect(getByText('卡片三')).toBeDefined(); 51 | expect(getByText('请选择性别')).toBeDefined(); 52 | expect(getByText('请选择喜欢的水果')).toBeDefined(); 53 | await waitFor(() => { 54 | expect(getByText('卡片一').parentNode?.firstChild).toHaveClass( 55 | 'alitajs-dform-card-require', 56 | ); 57 | }); 58 | await waitFor(() => { 59 | expect(getByText('卡片三').parentNode?.firstChild).toHaveStyle( 60 | 'background: rgb(24, 144, 255)', 61 | ); 62 | }); 63 | 64 | fireEvent.click(getByText('男')); 65 | await waitFor(() => { 66 | expect(getByText('男').parentNode?.firstChild).toHaveClass( 67 | 'alitajs-dform-radio-checked', 68 | ); 69 | }); 70 | fireEvent.click(getByText('香梨')); 71 | expect(getByText('请选择喜欢的水果')).toBeDefined(); 72 | fireEvent.click(getByText('菠萝')); 73 | await waitFor(() => { 74 | expect(getByText('菠萝').parentNode?.firstChild).toHaveClass( 75 | 'alitajs-dform-box-botton-checked', 76 | ); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 NomarImagePicker 3 | * desc: 表单使用 demo 4 | */ 5 | 6 | import React from 'react'; 7 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 8 | import DynamicForm, { 9 | IFormItemProps, 10 | useForm, 11 | Store, 12 | ValidateErrorEntity, 13 | DformImagePicker, 14 | } from '@alitajs/dform'; 15 | 16 | const fileList = [ 17 | { 18 | url: 'https://zos.alipayobjects.com/rmsportal/PZUUCKTRIHWiZSY.jpeg', 19 | id: '2121', 20 | }, 21 | { 22 | url: 'https://zos.alipayobjects.com/rmsportal/hqQWgTXdrlmVVYi.jpeg', 23 | id: '2122', 24 | }, 25 | ]; 26 | 27 | const Page = () => { 28 | const [form] = useForm(); 29 | const onFinish = (values: Store) => { 30 | // eslint-disable-next-line no-console 31 | console.log('Success:', values); 32 | }; 33 | 34 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 35 | // eslint-disable-next-line no-console 36 | console.log('Failed:', errorInfo); 37 | }; 38 | 39 | const formsValues = { 40 | maxLengthImg: fileList, 41 | }; 42 | 43 | const formProps = { 44 | onFinish, 45 | onFinishFailed, 46 | formsValues, 47 | form, 48 | isDev: true, 49 | }; 50 | 51 | return ( 52 | <> 53 | 54 | { 60 | // eslint-disable-next-line no-console 61 | console.log(files, type, index); 62 | }} 63 | maxLength={2} 64 | /> 65 | { 70 | // eslint-disable-next-line no-console 71 | console.log(index, files); 72 | }} 73 | limitSize={3 * 1024 * 1024} 74 | defaultValue={fileList} 75 | /> 76 | 83 | 89 | 90 | 91 | 94 | 95 | ); 96 | }; 97 | 98 | export default Page; 99 | -------------------------------------------------------------------------------- /src/components/NomarCheckBox/tests/demos/couplet.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm from '../../../../baseComponents/DynamicForm'; 4 | import { useForm } from 'rc-field-form'; 5 | import DformCheckBox from '../../'; 6 | 7 | const drinksList = [ 8 | { foodId: 'cola', foodName: '可乐' }, 9 | { foodId: 'sprite', foodName: '雪碧' }, 10 | { foodId: 'water', foodName: '矿泉水' }, 11 | { foodId: 'milk', foodName: '牛奶' }, 12 | { foodId: 'fruitJuice', foodName: '果汁' }, 13 | ]; 14 | const selectList = [ 15 | { foodId: 'all', foodName: '全选' }, 16 | { foodId: 'onlyCola', foodName: '只要可乐' }, 17 | ]; 18 | const CoupletText: FC = () => { 19 | const [form] = useForm(); 20 | const [formsValues, setFormsValues] = React.useState({ fruit: [] }); 21 | React.useEffect(() => { 22 | setFormsValues({ 23 | fruit: [...formsValues.fruit, 'milk', 'fruitJuice'], 24 | }); 25 | }, []); 26 | 27 | function onChange(e: any) { 28 | //全部都选 29 | if (e.indexOf('all') === 0) { 30 | setFormsValues({ 31 | fruit: ['cola', 'sprite', 'water', 'milk', 'fruitJuice'], 32 | }); 33 | } else { 34 | setFormsValues({ fruit: [] }); 35 | } 36 | // 只要可乐 37 | if (e.indexOf('all') === -1 && e.indexOf('onlyCola') === 0) { 38 | setFormsValues({ 39 | fruit: ['cola'], 40 | }); 41 | } 42 | //两个都有 43 | if (e.length === 2) { 44 | setFormsValues({ 45 | fruit: ['cola', 'sprite', 'water', 'milk', 'fruitJuice'], 46 | }); 47 | } 48 | } 49 | 50 | const formProps = { 51 | formsValues, 52 | form, 53 | failScroll: false, 54 | }; 55 | return ( 56 | <> 57 | 58 | 70 | { 81 | onChange(e); 82 | }} 83 | /> 84 | 85 | 86 | 94 | 95 | ); 96 | }; 97 | export default CoupletText; 98 | -------------------------------------------------------------------------------- /src/components/CoverRadio/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 coverRadio 3 | * desc: 表单使用 demo 4 | */ 5 | 6 | import React from 'react'; 7 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 8 | import DynamicForm, { 9 | useForm, 10 | Store, 11 | ValidateErrorEntity, 12 | CoverRadio, 13 | } from '@alitajs/dform'; 14 | 15 | const sexList = [ 16 | { sexName: '男', sexId: 'man' }, 17 | { sexName: '女', sexId: 'woman' }, 18 | ]; 19 | 20 | const foodList = [ 21 | { 22 | label: '宫保鸡丁', 23 | value: '宫保鸡丁', 24 | }, 25 | { 26 | label: '可乐鸡翅', 27 | value: '可乐鸡翅', 28 | }, 29 | { 30 | label: '爆炒虾仁', 31 | value: '爆炒虾仁', 32 | }, 33 | { 34 | label: '清蒸小黄鱼', 35 | value: '清蒸小黄鱼', 36 | }, 37 | { 38 | label: '红烧肉', 39 | value: '红烧肉', 40 | }, 41 | ]; 42 | 43 | const Page = () => { 44 | const [form] = useForm(); 45 | const onFinish = (values: Store) => { 46 | // eslint-disable-next-line no-console 47 | console.log('Success:', values); 48 | }; 49 | 50 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 51 | // eslint-disable-next-line no-console 52 | console.log('Failed:', errorInfo); 53 | }; 54 | 55 | const formsValues = { 56 | sex: 'man', 57 | }; 58 | 59 | const formProps = { 60 | onFinish, 61 | onFinishFailed, 62 | formsValues, 63 | form, 64 | isDev: false, 65 | }; 66 | 67 | return ( 68 | <> 69 | 70 | { 76 | // eslint-disable-next-line no-console 77 | console.log(val); 78 | }} 79 | alias={{ 80 | label: 'sexName', 81 | value: 'sexId', 82 | }} 83 | /> 84 | 96 | 104 | 105 | 106 | 109 | 110 | ); 111 | }; 112 | 113 | export default Page; 114 | -------------------------------------------------------------------------------- /src/components/AddressPicker/demo/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 选址 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Toast, Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import { AddressPicker } from '@alitajs/dform'; 8 | // @ts-ignore 9 | import CountryList from '@bang88/china-city-data'; 10 | 11 | const Page: FC = () => { 12 | const [homeAddrData, setHomeAddrData] = useState([]); 13 | const [value, setValue] = useState({ 14 | label: ['福建省', '福州市', '鼓楼区'], 15 | value: ['35', '3501', '350102'], 16 | }); 17 | 18 | const queryList = (list: any, val: string | number) => { 19 | let newList: any[] = []; 20 | list.map((item: { value: string; children: any[] }) => { 21 | if (item.value === val) { 22 | newList = item.children; 23 | } 24 | if (item.children && Array.isArray(item.children)) { 25 | const vals = queryList(item.children, val); 26 | if (vals && vals.length > 0) { 27 | newList = vals; 28 | } 29 | } 30 | }); 31 | return newList; 32 | }; 33 | 34 | const getResetHomeAddrList = (values: (number | string)[]) => { 35 | let data: { label: string; value: string }[] = []; 36 | switch (values.length) { 37 | case 0: 38 | data = CountryList; 39 | break; 40 | case 1: 41 | case 2: 42 | data = queryList(CountryList, values[values.length - 1]); 43 | break; 44 | case 3: 45 | break; 46 | default: 47 | break; 48 | } 49 | return data; 50 | }; 51 | 52 | const resetHomeAddrList = (values: (number | string)[], key: string) => { 53 | let mValues = JSON.parse(JSON.stringify(values)); 54 | let data: { label: string; value: string }[] = 55 | getResetHomeAddrList(mValues); 56 | Toast.hide(); 57 | setHomeAddrData(data); 58 | }; 59 | 60 | return ( 61 | <> 62 | { 70 | console.log('values', values); 71 | Toast.show('加载中'); 72 | // eslint-disable-next-line no-console 73 | setTimeout(() => { 74 | resetHomeAddrList(values, 'homeAddrData'); 75 | }, 300); 76 | }} 77 | defaultValue={value} 78 | onChange={(value: any) => { 79 | setValue(value); 80 | }} 81 | /> 82 | 83 | 86 | 87 | ); 88 | }; 89 | 90 | export default Page; 91 | -------------------------------------------------------------------------------- /src/components/AddressPicker/tests/demos/single.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 选址 3 | * desc: 单独使用 demo 4 | */ 5 | import React, { FC, useState } from 'react'; 6 | import { Toast, Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import AddressPicker from '../..'; 8 | 9 | // @ts-ignore 10 | import CountryList from '@bang88/china-city-data'; 11 | 12 | const Page: FC = () => { 13 | const [homeAddrData, setHomeAddrData] = useState([]); 14 | const [value, setValue] = useState({ 15 | label: ['福建省', '福州市', '鼓楼区'], 16 | value: ['35', '3501', '350102'], 17 | }); 18 | 19 | const queryList = (list: any, val: string | number) => { 20 | let newList: any[] = []; 21 | list.map((item: { value: string; children: any[] }) => { 22 | if (item.value === val) { 23 | newList = item.children; 24 | } 25 | if (item.children && Array.isArray(item.children)) { 26 | const vals = queryList(item.children, val); 27 | if (vals && vals.length > 0) { 28 | newList = vals; 29 | } 30 | } 31 | }); 32 | return newList; 33 | }; 34 | 35 | const getResetHomeAddrList = (values: (number | string)[]) => { 36 | let data: { label: string; value: string }[] = []; 37 | switch (values.length) { 38 | case 0: 39 | data = CountryList; 40 | break; 41 | case 1: 42 | case 2: 43 | data = queryList(CountryList, values[values.length - 1]); 44 | break; 45 | case 3: 46 | break; 47 | default: 48 | break; 49 | } 50 | return data; 51 | }; 52 | 53 | const resetHomeAddrList = (values: (number | string)[], key: string) => { 54 | let mValues = JSON.parse(JSON.stringify(values)); 55 | let data: { label: string; value: string }[] = 56 | getResetHomeAddrList(mValues); 57 | Toast.hide(); 58 | setHomeAddrData(data); 59 | }; 60 | 61 | return ( 62 | <> 63 | { 71 | console.log('values', values); 72 | Toast.show('加载中'); 73 | // eslint-disable-next-line no-console 74 | setTimeout(() => { 75 | resetHomeAddrList(values, 'homeAddrData'); 76 | }, 300); 77 | }} 78 | defaultValue={value} 79 | onChange={(value: any) => { 80 | setValue(value); 81 | }} 82 | /> 83 | 84 | 87 | 88 | ); 89 | }; 90 | 91 | export default Page; 92 | -------------------------------------------------------------------------------- /src/components/NomarImagePicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useEffect, useContext, useMemo } from 'react'; 2 | import { 3 | CardContext, 4 | CardContextProps, 5 | DformContext, 6 | DformContextProps, 7 | } from '../../baseComponents/Context'; 8 | import ImagePickerGroup from './imagePickerGroup'; 9 | import { ImageFile, INomarImagePickerProps } from './interface'; 10 | import Field from '../Field'; 11 | import Title from '../../baseComponents/Title'; 12 | import { allPrefixCls } from '../../const/index'; 13 | import './index.less'; 14 | 15 | const DformImagePicker: FC = (props) => { 16 | const { 17 | coverStyle, 18 | title, 19 | required = false, 20 | fieldProps, 21 | fieldName, 22 | rules = [], 23 | hasStar = true, 24 | limitSize = 0, 25 | subTitle, 26 | hidden = false, 27 | extra = '', 28 | onChange, 29 | defaultValue = [], 30 | boxStyle, 31 | titleStyle, 32 | formFlag = true, 33 | disabled = false, 34 | ...otherProps 35 | } = props; 36 | 37 | const { cDisabled } = useContext(CardContext); 38 | const [mregedDisabled, setMregedDisabled] = useState( 39 | disabled || cDisabled, 40 | ); 41 | const { changeForm } = useContext(DformContext); 42 | 43 | const fieldKey: any = fieldName || fieldProps; 44 | 45 | useMemo(() => { 46 | if (cDisabled) return; 47 | if (changeForm[fieldKey]?.disabled !== undefined) { 48 | setMregedDisabled(changeForm[fieldKey]?.disabled); 49 | } else { 50 | setMregedDisabled(disabled); 51 | } 52 | }, [changeForm[fieldKey], disabled, cDisabled]); 53 | 54 | const imageChange = ( 55 | files: ImageFile[], 56 | operationType: string, 57 | index: number | undefined, 58 | ) => { 59 | if (onChange) onChange(files, operationType, index); 60 | }; 61 | 62 | return ( 63 | 69 | <div className={`${allPrefixCls}-image`}> 70 | <Field 71 | title={title} 72 | required={required} 73 | rules={rules} 74 | name={fieldKey} 75 | initialValue={defaultValue} 76 | params={{ 77 | hidden, 78 | formFlag, 79 | }} 80 | type="image" 81 | > 82 | <ImagePickerGroup 83 | {...otherProps} 84 | disabled={mregedDisabled} 85 | value={defaultValue} 86 | onChange={imageChange} 87 | limitSize={limitSize} 88 | /> 89 | </Field> 90 | </div> 91 | 92 | ); 93 | }; 94 | 95 | DformImagePicker.displayName = 'dformImagePicker'; 96 | export default DformImagePicker; 97 | -------------------------------------------------------------------------------- /src/components/Field/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useContext, useState, useMemo } from 'react'; 2 | import { Field } from 'rc-field-form'; 3 | import { FieldProps } from 'rc-field-form/es/Field'; 4 | import { DformContext, DformContextProps } from '../../baseComponents/Context'; 5 | import { TitleTypePorps } from '../../PropsType'; 6 | import { PLACEHOLDER_MENU } from '../../utils/menu'; 7 | import '../../styles/index.less'; 8 | 9 | export interface CustomFieldProps extends FieldProps { 10 | params?: any; 11 | style?: React.CSSProperties; 12 | title?: string; 13 | required?: boolean; 14 | type?: TitleTypePorps; 15 | } 16 | 17 | const CustomField: FC = (props: any) => { 18 | const { 19 | rules = [], 20 | params = {}, 21 | style = {}, 22 | required = false, 23 | title = '', 24 | type, 25 | name, 26 | ...restProps 27 | } = props; 28 | const { hidden = false, formFlag: fFlag = true } = params; 29 | const [mregedRequired, setMregedRequired] = useState(required); 30 | const [mregedHidden, setMregedHidden] = useState(hidden); 31 | 32 | const { 33 | changeForm, 34 | formFlag = false, 35 | updateErrorValue, 36 | } = useContext(DformContext); 37 | 38 | useMemo(() => { 39 | if (changeForm[name]?.required !== undefined) { 40 | setMregedRequired(changeForm[name]?.required); 41 | } else { 42 | setMregedRequired(required); 43 | } 44 | if (changeForm[name]?.hidden !== undefined) { 45 | setMregedHidden(changeForm[name]?.hidden); 46 | } else { 47 | setMregedHidden(hidden); 48 | } 49 | }, [changeForm[name], hidden, required]); 50 | 51 | const shouldUpdate = (prevValue: any, nextValue: any) => { 52 | if (prevValue[props?.name] !== nextValue[props?.name]) { 53 | if (updateErrorValue) updateErrorValue(name); 54 | } 55 | if (props.shouldUpdate && typeof props.shouldUpdate === 'function') { 56 | props.shouldUpdate(prevValue, nextValue, {}); 57 | } 58 | return prevValue !== nextValue; 59 | }; 60 | 61 | // 不在DynamicForm中 取消Field包裹; 62 | if (!formFlag || !fFlag) { 63 | return
{props.children}
; 64 | } 65 | 66 | return ( 67 |
68 | 84 |
85 | ); 86 | }; 87 | 88 | export default CustomField; 89 | -------------------------------------------------------------------------------- /src/utils/tool.ts: -------------------------------------------------------------------------------- 1 | import { ValidateErrorEntity } from 'rc-field-form/es/interface'; 2 | import { ErrorValueProps, IFormItemProps } from '../PropsType'; 3 | 4 | /** 5 | * 重置错误信息提示 6 | */ 7 | export const resetErrorField = ( 8 | errorValue: ErrorValueProps, 9 | values: { [key: string]: any }, 10 | ) => { 11 | const errorObj = { ...errorValue }; 12 | const key = Object.keys(values)[0]; 13 | if (errorObj[key]) { 14 | errorObj[key] = undefined; 15 | return { success: true, errorObj }; 16 | } else { 17 | return { success: false }; 18 | } 19 | }; 20 | 21 | /** 22 | * 通过 onFinishFailed 的方法 23 | */ 24 | export const getAllError = (errorInfo: ValidateErrorEntity) => { 25 | const { errorFields = [] } = errorInfo; 26 | const errorObj = {} as any; 27 | errorFields.forEach((item: any) => { 28 | errorObj[item[`name`][0]] = item[`errors`][0]; 29 | }); 30 | return errorObj; 31 | }; 32 | 33 | /** 34 | * 滚动到错误的位置 35 | * @param errorInfo 36 | * @param onFinishFailed 37 | * @param failScroll 38 | * @returns 39 | */ 40 | export const defaultFailed = ( 41 | errorInfo: ValidateErrorEntity, 42 | onFinishFailed?: (errorInfo: ValidateErrorEntity) => void, 43 | failScroll?: boolean, 44 | ) => { 45 | if ( 46 | !errorInfo || 47 | !errorInfo.errorFields || 48 | errorInfo.errorFields.length === 0 49 | ) { 50 | if (onFinishFailed) onFinishFailed(errorInfo); 51 | return; 52 | } 53 | const scrollToField = (fieldKey: any) => { 54 | const labelNode = document.getElementById(`alita-dform-${fieldKey}`); 55 | if (labelNode && labelNode.scrollIntoView) { 56 | labelNode.scrollIntoView?.({ 57 | behavior: 'smooth', 58 | block: 'center', 59 | inline: 'center', 60 | }); 61 | } 62 | }; 63 | if (failScroll) scrollToField(errorInfo.errorFields[0].name[0]); 64 | if (onFinishFailed) onFinishFailed(errorInfo); 65 | }; 66 | 67 | export const getByteLen = (val: string) => { 68 | let len = 0; 69 | `${val}`.split('').forEach((item) => { 70 | // eslint-disable-next-line no-control-regex 71 | if (item.match(/[^\x00-\xff]/gi) != null) { 72 | len += 2; 73 | } else { 74 | len += 1; 75 | } 76 | }); 77 | return len; 78 | }; 79 | 80 | export const changeData = (item: IFormItemProps, autoLineFeed: boolean) => { 81 | if (item?.hidden) { 82 | item.required = false; 83 | } 84 | if (item.positionType === 'vertical' || !autoLineFeed) return item; 85 | if (item.title) { 86 | const titleSize = getByteLen(item.title); 87 | if (titleSize >= 16) { 88 | item.positionType = 'vertical'; 89 | } else if (item.type === 'input' || item.type === 'extraInput') { 90 | if (titleSize > 8) { 91 | item.labelNumber = titleSize / 2 + 1; 92 | } else { 93 | item.labelNumber = 7; 94 | } 95 | } 96 | } 97 | return item; 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/NomarCustom/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect } from 'react'; 2 | import classnames from 'classnames'; 3 | import Field from '../Field'; 4 | import Title from '../../baseComponents/Title'; 5 | import CustomGroup from './CustomGroup'; 6 | import HorizontalTitle from '../../baseComponents/HorizontalTitle'; 7 | import { INomarCustomPorps } from './interface'; 8 | import { allPrefixCls } from '../../const'; 9 | import './index.less'; 10 | 11 | const DformCustom: FC = (props) => { 12 | const { 13 | defaultValue, 14 | fieldProps, 15 | fieldName, 16 | required = false, 17 | rules = [], 18 | title, 19 | CustomDom, 20 | customDomProps, 21 | children, 22 | hasStar = true, 23 | positionType = 'vertical', 24 | labelNumber = 7, 25 | onChange, 26 | boxStyle, 27 | titleStyle, 28 | formFlag = true, 29 | hidden = false, 30 | } = props; 31 | 32 | const isVertical = positionType === 'vertical'; 33 | 34 | const fieldKey: any = fieldName || fieldProps; 35 | 36 | useEffect(() => { 37 | if (CustomDom || customDomProps) { 38 | console.warn( 39 | 'DformCustom组件已放弃CustomDom、customDomProps属性,请切换为children', 40 | ); 41 | } 42 | }, [CustomDom, customDomProps]); 43 | 44 | const cutomTitle = () => ( 45 | 54 | ); 55 | 56 | const fieldChange = (e: any) => { 57 | if (onChange) onChange(e); 58 | }; 59 | 60 | return ( 61 | 67 | <div 68 | className={classnames({ 69 | [`${allPrefixCls}-dom`]: true, 70 | })} 71 | > 72 | {!isVertical && cutomTitle()} 73 | <Field 74 | name={fieldKey} 75 | title={title} 76 | required={required} 77 | rules={rules} 78 | initialValue={defaultValue} 79 | type="custom" 80 | params={{ 81 | hidden, 82 | formFlag, 83 | }} 84 | > 85 | <CustomGroup 86 | isVertical={isVertical} 87 | CustomDom={CustomDom} 88 | customDomProps={customDomProps} 89 | cutomTitle={cutomTitle()} 90 | onChange={fieldChange} 91 | children={children} 92 | /> 93 | </Field> 94 | </div> 95 | 96 | ); 97 | }; 98 | 99 | DformCustom.displayName = 'dformCustom'; 100 | export default DformCustom; 101 | -------------------------------------------------------------------------------- /src/components/NomarCustom/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多选框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | DformCustom, 12 | } from '@alitajs/dform'; 13 | 14 | interface IDemoPage { 15 | name: string; 16 | onChange?: (currentActiveLink: string) => void; 17 | value?: string; 18 | } 19 | 20 | const DemoPage: FC = (props) => { 21 | const { name, onChange, value } = props; 22 | // eslint-disable-next-line react-hooks/rules-of-hooks 23 | // eslint-disable-next-line react-hooks/rules-of-hooks 24 | return ( 25 |
26 |

name: {name}

27 |

28 | age: 29 | { 34 | if (onChange) onChange(e.target.value); 35 | }} 36 | /> 37 |

38 |
39 | ); 40 | }; 41 | 42 | const Page: FC = () => { 43 | const [form] = useForm(); 44 | const onFinish = (values: Store) => { 45 | // eslint-disable-next-line no-console 46 | console.log('Success:', values); 47 | }; 48 | 49 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 50 | // eslint-disable-next-line no-console 51 | console.log('Failed:', errorInfo); 52 | }; 53 | 54 | const formsValues = { 55 | age: '17', 56 | }; 57 | 58 | const formProps = { 59 | form, 60 | onFinish, 61 | onFinishFailed, 62 | formsValues, 63 | isDev: true, 64 | // isPc: true, 65 | }; 66 | return ( 67 | <> 68 | 69 | 75 |
78 | This is a display page 79 |
80 |
81 | console.log(e)} 88 | > 89 | 90 | 91 |
92 | 93 | 101 | 102 | ); 103 | }; 104 | 105 | export default Page; 106 | -------------------------------------------------------------------------------- /src/assets/svg.less: -------------------------------------------------------------------------------- 1 | .svg-file-icon() { 2 | @svg-bg-img: ' '; 3 | // .encoded-svg-background-i(@svg-bg-img); 4 | } 5 | -------------------------------------------------------------------------------- /src/components/ExtraInput/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor, sleep } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import BasicTest from './demos/basic'; 5 | import ExtraInput from '../index'; 6 | 7 | const myProps = { 8 | fieldProps: 'minPrise', 9 | fieldProps2: 'maxPrise', 10 | title: '价格区间(数字输入)', 11 | firstProps: { placeholder: '输入最小价格' }, 12 | secondProps: { extra: '¥', placeholder: '输入最大价格' }, 13 | required: true, 14 | }; 15 | it('passes picker a11y test', async () => { 16 | const { container, getByText } = render( 17 |
18 |
19 | 20 | 21 |
, 22 | ); 23 | await testA11y(container); 24 | }); 25 | 26 | test("renders Basic", async () => { 27 | const onFinish = jest.fn(); 28 | const onFinishFailed = jest.fn(); 29 | const { getByText, getByLabelText } = render( 30 | 31 | ); 32 | expect(getByLabelText('minPrise')).toHaveValue('11'); 33 | fireEvent.change(getByLabelText('minPrise'), { target: { value: '12' } }); 34 | expect(getByLabelText('minPosition')).toHaveValue(''); 35 | fireEvent.change(getByLabelText('minLength'), { target: { value: '10' } }); 36 | expect(getByLabelText('minLength')).toHaveValue('10'); 37 | await waitFor(() => { 38 | fireEvent.click(getByText('Submit')); 39 | }); 40 | expect(onFinish).toBeCalled(); 41 | fireEvent.change(getByLabelText('price'), { target: { value: '40' } }); 42 | expect(getByLabelText('price')).toHaveValue('40'); 43 | fireEvent.click(getByText('选择区间')); 44 | await waitFor(() => { 45 | fireEvent.click(getByText("取消")) 46 | }) 47 | fireEvent.click(getByText('选择区间')); 48 | await waitFor(() => { 49 | expect(getByText("确定")).toBeDefined() 50 | fireEvent.click(getByText("元")) 51 | fireEvent.click(getByText("确定")) 52 | }) 53 | fireEvent.click(getByText('元')); 54 | await waitFor(() => { 55 | fireEvent.click(getByText("万元")) 56 | fireEvent.click(getByText("确定")) 57 | expect(getByText("万元")).toBeDefined(); 58 | }) 59 | const focu = getByLabelText('minLength'); 60 | focu.focus(); 61 | await waitFor(() => { 62 | expect(getByLabelText("minLength").parentNode).toHaveClass( 63 | "alitajs-dform-input-item-focus" 64 | ) 65 | }) 66 | focu.blur(); 67 | fireEvent.click(getByText("选择长度单位")) 68 | await waitFor(() => { 69 | expect(getByText("确定")).toBeDefined(); 70 | fireEvent.click(getByText('百元')); 71 | fireEvent.click(getByText('确定')) 72 | }) 73 | await sleep(500) 74 | fireEvent.click(getByText("百元")) 75 | await waitFor(() => { 76 | expect(getByText("确定")).toBeDefined(); 77 | fireEvent.click(getByText('千元')); 78 | fireEvent.click(getByText('确定')) 79 | expect(getByText("千元")).toBeDefined(); 80 | }) 81 | 82 | }) 83 | -------------------------------------------------------------------------------- /src/components/NomarCustom/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom 3 | group: 4 | title: Custom 5 | nav: 6 | title: 组件 7 | path: /components 8 | --- 9 | 10 | # Custom 11 | 12 | ## 代码演示 13 | 14 | 15 | 16 | ## API 17 | 18 | | 参数 | 说明 | 类型 | 默认值 | 是否必填 | 19 | | -------------- | ---------------------------------------------------------- | ----------------------------- | ----------- | ---------- | 20 | | title | 标题 | string | '' | 是 | 21 | | fieldProps | 文本属性 | boolean | false | 是 | 22 | | required | 必填判断 | boolean | false | 否 | 23 | | positionType | 样式类型 | `vertical` or `horizontal` | false | `vertical` | 24 | | hasStar | 必填项红\*展示与否的判断 | boolean | true | 否 | 25 | | rules | 规则校验(如需用到该字段,请重写 `required` 校验) | array | [] | 否 | 26 | | CustomDom | 自定义样式 | React.Node | | 是 | 27 | | customDomProps | 自定义样式传入的值 | Object | {} | 否 | 28 | | subTitle | 标题右侧的副标题,仅在 `positionType` 为 `vertical` 时生效 | string or node | '' | 否 | 29 | | hidden | 字段展示与否的判断 | boolean | false | 否 | 30 | | renderHeader | 组件头部 | `number` or `string` | - | 否 | 31 | | defaultValue | 设置初始取值 | string | - | 否 | 32 | | onChange | 值改变事件 | object | (e) => void | 否 | 33 | | extra | | `string` or `React.ReactNode` | '' | 否 | 34 | | renderHeader | 组件头部 | `string` or `React.ReactNode` | '' | 否 | 35 | | renderFooter | 组件尾部 | `string` or `React.ReactNode` | '' | 否 | 36 | 37 | ## 自定义组件开发教程 38 | 39 | ### 非受控 40 | 41 | 如果你在项目中使用到的组件只展示内容,只需要将组件做为 `CustomDom` 参数传入,组件里需要传入的参数,通过 `customDomProps` 传入。 42 | 43 | ### 受控 44 | 45 | 如果项目中用到的自定义组件需要受控,需在自定义组件中增加 `props.onChange()` 函数,改变表单受控值。 46 | 47 | 组件会通过 `props.defaultValue` 传入在 `initialValue` 设置的初始值。你可以进行页面初始化的数据回填操作。 48 | -------------------------------------------------------------------------------- /src/components/NomarPicker/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 单选框 3 | * desc: 表单使用 demo 4 | */ 5 | 6 | import React, { FC } from 'react'; 7 | import { Button, WhiteSpace, Toast } from 'antd-mobile-v2'; 8 | import DynamicForm, { 9 | useForm, 10 | Store, 11 | ValidateErrorEntity, 12 | DformPicker, 13 | } from '@alitajs/dform'; 14 | 15 | const Page: FC = () => { 16 | const [form] = useForm(); 17 | const onFinish = (values: Store) => { 18 | // eslint-disable-next-line no-console 19 | console.log('Success:', values); 20 | }; 21 | 22 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 23 | // eslint-disable-next-line no-console 24 | console.log('Failed:', errorInfo); 25 | }; 26 | 27 | const aliasCityList = [ 28 | { 29 | cityId: '深圳', 30 | cityName: 'shenzhen', 31 | }, 32 | { 33 | cityId: '杭州', 34 | cityName: 'hangzhou', 35 | }, 36 | { 37 | cityId: '广州', 38 | cityName: 'guangzhou', 39 | }, 40 | ]; 41 | 42 | const cityList = [ 43 | { 44 | label: '北京', 45 | value: 'beijing', 46 | }, 47 | { 48 | label: '上海', 49 | value: 'shanghai', 50 | }, 51 | { 52 | label: '福州', 53 | value: 'fuzhou', 54 | }, 55 | ]; 56 | 57 | const formsValues = { 58 | youCity: 'fuzhou', 59 | }; 60 | const formProps = { 61 | onFinish, 62 | onFinishFailed, 63 | formsValues, 64 | form, 65 | autoLineFeed: false, 66 | isDev: false, 67 | }; 68 | return ( 69 | <> 70 | 71 | 79 | 92 | 99 | console.log(e)} 105 | disabled 106 | /> 107 | 108 | 109 | 112 | 113 | ); 114 | }; 115 | 116 | export default Page; 117 | -------------------------------------------------------------------------------- /src/components/NomarSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useContext, useMemo } from 'react'; 2 | import { 3 | CardContext, 4 | CardContextProps, 5 | DformContext, 6 | DformContextProps, 7 | } from '../../baseComponents/Context'; 8 | import { Switch } from 'antd-mobile-v2'; 9 | import { allPrefixCls } from '../../const'; 10 | import HorizontalTitle from '../../baseComponents/HorizontalTitle'; 11 | import Field from '../Field'; 12 | import Title from '../../baseComponents/Title'; 13 | import { INomarSwitchProps } from './interface'; 14 | import './index.less'; 15 | 16 | const DformSwitch: FC = (props) => { 17 | const { 18 | coverStyle, 19 | title, 20 | required = false, 21 | fieldProps, 22 | fieldName, 23 | rules = [], 24 | placeholder, 25 | hasStar = true, 26 | hidden = false, 27 | className = '', 28 | defaultValue = false, 29 | labelNumber = 7, 30 | boxStyle, 31 | titleStyle, 32 | formFlag = true, 33 | disabled = false, 34 | ...otherProps 35 | } = props; 36 | 37 | const { cDisabled } = useContext(CardContext); 38 | const [mregedDisabled, setMregedDisabled] = useState( 39 | disabled || cDisabled, 40 | ); 41 | const { changeForm } = useContext(DformContext); 42 | 43 | const fieldKey: any = fieldName || fieldProps; 44 | 45 | useMemo(() => { 46 | if (cDisabled) return; 47 | if (changeForm[fieldKey]?.disabled !== undefined) { 48 | setMregedDisabled(changeForm[fieldKey]?.disabled); 49 | } else { 50 | setMregedDisabled(disabled); 51 | } 52 | }, [changeForm[fieldKey], disabled, cDisabled]); 53 | 54 | return ( 55 | 61 | {!hidden && ( 62 | <div className={`${allPrefixCls}-switch`}> 63 | <HorizontalTitle 64 | required={required} 65 | hasStar={hasStar} 66 | title={title} 67 | labelNumber={labelNumber} 68 | isVertical={false} 69 | fieldProps={fieldKey} 70 | titleStyle={titleStyle} 71 | /> 72 | <Field 73 | type="switch" 74 | title={title} 75 | required={required} 76 | rules={rules} 77 | name={fieldKey} 78 | valuePropName="checked" 79 | initialValue={defaultValue} 80 | params={{ 81 | hidden, 82 | formFlag, 83 | }} 84 | > 85 | <Switch 86 | checked={defaultValue} 87 | {...otherProps} 88 | disabled={mregedDisabled} 89 | /> 90 | </Field> 91 | </div> 92 | )} 93 | 94 | ); 95 | }; 96 | 97 | DformSwitch.displayName = 'dformSwitch'; 98 | export default DformSwitch; 99 | -------------------------------------------------------------------------------- /src/components/NomarRadio/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render, testA11y, fireEvent, waitFor } from '@alita/test'; 3 | import Form from 'rc-field-form'; 4 | import NomarRadio from '../index'; 5 | import BasicTest from './demos/basic'; 6 | import SingleText from './demos/single'; 7 | 8 | const radioList = [ 9 | { 10 | label: '是', 11 | value: 'yes', 12 | }, 13 | { 14 | label: '否', 15 | value: 'no', 16 | }, 17 | ]; 18 | const myProps = { 19 | type: 'redio', 20 | title: '上学', 21 | fieldProps: 'goToSchool', 22 | required: true, 23 | data: radioList, 24 | }; 25 | 26 | it('passes picker a11y test', async () => { 27 | const { container, getByText } = render( 28 |
29 |
30 | 31 | 32 |
, 33 | ); 34 | fireEvent.click(getByText('否')); 35 | await testA11y(container); 36 | }); 37 | 38 | test('renders Basic', async () => { 39 | const onFinish = jest.fn(); 40 | const onFinishFailed = jest.fn(); 41 | const { getByText, getAllByText } = render( 42 | , 43 | ); 44 | fireEvent.click(getByText('Submit')); 45 | await waitFor(() => { 46 | expect(onFinishFailed).toBeCalled(); 47 | }); 48 | expect(getByText('发票')).toBeDefined(); 49 | // 判断是否选择 50 | fireEvent.click(getAllByText('否')[0]); 51 | await waitFor(() => { 52 | expect(getAllByText('否')[0].parentNode?.firstChild).toHaveClass( 53 | 'alitajs-dform-radio-checked', 54 | ); 55 | }); 56 | //不可选中 57 | fireEvent.click(getByText('晴')); 58 | await waitFor(() => { 59 | expect(getByText('雨').parentNode?.firstChild).toHaveClass( 60 | 'alitajs-dform-radio-checked', 61 | ); 62 | }); 63 | fireEvent.click(getAllByText('是')[1]); 64 | fireEvent.click(getByText('宫保鸡丁')); 65 | fireEvent.click(getByText('宫保鸡丁')); 66 | fireEvent.click(getByText('宫保鸡丁')); 67 | fireEvent.click(getByText('Submit')); 68 | await waitFor(() => { 69 | expect(onFinish).toBeCalled(); 70 | }); 71 | }); 72 | 73 | test('single basic', async () => { 74 | const onChange = jest.fn(); 75 | const { getByText, getAllByText } = render( 76 | , 77 | ); 78 | 79 | // with default value 80 | expect(getByText('女')).parentNode?.firstChild.toHaveClass( 81 | 'litajs-dform-radio-checked', 82 | ); 83 | 84 | // onchange 85 | fireEvent.click(getAllByText('男')[0]); 86 | expect(onChange).toBeCalled(); 87 | expect(getByText('男')).parentNode?.firstChild.toHaveClass( 88 | 'litajs-dform-radio-checked', 89 | ); 90 | expect(getByText('女')) 91 | .parentNode?.firstChild.toHaveClass('litajs-dform-radio-checked') 92 | .toBeFalsy(); 93 | 94 | // console.log after submit 95 | const consoleSpy = jest.spyOn(console, 'log'); 96 | fireEvent.click(getByText('Submit')); 97 | expect(consoleSpy).toHaveBeenCalledWith({ value: '1' }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/components/NomarSelect/tests/demos/couplet.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState, useEffect } from 'react'; 2 | import { act } from 'react-dom/test-utils'; 3 | import { Button } from 'antd-mobile-v2'; 4 | import DynamicForm from '../../../../'; 5 | import { useForm } from 'rc-field-form'; 6 | import { Store, ValidateErrorEntity, IFormItemProps } from '../../../..'; 7 | 8 | const pickerData = [ 9 | { 10 | label: '北京', 11 | value: 'beijing', 12 | }, 13 | { 14 | label: '上海', 15 | value: 'shanghai', 16 | }, 17 | { 18 | label: '福州', 19 | value: 'fuzhou', 20 | }, 21 | { 22 | label: '杭州', 23 | value: 'hangzhou', 24 | }, 25 | ]; 26 | 27 | const Couplet: FC = () => { 28 | const [form] = useForm(); 29 | const [formsValues, setFormsValues] = useState({}); 30 | const [delayData, setDelayData] = useState([]); 31 | useEffect(() => { 32 | setTimeout(() => { 33 | act(() => { 34 | setFormsValues({ 35 | ...formsValues, 36 | delayValue: 'shanghai', 37 | delayData: 'hangzhou', 38 | }); 39 | }); 40 | }, 1000); 41 | setTimeout(() => { 42 | act(() => { 43 | setDelayData(pickerData); 44 | }); 45 | }, 3000); 46 | }, []); 47 | const onFinish = (values: Store) => {}; 48 | 49 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => {}; 50 | 51 | const formsData = [ 52 | { 53 | type: 'picker', 54 | fieldProps: '', 55 | fieldName: 'delayValue', 56 | data: pickerData, 57 | title: '延迟赋值', 58 | labelNumber: 7, 59 | placeholder: '请选择延迟赋值', 60 | }, 61 | { 62 | type: 'picker', 63 | fieldProps: '', 64 | fieldName: 'couplet', 65 | data: pickerData, 66 | title: '改值后及联', 67 | labelNumber: 7, 68 | placeholder: '请选择改值后及联', 69 | onChange: () => { 70 | setFormsValues({ 71 | ...formsValues, 72 | delayValue: 'fuzhou', 73 | }); 74 | }, 75 | }, 76 | { 77 | type: 'picker', 78 | fieldProps: 'delayData', 79 | data: delayData, 80 | title: '延迟赋数据源', 81 | labelNumber: 7, 82 | placeholder: '请选择延迟赋数据源', 83 | }, 84 | ] as IFormItemProps[]; 85 | const formProps = { 86 | onFinish, 87 | onFinishFailed, 88 | data: formsData, 89 | formsValues, 90 | form, 91 | failScroll: false, 92 | }; 93 | return ( 94 |
95 | 96 | 97 | 107 |
108 | ); 109 | }; 110 | 111 | export default Couplet; 112 | -------------------------------------------------------------------------------- /src/demo/api.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { Button } from 'antd-mobile-v2'; 3 | import DynamicForm, { 4 | useForm, 5 | Store, 6 | ValidateErrorEntity, 7 | DformInput, 8 | DformPicker, 9 | DformRadio, 10 | DformSelect, 11 | DformTextArea, 12 | MultiplePicker, 13 | } from '@alitajs/dform'; 14 | 15 | const cityList = [ 16 | { 17 | label: '北京', 18 | value: 'beijing', 19 | }, 20 | { 21 | label: '上海', 22 | value: 'shanghai', 23 | }, 24 | { 25 | label: '福州', 26 | value: 'fuzhou', 27 | }, 28 | ]; 29 | 30 | const Page: FC = () => { 31 | const [form] = useForm(); 32 | const [formsValues, setFormsValues] = useState>({ 33 | inputs: '1', 34 | radios: 'fuzhou', 35 | selects: ['fuzhou'], 36 | areas: '1', 37 | }); 38 | 39 | const onFinish = (values: Store) => { 40 | // eslint-disable-next-line no-console 41 | console.log('Success:', values); 42 | }; 43 | 44 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 45 | // eslint-disable-next-line no-console 46 | console.log('Failed:', errorInfo); 47 | }; 48 | 49 | const formProps = { 50 | onFinish, 51 | onFinishFailed, 52 | formsValues, 53 | form, 54 | isDev: false, 55 | }; 56 | 57 | return ( 58 |
59 | 60 | 61 | 68 | 74 | 82 | 87 | 97 | 98 | 106 | 113 |
114 | ); 115 | }; 116 | 117 | export default Page; 118 | -------------------------------------------------------------------------------- /src/baseComponents/DynamicForm/index.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/index.less'; 2 | 3 | @paddingWrapper: 14 * @hd; 4 | 5 | @prefixClsExtends: alitajs-dform-card-extends; 6 | 7 | .@{prefixCls} { 8 | background-color: @fill-base; 9 | border-radius: 6 * @hd; 10 | &-header { 11 | display: flex; 12 | flex-direction: row; 13 | padding: 16 * @hd 16 * @hd 0; 14 | &-title { 15 | flex: 1; 16 | margin-right: 10 * @hd; 17 | color: @color-text-base; 18 | font-weight: 600; 19 | font-size: 17 * @hd; 20 | } 21 | &-icon { 22 | padding-left: 10 * @hd; 23 | } 24 | } 25 | } 26 | 27 | .@{prefixCls}-group { 28 | // 子元素下如果还有嵌套group的话,应该在子元素增加padding-bottom; 29 | .@{prefixCls}-group:last-child { 30 | padding-bottom: @paddingWrapper; 31 | } 32 | } 33 | .@{prefixCls}-card { 34 | // margin: 0 12 * @hd; 35 | padding-top: @paddingWrapper; 36 | overflow: hidden; 37 | background: #fff; 38 | border-radius: 6 * @hd; 39 | 40 | &-border { 41 | border: 1 * @hd solid rgb(237, 240, 245); 42 | } 43 | 44 | &-title-box { 45 | display: flex; 46 | flex-direction: row; 47 | padding: 0 12 * @hd; 48 | } 49 | &-require { 50 | color: #ff4848; 51 | font-size: 17 * @hd; 52 | } 53 | &-title { 54 | flex: 1; 55 | color: #333; 56 | font-weight: bold; 57 | font-size: 18 * @hd; 58 | } 59 | 60 | .@{prefixCls}-content:last-child { 61 | .@{prefixCls}-cell { 62 | border-bottom: none !important; 63 | } 64 | } 65 | 66 | &-extends-icon { 67 | font-size: 10 * @hd; 68 | display: flex; 69 | align-items: center; 70 | padding-left: 5 * @hd; 71 | } 72 | } 73 | 74 | .@{prefixClsExtends} { 75 | &-animation { 76 | display: none; 77 | } 78 | &-entered { 79 | display: block; 80 | } 81 | &-body { 82 | margin-top: 12 * @hd; 83 | &-enter { 84 | .@{prefixClsExtends}-animation { 85 | display: block; 86 | max-height: 0; 87 | } 88 | &-active { 89 | .@{prefixClsExtends}-animation { 90 | display: block; 91 | max-height: 100vh; 92 | transition: max-height 200ms linear; 93 | } 94 | } 95 | &-done { 96 | .@{prefixClsExtends}-animation { 97 | display: block; 98 | max-height: 100vh; 99 | } 100 | } 101 | } 102 | 103 | &-exit { 104 | .@{prefixClsExtends}-animation { 105 | display: block; 106 | max-height: 100vh; 107 | } 108 | &-active { 109 | .@{prefixClsExtends}-animation { 110 | display: block; 111 | max-height: 1px; 112 | transition: max-height 200ms linear; 113 | } 114 | } 115 | &-done { 116 | .@{prefixClsExtends}-animation { 117 | display: none; 118 | max-height: 0; 119 | } 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/components/NomarTextArea/demo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 基础 多行文本输入框 3 | * desc: 表单使用 demo 4 | */ 5 | import React, { FC } from 'react'; 6 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 7 | import DynamicForm, { 8 | useForm, 9 | Store, 10 | ValidateErrorEntity, 11 | DformTextArea, 12 | } from '@alitajs/dform'; 13 | // @ts-ignore 14 | import PhotoIcon from '../../../assets/photo.png'; 15 | 16 | const Page: FC = () => { 17 | const [form] = useForm(); 18 | const onFinish = (values: Store) => { 19 | // eslint-disable-next-line no-console 20 | console.log('Success:', values); 21 | }; 22 | 23 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 24 | // eslint-disable-next-line no-console 25 | console.log('Failed:', errorInfo); 26 | }; 27 | 28 | const photoImg = () => ( 29 | 30 | ); 31 | 32 | const formsValues = { 33 | textArea2: '只读,不可编辑', 34 | }; 35 | 36 | const formProps = { 37 | onFinish, 38 | onFinishFailed, 39 | formsValues, 40 | form, 41 | isDev: false, 42 | }; 43 | 44 | return ( 45 | <> 46 | 47 | 54 | 59 | 68 | 80 | 89 | 96 | 97 | 98 | 101 | 102 | ); 103 | }; 104 | 105 | export default Page; 106 | -------------------------------------------------------------------------------- /src/components/NomarSelect/tests/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Button, WhiteSpace } from 'antd-mobile-v2'; 3 | import DynamicForm, { IFormItemProps } from '../../../../'; 4 | import { useForm } from 'rc-field-form'; 5 | import DformSelect from '../../'; 6 | 7 | interface BasicProps { 8 | onFinish: any; 9 | onFinishFailed: any; 10 | } 11 | const Page: FC = ({ onFinish, onFinishFailed }) => { 12 | const [form] = useForm(); 13 | //选择季节 14 | const seasons = [ 15 | [ 16 | { 17 | label: '2013', 18 | value: '2013', 19 | }, 20 | { 21 | label: '2014', 22 | value: '2014', 23 | }, 24 | ], 25 | [ 26 | { 27 | label: '春', 28 | value: '春', 29 | }, 30 | { 31 | label: '夏', 32 | value: '夏', 33 | }, 34 | ], 35 | ]; 36 | //选择城市 37 | const citys = [ 38 | [ 39 | { 40 | label: '福州', 41 | value: '福州', 42 | }, 43 | { 44 | label: '厦门', 45 | value: '厦门', 46 | }, 47 | ], 48 | ]; 49 | 50 | const myProps = { 51 | type: 'select', 52 | fieldProps: 'userPicker1', 53 | title: '季节', 54 | placeholder: '请选择', 55 | data: seasons, 56 | }; 57 | const formsValues = { 58 | userPicker2: ['厦门'], 59 | // userPicker3: ['福州'], 60 | }; 61 | 62 | const formsData = [ 63 | { 64 | type: 'select', 65 | fieldProps: 'userPicker1', 66 | title: '季节', 67 | placeholder: '请选择', 68 | data: seasons, 69 | maxLine: 1, 70 | }, 71 | { 72 | type: 'select', 73 | fieldProps: 'userPicker2', 74 | required: true, 75 | title: '城市(不可编辑)', 76 | placeholder: '请选择', 77 | data: citys, 78 | disabled: true, 79 | maxLine: 1, 80 | }, 81 | ] as IFormItemProps[]; 82 | 83 | const formProps = { 84 | form, 85 | onFinish, 86 | onFinishFailed, 87 | formsValues, 88 | autoLineFeed: false, 89 | isDev: true, 90 | }; 91 | 92 | return ( 93 | <> 94 | 95 | 104 | 115 | 116 | 117 | 120 | 121 | ); 122 | }; 123 | 124 | export default Page; 125 | -------------------------------------------------------------------------------- /src/components/NomarInput/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Form from 'rc-field-form'; 3 | import { render, testA11y, fireEvent, waitFor } from '@alita/test'; 4 | import { DformInput } from '../../..'; 5 | import BasicText from './demos/basic'; 6 | import SingleText from './demos/single'; 7 | 8 | it('passes input a11y test', async () => { 9 | const { getByText, container } = render( 10 |
11 |
12 | 13 | 14 |
, 15 | ); 16 | fireEvent.click(getByText('用户名')); 17 | testA11y(container); 18 | }); 19 | 20 | test('render Basic', async () => { 21 | const onFinish = jest.fn(); 22 | const onFinishFailed = jest.fn(); 23 | const clickBlur = jest.fn(); 24 | 25 | const { getByText, getByLabelText, getByTestId } = render( 26 | , 31 | ); 32 | await waitFor(() => { 33 | fireEvent.click(getByText('submit')); 34 | }); 35 | expect(onFinishFailed).toBeCalled(); 36 | expect(getByLabelText('username')).toHaveValue(''); 37 | expect(getByLabelText('defaultValue')).toHaveValue('这是默认值'); 38 | // expect(getByLabelText('userAge')).toHaveValue('不可编辑'); 39 | fireEvent.change(getByLabelText('username'), { target: { value: '小明' } }); 40 | expect(getByLabelText('username')).toHaveValue('小明'); 41 | await waitFor(() => { 42 | fireEvent.click(getByText('submit')); 43 | }); 44 | expect(onFinish).toBeCalled(); 45 | expect(getByLabelText('userPwd')).toHaveValue(''); 46 | fireEvent.change(getByLabelText('userPwd'), { target: { value: '123456' } }); 47 | expect(getByLabelText('userPwd')).toHaveValue('123456'); 48 | expect(getByLabelText('userPwd')).toHaveAttribute('type', 'text'); 49 | await waitFor(() => { 50 | fireEvent.click(getByTestId('pwdId')); 51 | }); 52 | expect(getByLabelText('userPwd')).toHaveValue('654321'); 53 | expect(getByLabelText('userPwd')).toHaveAttribute('type', 'password'); 54 | fireEvent.change(getByLabelText('userBlur'), { target: { value: '1' } }); 55 | const blu = getByLabelText('userBlur'); 56 | blu.focus(); 57 | blu.blur(); 58 | expect(clickBlur).toBeCalled(); 59 | expect(getByText('身份证')).toBeDefined(); 60 | let username5 = getByLabelText('username5'); 61 | fireEvent.change(getByLabelText('username5'), { target: { value: 111 } }); 62 | fireEvent.change(getByLabelText('bankCard'), { 63 | target: { value: 'bankCard' }, 64 | }); 65 | fireEvent.change(getByLabelText('phone'), { target: { value: '1' } }); 66 | fireEvent.change(getByLabelText('phone'), { target: { value: '6666' } }); 67 | fireEvent.change(getByLabelText('phone'), { 68 | target: { value: '1468282282' }, 69 | }); 70 | fireEvent.change(getByLabelText('digit'), { target: { value: 'digit' } }); 71 | }); 72 | 73 | test('single basic', async () => { 74 | const { getByLabelText } = render(); 75 | expect(getByLabelText('a')).toHaveValue('1'); 76 | fireEvent.change(getByLabelText('a'), { target: { value: '小明' } }); 77 | expect(getByLabelText('a')).toHaveValue('小明'); 78 | }); 79 | --------------------------------------------------------------------------------