# Componente

# Introducción

Componente que mostrará un input de tipo múltiples opciones o dropdown.

# Uso

Es importante recalcar el nombre del componente IrSelect a nivel de código, ya que este deberá usarse para llamar al componente para renderizarlo, ya sea individiualmente o en un IrForm.

# Ejemplo

# Propiedades

Propiedad Tipo Default Descripción
readonly Boolean

false

El valor readonly = true, establecerá al campo como solo lectura.

hint String

``

Establece un texto de ayuda para el campo.
value String

String que almacenará los valores ingresados en el componente.

required Boolean

false

En valor true, define que el componente debe ser valorizado para ser válido.

placeholder String

''

Establece el placeholder en el campo.

items_fcn String

''

Establece la función de base de datos a la cual se consultará para completar la información de la propiedad items. La consulta a la base de datos reemplazará lo contenido en la propiedad items, a menos que no se envíe la propiedad fcn o se envíe con el valor '', en dicho caso no se consultará a la BD y se utilizarán los datos provistos en items.

En caso de que la consulta a la BD devuelva un objeto con propiedades del IrSelect, las mismas se utilizarán para reemplazar las propiedades actuales. Si únicamente devuelve un arreglo, el mismo se utilizará para reemplazar los items.

args_items_fcn Array

''

Establece los argumentos que recibirá la función provista en items_fcn

items Array

[]

Array de objectos con la estructura:

[
  {
    text: String,
    value: Any type,
    icon: String, // Opcional - nombre del icono de Material Design Icons
    image: String // Opcional - URL de la imagen a mostrar
  }
]

que determinará de cada opción del select, el texto a mostrar y el valor que tomará la propiedad value al seleccionar dicha opción.

Propiedades adicionales de los items:

  • icon: (String, opcional, definido por propiedad del componente) Nombre del icono de Material Design Icons que se mostrará junto al texto del item. Por ejemplo: 'mdi-home', 'mdi-account', etc.

  • image: (String, opcional, definido por propiedad del componente) URL de la imagen que se mostrará en un avatar junto al texto del item. La imagen se mostrará como un avatar circular.

value Any

Propiedad que almacenará el value del item seleccionado.

linked String | Boolean false

Determina la actualización del formulario contenedor cuando se cambia de valor del componente. Valores posibles: 'replace' 'update' false true. En caso de ser update el formulario refrescará únicamente el campo modificado. Aquí se explica mejor su funcionamiento.

searchable Boolean false

Determina si se renderizará un campo de búsqueda para los items del componente.

icon-drop-down Object

Establece un icono para sustituir el de desplegar opciones. El objeto debe contar con la propiedad icon valuada con el nombre del icono y luego las propiedades de vuetify para v-icon que quieran aplicarse.

label-attrs Object

{}

Establece los atributos a pasarse en al IrLabel del campo solo en el caso de que la propiedad outlined tenga valor true

hint Object | String

''

Genera un texto de ayuda para el componente, que podrá ser o no, clickeable para navegar a un link determinado

Propiedad con estructura de objeto con los siguientes keys:

{
  link: String,
  text: String
  openNewTab:Boolean
  classes:String
}

En la propiedad text del objeto se determinará el texto que mostrará el hint, esta propiedad debe estar valorizada para que se muestre el hint.

En la propiedad link del objeto se determinará el link al cual navegará al clickear en el hint, si no se encuentra valorizada el hint no será clickeable, solo será texto.

En la propiedad openNewTab del objeto se determinará si al clickearse el hint con link valorizado, se navegará en una nueva pestaña. Solo es útil paracuando la propedad link del objeto está definida. Si no se encuentra definido, se navegará en la misma pestaña.

En la propiedad classes del objeto, se determinarán las clases que se aplicarán al hint, las clases serán separadas por un espacio, sirven para definir estilos. Se utilizarán principalmente clases de vuetify.

Las clases aplicables para las filas de la tabla podrán encontrarse en Vuetify en los siguientes enlaces:

- Colores `{color}` `text--{color}`

- Texto y tipografía

- Espaciado

iconDropDown Object

{icon: 'mdi-menu-down'}

