Este artículo fue escrito hace más de un año; en la actualidad utilizo la biblioteca Buefy, que resulta más práctica y mejor implementada, sugiero utilizar dicha biblioteca con una capa de personalización, que no tiene que ser tan extrema como la planteada en este artículo.
Si utilizas Vue.js seguramente sabrás que hacer componentes genéricos es una de las mejores inversiones de tiempo. Escribes código una vez (en el mejor de los casos) y las siguientes solo lo reutilizas.
Sin embargo crear un componente que funcione adecuadamente no siempre es sencillo, es por eso que escribo esta guía para hacer un select personalizado.
Lo primero que haremos es nombrar el componente. Según la guía de estilo de
Vue.js es recomendable nombrar los componentes de manera homogénea y se
sugiere que tengan un prefijo específico, por ejemplo Base
, App
o V
, en
mi caso nombro mis componentes base con el prefijo H
y B
para los
especificos del framework Bulma.
Creamos un componente para «envolver» el select. HField
que solo contiene
información para el usuario.
Y ahora pasamos al select:
Su estructura general es la siguiente:
<template>
<h-field>
<div>
<select>
<option></option>
<option></option>
</select>
</div>
<template>
<slot></slot>
</template>
</h-field>
</template>
<script></script>
Agregamos un nombre de referencia al HField
para poder agregarle los errores
en caso de que existan y también pasamos los datos que tienen que ver con
ellos.
<h-field
ref="field"
:label="label"
:errors="errors"
:display-errors="displayErrors"
:has-addons="hasAddons"
></h-field>
Nuestro select puede ser de tamaño completo y además puede tener errores. En
bulma utilizamos un div
para este fin.
<div
class="select"
:class="{ 'is-fullwidth': isFullwidth, 'is-danger': errorsActive }"
></div>
Agregamos también el select y utilizaremos el v-model
para que nuestra
selección se actualice en las dos direcciones (vista-modelo, modelo-vista). Y
agregamos todos los atributos que se coloquen en la etiqueta del componente
con v-bind="$attrs"
.
<select v-model="selectedItem" v-bind="$attrs"></select>
Tenemos una opción deshabilitada en donde colocaremos más información para el usuario, que además se elige por defecto.
<option :value="placeholderValue" disabled>{{ placeholder }}</option>
Y después agregamos todas las opciones que queremos que contenga nuestro select.
<option
v-for="(element, index) in elements"
:value="element[idProperty]"
:key="element[idProperty]"
>
{{ element[displayProperty] }}
</option>
Utilizaremos el atributo idProperty
porque nuestros datos pueden no ser
homogéneos y también displayProperty
por la misma razón, en todo caso
imprimimos los elementos y casi estamos listos para el script. Por último
agregamos el espacio donde pueden ir (o no), algunos anexos.
<template #addons>
<slot name="addons"></slot>
</template>
Ahora sí pasamos al código del script. Importamos el componente que creamos
hace unos momentos y después de exportar el nuevo componente (HSelect), y
nombrarlo, evitaremos que el elemento padre h-field
herede los atributos que
se coloquen en su etiqueta.
import HField from "./HField.vue";
export default {
name: "h-select",
inheritAttrs: false,
components: {
HField
},
Queremos saber si hay errores activos, cual es el elemento seleccionado y cuales son los elementos actuales, así los datos que tendrá nuestro componente son los siguientes:
data() {
return {
errorsActive: false,
selectedItem: "",
elements: []
};
},
Ahora vamos a «observar» 3 atributos y es aquí donde sucede algo importante en nuestro componente.
Lo primero es observar el value
esto lo hacemos porque si tenemos un
v-model
asociado a nuestro componente, queremos que el valor cambie cuando
actualizamos el modelo.
Después observamos el atributo que está conectado con nuestro select como su
v-model
, queremos seleccionar el valor por defecto en caso de que no se
encuentre el elemento. Por el contrario, si se encuentra, y es diferente al
dato que pasamos como «viejo» escondemos los errores. Además, disparamos dos
eventos, y esto es bastante importante, porque un select en Vue.js, dispara el
evento “change” y también el evento “input”, con esto lograremos que si existe
un v-model
se actualice al realizar un cambio en nuestro select.
Por último observamos el atributo elements
y así, cuando nuestros elementos
cambien, tendremos control del elemento seleccionado.
watch: {
value(newValue) {
this.selectedItem = newValue;
},
selectedItem(newValue) {
if (!this.elementsContain(newValue)) {
this.selectedItem = this.placeholderValue;
return;
}
if (newValue !== this.old) this.hideError();
this.$emit("change", newValue);
this.$emit("input", newValue);
},
elements(newValue) {
var exists = this.elementsContain(this.value ? this.value : this.old);
if (newValue && newValue.length === 1)
this.selectedItem = newValue[0][this.idProperty];
else
this.selectedItem = exists
? this.value
? this.value
: this.old
: this.placeholderValue;
}
},
Cuando creamos el componente, queremos asignar los elementos y si existen errores, mostrarlos.
created() {
this.elements = this.initialElements;
if (this.errors && this.errors.length > 0) {
this.errorsActive = true;
}
},
Tenemos los métodos que nos ayudarán a ocultar errores, revisar si existe un
elemento en nuestra lista y obtener el elemento seleccionado, además de uno
que solo compara que el parámetro no sea null
ni undefined
.
methods: {
hideError() {
if (this.$refs.field) this.$refs.field.errorsActive = false;
this.errorsActive = false;
},
elementsContain(itemId) {
if (!this.isSomething(itemId)) return false;
if (itemId.toString() === this.placeholderValue.toString())
return true;
return (
this.elements.find(
e => e[this.idProperty].toString() === itemId.toString()
) !== undefined
);
},
isSomething(element) {
return element !== undefined && element !== null;
},
getSelected() {
return this.elements.find(
e => e[this.idProperty].toString() === this.selectedItem.toString()
);
}
},
Y por último tenemos las propiedades, que grosso modo se ven así:
props: {
label: String,
placeholder: String,
initialElements: [Object, Array],
value: [String, Number],
old: [String, Number],
hasAddons: Boolean,
placeholderValue: String,
displayProperty: String,
idProperty: String,
displayErrors: Boolean,
isFullwidth: Boolean,
errors: Array,
}
Sin más aquí está el código del componente, queda mucho trabajo por hacer, pero sin duda simplifica mucho el trabajo de hacer un select en Laravel con Vue.js.
Este es un ejemplo de caso de uso.
<h-select
label="{{ __('areas.area') }}"
:initial-elements='@json($areas)'
placeholder="{{ __('areas.area') }}"
display-property="name"
name="area_id"
old="{{ old('area_id') }}"
:errors='@json($errors->get('area_id'))'
required>
</h-select>
Podemos ver que asignar una etiqueta, elementos en forma de json, un placeholder, ademas de una seleccion previa o «vieja» y mostrar errores se vuelve mucho más sencillo.
Espero que este código te sea de ayuda. Adiós 👋🏽