form-select.js 4.49 KB
import { extend } from '../../vue'
import { NAME_FORM_SELECT } from '../../constants/components'
import { EVENT_NAME_CHANGE } from '../../constants/events'
import {
  PROP_TYPE_BOOLEAN,
  PROP_TYPE_BOOLEAN_STRING,
  PROP_TYPE_NUMBER
} from '../../constants/props'
import { SLOT_NAME_FIRST } from '../../constants/slots'
import { from as arrayFrom } from '../../utils/array'
import { attemptBlur, attemptFocus } from '../../utils/dom'
import { htmlOrText } from '../../utils/html'
import { isArray } from '../../utils/inspect'
import { sortKeys } from '../../utils/object'
import { makeProp, makePropsConfigurable } from '../../utils/props'
import { formControlMixin, props as formControlProps } from '../../mixins/form-control'
import { formCustomMixin, props as formCustomProps } from '../../mixins/form-custom'
import { formSizeMixin, props as formSizeProps } from '../../mixins/form-size'
import { formStateMixin, props as formStateProps } from '../../mixins/form-state'
import { idMixin, props as idProps } from '../../mixins/id'
import {
  MODEL_EVENT_NAME,
  MODEL_PROP_NAME,
  modelMixin,
  props as modelProps
} from '../../mixins/model'
import { normalizeSlotMixin } from '../../mixins/normalize-slot'
import { optionsMixin } from './helpers/mixin-options'
import { BFormSelectOption } from './form-select-option'
import { BFormSelectOptionGroup } from './form-select-option-group'

// --- Props ---

export const props = makePropsConfigurable(
  sortKeys({
    ...idProps,
    ...modelProps,
    ...formControlProps,
    ...formCustomProps,
    ...formSizeProps,
    ...formStateProps,
    ariaInvalid: makeProp(PROP_TYPE_BOOLEAN_STRING, false),
    multiple: makeProp(PROP_TYPE_BOOLEAN, false),
    // Browsers default size to `0`, which shows 4 rows in most browsers in multiple mode
    // Size of `1` can bork out Firefox
    selectSize: makeProp(PROP_TYPE_NUMBER, 0)
  }),
  NAME_FORM_SELECT
)

// --- Main component ---

// @vue/component
export const BFormSelect = /*#__PURE__*/ extend({
  name: NAME_FORM_SELECT,
  mixins: [
    idMixin,
    modelMixin,
    formControlMixin,
    formSizeMixin,
    formStateMixin,
    formCustomMixin,
    optionsMixin,
    normalizeSlotMixin
  ],
  props,
  data() {
    return {
      localValue: this[MODEL_PROP_NAME]
    }
  },
  computed: {
    computedSelectSize() {
      // Custom selects with a size of zero causes the arrows to be hidden,
      // so dont render the size attribute in this case
      return !this.plain && this.selectSize === 0 ? null : this.selectSize
    },
    inputClass() {
      return [
        this.plain ? 'form-control' : 'custom-select',
        this.size && this.plain ? `form-control-${this.size}` : null,
        this.size && !this.plain ? `custom-select-${this.size}` : null,
        this.stateClass
      ]
    }
  },
  watch: {
    value(newValue) {
      this.localValue = newValue
    },
    localValue() {
      this.$emit(MODEL_EVENT_NAME, this.localValue)
    }
  },
  methods: {
    focus() {
      attemptFocus(this.$refs.input)
    },
    blur() {
      attemptBlur(this.$refs.input)
    },
    onChange(event) {
      const { target } = event
      const selectedValue = arrayFrom(target.options)
        .filter(o => o.selected)
        .map(o => ('_value' in o ? o._value : o.value))

      this.localValue = target.multiple ? selectedValue : selectedValue[0]

      this.$nextTick(() => {
        this.$emit(EVENT_NAME_CHANGE, this.localValue)
      })
    }
  },
  render(h) {
    const { name, disabled, required, computedSelectSize: size, localValue: value } = this

    const $options = this.formOptions.map((option, index) => {
      const { value, label, options, disabled } = option
      const key = `option_${index}`

      return isArray(options)
        ? h(BFormSelectOptionGroup, { props: { label, options }, key })
        : h(BFormSelectOption, {
            props: { value, disabled },
            domProps: htmlOrText(option.html, option.text),
            key
          })
    })

    return h(
      'select',
      {
        class: this.inputClass,
        attrs: {
          id: this.safeId(),
          name,
          form: this.form || null,
          multiple: this.multiple || null,
          size,
          disabled,
          required,
          'aria-required': required ? 'true' : null,
          'aria-invalid': this.computedAriaInvalid
        },
        on: { change: this.onChange },
        directives: [{ name: 'model', value }],
        ref: 'input'
      },
      [this.normalizeSlot(SLOT_NAME_FIRST), $options, this.normalizeSlot()]
    )
  }
})