# 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 |
| El valor |
| hint | String | `` | Establece un texto de ayuda para el campo. |
| value | String | String que almacenará los valores ingresados en el componente. | |
| required | Boolean |
| En valor |
| 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 En caso de que la consulta a la BD devuelva un objeto con propiedades del |
| args_items_fcn | Array |
| Establece los argumentos que recibirá la función provista en |
| items | Array |
| Array de objectos con la estructura: que determinará de cada opción del select, el texto a mostrar y el valor que tomará la propiedad Propiedades adicionales de los items:
|
| 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: |
| 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 | |
| label-attrs | Object |
| Establece los atributos a pasarse en al IrLabel del campo solo en el caso de que la propiedad |
| 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: En la propiedad En la propiedad En la propiedad En la propiedad Las clases aplicables para las filas de la tabla podrán encontrarse en Vuetify en los siguientes enlaces: - Colores `{color}` `text--{color}` |
| iconDropDown | Object |
| Establece el icono que se muestra en el botón de desplegar las opciones. El objeto debe contener la propiedad |
| itemImageProp | String |
| Especifica el nombre de la propiedad en los items que contiene la URL de la imagen a mostrar. |
| itemIconProp | String |
| Especifica el nombre de la propiedad en los items que contiene el nombre del icono a mostrar. |
| imageAttrs | Object |
| Configura los atributos para el avatar e imagen de los items en la lista desplegable. El objeto debe contener:
|
| itemIconAttrs | Object |
| 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 |
| Configura los atributos para el avatar e imagen del item seleccionado que se muestra en el campo. El objeto debe contener:
|
# 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>