baseForm.tsx 9.9 KB

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