Establece el icono que se muestra en el botón de desplegar las opciones. El objeto debe contener la propiedad icon con el nombre del icono y puede incluir otras propiedades de Vuetify para v-icon.

itemImageProp String

'image'

Especifica el nombre de la propiedad en los items que contiene la URL de la imagen a mostrar.

itemIconProp String

'icon'

Especifica el nombre de la propiedad en los items que contiene el nombre del icono a mostrar.

imageAttrs Object

{avatar: {size: 35}, image: {alt: ''}}

Configura los atributos para el avatar e imagen de los items en la lista desplegable. El objeto debe contener:

  • avatar: atributos para el componente v-avatar
  • image: atributos para el componente v-img
itemIconAttrs Object

{size: '30px'}

Configura los atributos para los iconos de los items en la lista desplegable. Puede incluir cualquier propiedad válida de v-icon de Vuetify.

selectionImageAttrs Object

{avatar: {size: 35}, image: {alt: ''}}

Configura los atributos para el avatar e imagen del item seleccionado que se muestra en el campo. El objeto debe contener:

  • avatar: atributos para el componente v-avatar
  • image: atributos para el componente v-img

# Estilo alternativo

Existe una manera de tener un estilo alternativo de IrSelect, el cual se renderiza sin bordes y estableciendo en la misma linea el input y su respectivo label. El funcionamiento es exactamente el mismo que ir-select pero en lugar de utilizarse una propiedad, debe llamarse como IrSelectInline. Ejemplo a continuación.

# Código fuente

<template>
  <ir-select-bone v-bind="$attrs" @updateValue="updateSelectValue" @update-loading="(status) => isLoadingItems = status" v-on="$listeners">
    <div
      slot-scope="{ selectItems, placeholder, rules, readonly , hideDetails, returnObject, linked, label, comportamientos,
      comportamientosPositions, id_resolver, makeQuery, search, isFilterVisible, originalItems }"
    >
      <ir-label
        v-if="isOutlined"
        :label="label"
        :error="hasError"
        :disabled="otherProps.disabled"
        :readonly="readonly"
        v-bind="otherProps['label-attrs']"
      />
      <v-select
        :ref="irSelectRef"
        v-model="localValue"
        :placeholder="placeholder"
        class="pt-0 align-center outlined-inner"
        :readonly="readonly"
        :rules="rules"
        :label="isOutlined ? undefined : label"
        :loading="isLoading"
        :items="selectItems"
        :disabled="readonly || otherProps.disabled || isLoading"
        :hide-details="hideDetails"
        :return-object="returnObject"
        :menu-props="menuProps"
        v-bind="getFilteredProps"
        @input="(e) => updateValue({ newValue: e, linked, makeQuery, comportamientos, items: originalItems })"
        @click="focusOnFilter(isFilterVisible)"
        @keydown.enter="focusOnFilter(isFilterVisible)"
        @keydown="handleKeyDown"
        @update:error="setError"
        v-on="$listeners"
      >
        <template v-if="isOutlined" #item="{ item , on , attrs}">
          <v-list-item :class="getItemClass(item.value, selectItems)" class="textPrimary--text" v-bind="attrs" v-on="on">
            <v-avatar v-if="item[itemImageProp]" v-bind="imageAttrs.avatar" class="mr-2">
              <v-img :src="item[itemImageProp]" v-bind="imageAttrs.image"></v-img>
            </v-avatar>
            <v-icon v-if="item[itemIconProp]" v-bind="itemIconAttrs">
              {{ item[itemIconProp] }}
            </v-icon>
            <span>{{item.text}}</span>
          </v-list-item>
        </template>
        <template v-else #item="{ item , on , attrs}">
          <v-list-item class="textPrimary--text" v-bind="attrs" v-on="on">
            <v-icon v-if="item[itemIconProp]" v-bind="itemIconAttrs">
              {{ item[itemIconProp] }}
            </v-icon>
            <v-avatar v-if="item[itemImageProp]" v-bind="imageAttrs.avatar" class="mr-2">
              <v-img :src="item[itemImageProp]" v-bind="imageAttrs.image"></v-img>
            </v-avatar>
            <span>{{item.text}}</span>
          </v-list-item>
        </template>
        <template #append>
          <div>
            <v-icon v-bind="iconDropDown">{{iconDropDown.icon}}</v-icon>
          </div>
        </template>
        <template v-if="isFilterVisible" #prepend-item>
          <div class="pt-2 mb-2 px-4 search-items">
            <v-text-field
              data-cy="filter"
              ref="filter"
              outlined
              small
              clearable
              hide-details
              dense
              @input="search"
              @keydown.enter="selectFromList({ linked, makeQuery, comportamientos, items: originalItems }, returnObject, selectItems)"
            >
              <template #append>
                <v-icon>mdi-magnify</v-icon>
              </template>
            </v-text-field>
          </div>
        </template>
        <template v-slot:message="{ message, key }">
          <div v-html="message"></div>
        </template>
        <template v-slot:[position] v-for="position, k in comportamientosPositions">
          <div class="d-flex align-center" style="gap:10px;" :key="'slot-comportamientos-select-'+k">
            <template v-for="comportamiento, i in comportamientos">
              <ir-comportamiento
                :key="'comportamiento-select-' + i"
                v-if="(typeof comportamiento.visible === 'undefined' || comportamiento.visible) && comportamiento.position === position"
                v-bind="comportamiento"
                :value="localValue"
                @changeFieldValue="(e) => updateValue({ newValue: e, linked, items: originalItems })"
                :fatherComponentName="selectComponentName"
                :current_id_resolver="id_resolver"
              />
            </template>
          </div>
        </template>
        <template v-if="readonly && !localValue" v-slot:prepend-inner>
          <div class="pr-2">
            <v-icon color="warning">mdi-alert-circle</v-icon>
          </div>
        </template>
        <template #selection="{item}">
          <slot name="selection" :item="item">
          <!-- Se agrega este slot que muestra lo mismo que el v-select original, pero es usado para IrInline poder modificar el slot del v-select -->
            <div class="d-flex align-center flex-grow-1">
              <v-icon v-if="item[itemIconProp]" v-bind="itemIconAttrs">
                {{ item[itemIconProp] }}
              </v-icon>
              <v-avatar v-if="item[itemImageProp]" v-bind="selectionImageAttrs.avatar" class="mr-2">
                <v-img :src="item[itemImageProp]" v-bind="selectionImageAttrs.image"></v-img>
              </v-avatar>
              <span class="v-select__selection--comma v-select__selection">{{item.text}}</span>
            </div>
          </slot>
        </template>
      </v-select>
    </div>
  </ir-select-bone>
