selectField.tsx 4.5 KB

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