selectField.tsx 4.3 KB

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