baseForm.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /* eslint-disable prefer-template, max-len, @typescript-eslint/no-unused-vars */
  2. import React from 'react';
  3. import classNames from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import FormFoundation, { BaseFormAdapter } from '@douyinfe/semi-foundation/form/foundation';
  6. import { strings, cssClasses } from '@douyinfe/semi-foundation/form/constants';
  7. import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
  8. import warning from '@douyinfe/semi-foundation/utils/warning';
  9. import BaseComponent from '../_base/baseComponent';
  10. import { FormStateContext, FormApiContext, FormUpdaterContext } from './context';
  11. import { isEmptyChildren } from '../_base/reactUtils';
  12. import Row from '../grid/row';
  13. import { cloneDeep } from '../_utils/index';
  14. import Slot from './slot';
  15. import Section from './section';
  16. import Label from './label';
  17. import ErrorMessage from './errorMessage';
  18. import FormInputGroup from './group';
  19. import { noop } from 'lodash';
  20. import '@douyinfe/semi-foundation/form/form.scss';
  21. import {
  22. FormInput,
  23. FormInputNumber,
  24. FormTextArea,
  25. FormSelect,
  26. FormCheckboxGroup,
  27. FormCheckbox,
  28. FormRadioGroup,
  29. FormRadio,
  30. FormDatePicker,
  31. FormSwitch,
  32. FormSlider,
  33. FormTimePicker,
  34. FormTreeSelect,
  35. FormCascader,
  36. FormRating,
  37. FormAutoComplete,
  38. FormUpload,
  39. FormTagInput } from './field';
  40. import {
  41. BaseFormProps,
  42. FormState,
  43. FormApi,
  44. ErrorMsg
  45. } from './interface';
  46. const prefix = cssClasses.PREFIX;
  47. interface BaseFormState {
  48. formId: string;
  49. }
  50. class Form extends BaseComponent<BaseFormProps, BaseFormState> {
  51. static propTypes = {
  52. 'aria-label': PropTypes.string,
  53. onSubmit: PropTypes.func,
  54. onSubmitFail: PropTypes.func,
  55. /* Triggered from update, including field mount/unmount/value change/blur/verification status change/error prompt change, input parameter is formState, currentField */
  56. onChange: PropTypes.func,
  57. onReset: PropTypes.func,
  58. // Triggered when the value of the form is updated, only when the value of the subfield changes. The entry parameter is formState.values
  59. onValueChange: PropTypes.func,
  60. initValues: PropTypes.object,
  61. getFormApi: PropTypes.func,
  62. component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  63. render: PropTypes.func,
  64. validateFields: PropTypes.func,
  65. style: PropTypes.object,
  66. className: PropTypes.string,
  67. layout: PropTypes.oneOf(strings.LAYOUT),
  68. labelPosition: PropTypes.oneOf(strings.LABEL_POS),
  69. labelWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  70. labelAlign: PropTypes.oneOf(strings.LABEL_ALIGN),
  71. labelCol: PropTypes.object, // Control labelCol {span: number, offset: number} for all field child nodes
  72. wrapperCol: PropTypes.object, // Control wrapperCol {span: number, offset: number} for all field child nodes
  73. allowEmpty: PropTypes.bool,
  74. autoScrollToError: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
  75. disabled: PropTypes.bool,
  76. showValidateIcon: PropTypes.bool,
  77. extraTextPosition: PropTypes.oneOf(strings.EXTRA_POS),
  78. id: PropTypes.string,
  79. };
  80. static defaultProps = {
  81. onChange: noop,
  82. onSubmitFail: noop,
  83. onSubmit: noop,
  84. onReset: noop,
  85. onValueChange: noop,
  86. layout: 'vertical',
  87. labelPosition: 'top',
  88. allowEmpty: false,
  89. autoScrollToError: false,
  90. showValidateIcon: true,
  91. };
  92. static Input = FormInput;
  93. static TextArea = FormTextArea;
  94. static InputNumber = FormInputNumber;
  95. static Select = FormSelect;
  96. static Checkbox = FormCheckbox;
  97. static CheckboxGroup = FormCheckboxGroup;
  98. static Radio = FormRadio;
  99. static RadioGroup = FormRadioGroup;
  100. static DatePicker = FormDatePicker;
  101. static TimePicker = FormTimePicker;
  102. static Switch = FormSwitch;
  103. static Slider = FormSlider;
  104. static TreeSelect = FormTreeSelect;
  105. static Cascader = FormCascader;
  106. static Rating = FormRating;
  107. static AutoComplete = FormAutoComplete;
  108. static Upload = FormUpload;
  109. static TagInput = FormTagInput;
  110. static Slot = Slot;
  111. static ErrorMessage = ErrorMessage;
  112. static InputGroup = FormInputGroup;
  113. static Label = Label;
  114. static Section = Section;
  115. formApi: FormApi;
  116. constructor(props: BaseFormProps) {
  117. super(props);
  118. this.state = {
  119. formId: '',
  120. };
  121. warning(
  122. Boolean(props.component && props.render),
  123. '[Semi Form] You should not use <Form component> and <Form render> in ths same time; <Form render> will be ignored'
  124. );
  125. warning(
  126. props.component && props.children && !isEmptyChildren(props.children),
  127. '[Semi Form] You should not use <Form component> and <Form>{children}</Form> in ths same time; <Form>{children}</Form> will be ignored'
  128. );
  129. warning(
  130. props.render && props.children && !isEmptyChildren(props.children),
  131. '[Semi Form] You should not use <Form render> and <Form>{children}</Form> in ths same time; <Form>{children}</Form> will be ignored'
  132. );
  133. this.submit = this.submit.bind(this);
  134. this.reset = this.reset.bind(this);
  135. this.foundation = new FormFoundation(this.adapter);
  136. this.formApi = this.foundation.getFormApi();
  137. if (this.props.getFormApi) {
  138. this.props.getFormApi(this.formApi);
  139. }
  140. }
  141. componentDidMount() {
  142. this.foundation.init();
  143. }
  144. componentWillUnmount() {
  145. this.foundation.destroy();
  146. this.formApi = null;
  147. }
  148. get adapter(): BaseFormAdapter<BaseFormProps, BaseFormState> {
  149. return {
  150. ...super.adapter,
  151. cloneDeep,
  152. notifySubmit: (values: any) => {
  153. this.props.onSubmit(values);
  154. },
  155. notifySubmitFail: (errors: ErrorMsg, values: any) => {
  156. this.props.onSubmitFail(errors, values);
  157. },
  158. forceUpdate: (callback?: () => void) => {
  159. this.forceUpdate(callback);
  160. },
  161. notifyChange: (formState: FormState) => {
  162. this.props.onChange(formState);
  163. },
  164. notifyValueChange: (values: any, changedValues: any) => {
  165. this.props.onValueChange(values, changedValues);
  166. },
  167. notifyReset: () => {
  168. this.props.onReset();
  169. },
  170. initFormId: () => {
  171. this.setState({
  172. formId: getUuidv4()
  173. });
  174. },
  175. getInitValues: () => this.props.initValues,
  176. getFormProps: (keys: undefined | string | Array<string>) => {
  177. if (typeof keys === 'undefined') {
  178. return this.props;
  179. } else if (typeof keys === 'string') {
  180. return this.props[keys];
  181. } else {
  182. const props = {};
  183. keys.forEach(key => {
  184. props[key] = this.props[key];
  185. });
  186. return props;
  187. }
  188. },
  189. getAllErrorDOM: () => {
  190. const { formId } = this.state;
  191. return document.querySelectorAll(
  192. `form[x-form-id="${formId}"] .${cssClasses.PREFIX}-field-error-message`
  193. );
  194. },
  195. getFieldDOM: (field: string) =>
  196. document.querySelector(`.${cssClasses.PREFIX}-field[x-field-id="${field}"]`),
  197. };
  198. }
  199. get content() {
  200. const { children, component, render } = this.props;
  201. const formState = this.foundation.getFormState();
  202. const props = {
  203. formState,
  204. formApi: this.foundation.getFormApi(),
  205. values: formState.values,
  206. };
  207. if (component) {
  208. return React.createElement(component, props);
  209. }
  210. if (render) {
  211. return render(props);
  212. }
  213. if (typeof children === 'function') {
  214. return children(props);
  215. }
  216. return children;
  217. }
  218. submit(e: React.FormEvent<HTMLFormElement>) {
  219. e.preventDefault();
  220. this.foundation.submit();
  221. }
  222. reset(e: React.FormEvent<HTMLFormElement>) {
  223. e.preventDefault();
  224. this.foundation.reset();
  225. }
  226. render() {
  227. const needClone = false;
  228. const formState = this.foundation.getFormState(needClone);
  229. const updaterApi = this.foundation.getModifyFormStateApi();
  230. const { formId } = this.state;
  231. const {
  232. children,
  233. getFormApi,
  234. onChange,
  235. onSubmit,
  236. onSubmitFail,
  237. onValueChange,
  238. component,
  239. render,
  240. validateFields,
  241. initValues,
  242. layout,
  243. style,
  244. className,
  245. labelPosition,
  246. labelWidth,
  247. labelAlign,
  248. labelCol,
  249. wrapperCol,
  250. allowEmpty,
  251. autoScrollToError,
  252. showValidateIcon,
  253. extraTextPosition,
  254. ...rest
  255. } = this.props;
  256. const formCls = classNames(prefix, className, {
  257. [prefix + '-vertical']: layout === 'vertical',
  258. [prefix + '-horizontal']: layout === 'horizontal',
  259. });
  260. const showldAppendRow = wrapperCol && labelCol;
  261. const formContent = (
  262. <form
  263. style={style}
  264. {...rest}
  265. onReset={this.reset}
  266. onSubmit={this.submit}
  267. className={formCls}
  268. x-form-id={formId}
  269. >
  270. {this.content}
  271. </form>
  272. );
  273. const withRowForm = <Row>{formContent}</Row>;
  274. return (
  275. <FormUpdaterContext.Provider value={updaterApi}>
  276. <FormApiContext.Provider value={this.formApi}>
  277. <FormStateContext.Provider value={formState}>
  278. {showldAppendRow ? withRowForm : formContent}
  279. </FormStateContext.Provider>
  280. </FormApiContext.Provider>
  281. </FormUpdaterContext.Provider>
  282. );
  283. }
  284. }
  285. export default Form;