formField.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import classNames from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import {Context} from 'app/components/forms/form';
  6. import QuestionTooltip from 'app/components/questionTooltip';
  7. import {Meta} from 'app/types';
  8. import {defined} from 'app/utils';
  9. type Value = string | number | boolean;
  10. type DefaultProps = {
  11. required?: boolean;
  12. disabled?: boolean;
  13. hideErrorMessage?: boolean;
  14. };
  15. type FormFieldProps = DefaultProps & {
  16. name: string;
  17. style?: object;
  18. label?: React.ReactNode;
  19. defaultValue?: any;
  20. disabledReason?: string;
  21. help?: string | React.ReactNode;
  22. className?: string;
  23. onChange?: (value: Value) => void;
  24. error?: string;
  25. value?: Value;
  26. meta?: Meta;
  27. };
  28. type FormFieldState = {
  29. error: string | null;
  30. value: Value;
  31. };
  32. export default class FormField<
  33. Props extends FormFieldProps = FormFieldProps,
  34. State extends FormFieldState = FormFieldState
  35. > extends React.PureComponent<Props, State> {
  36. static contextTypes = {
  37. form: PropTypes.object,
  38. };
  39. static defaultProps: DefaultProps = {
  40. hideErrorMessage: false,
  41. disabled: false,
  42. required: false,
  43. };
  44. constructor(props: Props, context?: any) {
  45. super(props, context);
  46. this.state = {
  47. error: null,
  48. value: this.getValue(props, context),
  49. } as State;
  50. }
  51. componentDidMount() {}
  52. UNSAFE_componentWillReceiveProps(nextProps: Props, nextContext: Context) {
  53. const newError = this.getError(nextProps, nextContext);
  54. if (newError !== this.state.error) {
  55. this.setState({error: newError});
  56. }
  57. if (this.props.value !== nextProps.value || defined(nextContext.form)) {
  58. const newValue = this.getValue(nextProps, nextContext);
  59. if (newValue !== this.state.value) {
  60. this.setValue(newValue);
  61. }
  62. }
  63. }
  64. componentWillUnmount() {}
  65. getValue(props: Props, context: Context) {
  66. const form = (context || this.context || {}).form;
  67. props = props || this.props;
  68. if (defined(props.value)) {
  69. return props.value;
  70. }
  71. if (form && form.data.hasOwnProperty(props.name)) {
  72. return defined(form.data[props.name]) ? form.data[props.name] : '';
  73. }
  74. return defined(props.defaultValue) ? props.defaultValue : '';
  75. }
  76. getError(props: Props, context: Context) {
  77. const form = (context || this.context || {}).form;
  78. props = props || this.props;
  79. if (defined(props.error)) {
  80. return props.error;
  81. }
  82. return (form && form.errors[props.name]) || null;
  83. }
  84. getId() {
  85. return `id-${this.props.name}`;
  86. }
  87. coerceValue(value: any) {
  88. return value;
  89. }
  90. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  91. const value = e.target.value;
  92. this.setValue(value);
  93. };
  94. setValue = (value: Value) => {
  95. const form = (this.context || {}).form;
  96. this.setState(
  97. {
  98. value,
  99. },
  100. () => {
  101. const finalValue = this.coerceValue(this.state.value);
  102. this.props.onChange?.(finalValue);
  103. form?.onFieldChange(this.props.name, finalValue);
  104. }
  105. );
  106. };
  107. getField() {
  108. throw new Error('Must be implemented by child.');
  109. }
  110. getClassName(): string {
  111. throw new Error('Must be implemented by child.');
  112. }
  113. getFinalClassNames() {
  114. const {className, required} = this.props;
  115. const {error} = this.state;
  116. return classNames(className, this.getClassName(), {
  117. 'has-error': !!error,
  118. required,
  119. });
  120. }
  121. renderDisabledReason() {
  122. const {disabled, disabledReason} = this.props;
  123. if (!disabled) {
  124. return null;
  125. }
  126. if (!disabledReason) {
  127. return null;
  128. }
  129. return <QuestionTooltip title={disabledReason} position="top" size="sm" />;
  130. }
  131. render() {
  132. const {label, hideErrorMessage, help, style} = this.props;
  133. const {error} = this.state;
  134. const cx = this.getFinalClassNames();
  135. const shouldShowErrorMessage = error && !hideErrorMessage;
  136. return (
  137. <div style={style} className={cx}>
  138. <div className="controls">
  139. {label && (
  140. <label htmlFor={this.getId()} className="control-label">
  141. {label}
  142. </label>
  143. )}
  144. {this.getField()}
  145. {this.renderDisabledReason()}
  146. {defined(help) && <p className="help-block">{help}</p>}
  147. {shouldShowErrorMessage && <ErrorMessage>{error}</ErrorMessage>}
  148. </div>
  149. </div>
  150. );
  151. }
  152. }
  153. const ErrorMessage = styled('p')`
  154. font-size: ${p => p.theme.fontSizeMedium};
  155. color: ${p => p.theme.red300};
  156. `;