formField.tsx 4.4 KB

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