</template>

<script>
import IrSelectBone from './IrSelectBone.vue'
import { selectComponentName, apiLinked } from '../../../constants'
import { setHintTag } from '../../../helpers/hint'
import focusAndScrollMixin from '../../../mixins/focusAndScroll'
import { isOutlined } from '../../../helpers/utils.js'
import { filterProps } from '../../../helpers/filterProps'
export default {
  name: selectComponentName,
  components: {
    IrSelectBone,
},
  mixins: [focusAndScrollMixin],
  props: {
    value: {
      type: undefined,
      required: true
    },
    loading: {
      type: Boolean,
      default: false
    },
    isFocused: {
      type: Boolean,
      default: false
    },
    iconDropDown: {
      type: Object,
      default: () => ({icon: 'mdi-menu-down'})
    },
    hint: {
      type: [String, Object],
      default: ''
    },
    itemImageProp: {
      type: String,
      default: 'image'
    },
    itemIconProp: {
      type: String,
      default: 'icon'
    },
    imageAttrs: {
      type: Object,
      default: () => ({
        avatar: { size: 35 },
        image: { alt: '' }
      })
    },
    itemIconAttrs: {
      type: Object,
      default: () => ({
       size: '30px'
      })
    },
    selectionImageAttrs: {
      type: Object,
      default: () => ({
        avatar: { size: 35 },
        image: { alt: '' }
      })
    },
  },
  inheritAttrs: false,
  data() {
    return {
      irSelectRef: selectComponentName,
      otherProps: {},
      localValue: '',
      selectComponentName: selectComponentName,
      isLoadingItems: false,
      hasError: false
    }
  },
  computed: {
    getFilteredProps() {
      return filterProps(this.$refs[this.irSelectRef], this.otherProps, ['data-cy'])
    },
    isLoading() {
      return this.loading || this.isLoadingItems
    },
    isOutlined(){
      return isOutlined(this.otherProps.outlined)
    },
    menuProps() {
      return {
        nudgeTop: '2px'
      }
    },
  },
  watch: {
    value: {
      handler: function (newValue) {
        this.localValue = newValue
        this.$emit('input', newValue)
      }
    }
  },
  mounted() {
    if (this.isFocused) {
      let element = this.$refs[this.irSelectRef].$el.querySelector('input')
      this.focusAndScroll(element)
    }
    this.localValue = this.value
    const hint = setHintTag(this.$attrs, this.hint)
    let aux = { ...this.$attrs }
    delete aux.hint
    if (hint) {
      aux = { ...aux, hint }
    }
    delete aux.placeholder
    delete aux.readonly
    delete aux.label
    delete aux.required
    delete aux.items
    delete aux.value
    delete aux.loading
    delete aux['return-object']
    this.otherProps = aux
  },
  methods: {
    /**
    * Maneja el evento de teclado keydown.
    *
    * @param {Event} event - El evento de teclado.
    */
    handleKeyDown(event) {
      if (event.key === 'Tab')
        return
      event.preventDefault() // evita que se escriba en el select
    },
    /**
    * Selecciona un elemento de la lista y actualiza el valor según la configuración.
    *
    * @param {Object} data - Datos para enviar a linked.
    * @param {boolean} returnObject - Indica si se debe asignar el item completo o solo su valor.
    * @param {Array} selectItems - Elementos filtrados.
    */
    selectFromList(data, returnObject, selectItems) {
      if (Array.isArray(selectItems) && selectItems.length === 1) {
        this.$refs[this.irSelectRef].isMenuActive = false
        this.$refs[this.irSelectRef].focus()
        const item = selectItems[0]
        this.updateValue({ ...data, newValue: (returnObject ? item : item.value) })
      }
    },
    focusOnFilter(isFilterVisible) {
      this.$nextTick(() => {
        if (isFilterVisible) {
          setTimeout(() => { // Única forma de que haga focus
            const filterTextField = this.$refs.filter
            if (filterTextField) {
                filterTextField.focus()
            }
          }, 500)
        }
      })
    },
    updateValue({ newValue, linked, makeQuery, comportamientos, items }) {
      if (comportamientos) {
        const apiLinkedComp = comportamientos.find(comp => comp.type === apiLinked)
        if ((typeof apiLinkedComp) !== 'undefined') {
          linked = false
          makeQuery(apiLinkedComp, newValue)
        }
      }
      this.localValue = newValue
      this.$emit('input', newValue)
      this.$nextTick(() => {
        if (linked)
          this.$emit('change-linked-value', { action: linked, campo: { type: selectComponentName, props: { ...this.$attrs.props, items } } })
      })
    },
    updateSelectValue(value) {
      this.localValue = value
      this.$emit('input', value)
    },
    setError(eventValue){
      this.hasError = eventValue
    },
    getItemClass(itemValue, selectItems){
      if(selectItems[selectItems.length - 1].value === itemValue){
        return 'item-outlined item-outlined--last'
      }
      if(selectItems[0].value === itemValue)
        return 'item-outlined item-outlined--first'
      return 'item-outlined'
    },
  }
}
</script>

<style lang="scss" scoped>
.item-outlined {
  border-bottom: 1px solid var(--input-border-color);
  width: calc(100% - 32px);
  padding: 5px 8px;
  margin: 0 16px;
  &--last {
    padding: 5px 8px 0 8px;
    &:before{
      border-radius: 0 0 4px 4px;
    }
  }
  &--first {
    &:before{
      border-radius: 4px 4px 0 0;
    }
  }
  & .v-list-item--active {
    font-weight: 700;
    &::before {
      background-color: var(--v-primary-base);
      opacity: 0.04;
    }
  }
}
.search-items {
  position: sticky;
  z-index: 100;
  background: white;
  top: 0px;
}

.item-outlined {
  .v-avatar {
    flex-shrink: 0;
  }
}
</style>
Last Updated: 10/14/2025, 1:11:11 PM