selectField.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import styled from '@emotion/styled';
  2. import SelectControl, {
  3. ControlProps,
  4. } from 'sentry/components/forms/controls/selectControl';
  5. import {defined} from 'sentry/utils';
  6. import {StyledForm} from './form';
  7. import FormField from './formField';
  8. type SelectProps = Omit<ControlProps, 'onChange' | 'name'>;
  9. type FormProps = FormField['props'];
  10. type Props = FormProps & SelectProps;
  11. export default class SelectField extends FormField<Props> {
  12. static defaultProps = {
  13. ...FormField.defaultProps,
  14. clearable: true,
  15. multiple: false,
  16. };
  17. UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
  18. const newError = this.getError(nextProps, nextContext);
  19. if (newError !== this.state.error) {
  20. this.setState({error: newError});
  21. }
  22. if (this.props.value !== nextProps.value || defined(nextContext.form)) {
  23. const newValue = this.getValue(nextProps, nextContext);
  24. // This is the only thing that is different from parent, we compare newValue against coerced value in state
  25. // To remain compatible with react-select, we need to store the option object that
  26. // includes `value` and `label`, but when we submit the format, we need to coerce it
  27. // to just return `value`. Also when field changes, it propagates the coerced value up
  28. const coercedValue = this.coerceValue(this.state.value);
  29. // newValue can be empty string because of `getValue`, while coerceValue needs to return null (to differentiate
  30. // empty string from cleared item). We could use `!=` to compare, but lets be a bit more explicit with strict equality
  31. //
  32. // This can happen when this is apart of a field, and it re-renders onChange for a different field,
  33. // there will be a mismatch between this component's state.value and `this.getValue` result above
  34. if (newValue !== coercedValue && !!newValue !== !!coercedValue) {
  35. this.setValue(newValue);
  36. }
  37. }
  38. }
  39. // Overriding this so that we can support `multi` fields through property
  40. getValue(props, context) {
  41. const form = (context || this.context || {}).form;
  42. props = props || this.props;
  43. // Don't use `isMultiple` here because we're taking props from args as well
  44. const defaultValue = this.isMultiple(props) ? [] : '';
  45. if (defined(props.value)) {
  46. return props.value;
  47. }
  48. if (form && form.data.hasOwnProperty(props.name)) {
  49. return defined(form.data[props.name]) ? form.data[props.name] : defaultValue;
  50. }
  51. return defined(props.defaultValue) ? props.defaultValue : defaultValue;
  52. }
  53. // We need this to get react-select's `Creatable` to work properly
  54. // Otherwise, when you hit "enter" to create a new item, the "selected value" does
  55. // not update with new value (and also new value is not displayed in dropdown)
  56. //
  57. // This is also needed to get `multi` select working since we need the {label, value} object
  58. // for react-select (but forms expect just the value to be propagated)
  59. coerceValue(value) {
  60. if (!value) {
  61. return '';
  62. }
  63. if (this.isMultiple()) {
  64. return value.map(v => v.value);
  65. }
  66. if (value.hasOwnProperty('value')) {
  67. return value.value;
  68. }
  69. return value;
  70. }
  71. isMultiple(props?) {
  72. props = props || this.props;
  73. // this is to maintain compatibility with the 'multi' prop
  74. return props.multi || props.multiple;
  75. }
  76. getClassName() {
  77. return '';
  78. }
  79. onChange = opt => {
  80. // Changing this will most likely break react-select (e.g. you won't be able to select
  81. // a menu option that is from an async request, or a multi select).
  82. this.setValue(opt);
  83. };
  84. getField() {
  85. const {
  86. options,
  87. clearable,
  88. creatable,
  89. choices,
  90. placeholder,
  91. disabled,
  92. name,
  93. isLoading,
  94. } = this.props;
  95. return (
  96. <StyledSelectControl
  97. creatable={creatable}
  98. inputId={this.getId()}
  99. choices={choices}
  100. options={options}
  101. placeholder={placeholder}
  102. disabled={disabled}
  103. value={this.state.value}
  104. onChange={this.onChange}
  105. clearable={clearable}
  106. multiple={this.isMultiple()}
  107. name={name}
  108. isLoading={isLoading}
  109. />
  110. );
  111. }
  112. }
  113. // This is to match other fields that are wrapped by a `div.control-group`
  114. const StyledSelectControl = styled(SelectControl)`
  115. ${StyledForm} &, .form-stacked & {
  116. .control-group & {
  117. margin-bottom: 0;
  118. }
  119. margin-bottom: 15px;
  120. }
  121. `;