baseForm.tsx 12 KB

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