Cómo hacer componentes transparentes con Vue
Claramente no me refiero a bajarle la opacity
con CSS. Me refiero a hacer un componente que funcione como un elemento que ya conocemos, un <input>
por ejemplo. Para esto se necesita que la interfaz del componente sea similar a la del elemento que estamos simulando.
Aquí les cuento un poco sobre tres elementos que nos pueden ayudar para esto.
v-model
Elementos HTML como los <input>
, <select>
y <textarea>
funcionan directamente con v-model
para setear two-way data binding. Por esto, si queremos crear un componente que imite la funcionalidad de uno de ellos, es importante que este también permita el uso de esa directiva.
Para esto, nuestro componente debe cumplir dos cosas:
- Debe recibir una prop de nombre
value
y hacerle binding a un atributo del mismo nombre en el input - Debe emitir en algún momento el evento
input
con el valor que se quiere
Es decir, el componente debe tener un template que en su forma más simple se vería más o menos así:
<template>
<input
:value="value"
@input="event => $emit('input', event.target.value)"
>
</template>
$attrs
En Vue, los atributos que se le dan a un componente custom por defecto se aplican al elemento raíz del componente.
Sin embargo, a veces es deseable pasar estos atributos a un componente interno. $attrs
tiene la referencia a estos atributos, por lo que podemos usarlo para asignarlos al elemento HTML que queramos, usando v-bind="$attrs"
.
Sumado a lo anterior, podríamos tener un template para un custom-input
que se vea más o menos así:
<template>
<div>
...
<input
v-bind="$attrs"
:value="value"
@input="event => $emit('input', event.target.value)"
>
</div>
</template>
Y que se usa así:
<custom-input
v-model="someData"
name="someName"
autocomplete="off"
/>
En este ejemplo, los atributos name
y autocomplete
pasan a aplicarse al input gracias a la magia de v-bind="attrs"
🧙♂️.
$listeners
Similar a $attrs
, pero este tiene la referencia a los eventos que se definieron al instanciar mi componente. Se los podemos asignar a un elemento usando v-on="$listeners"
.
Importante: hay que tener en cuenta un detalle si se usa junto a lo mencionado de v-model
. v-on="$listeners"
sobreescribiría el @input
definido en nuestro componente. Para que esto no ocurra, se puede definir de la siguiente manera:
v-on="{
...this.$listeners,
input: event => $emit('input', event.target.value),
}"
Con todo lo anterior, el template de nuestro <custom-input>
quedaría de la siguiente manera:
<template>
<div>
...
<input
v-bind="$attrs"
:value="value"
v-on="{
...this.$listeners,
input: event => $emit('input', event.target.value),
}"
>
</div>
</template>
Ahora se le pueden pasar eventos al input:
<custom-input
v-model="someData"
name="someName"
autocomplete="off"
@hover="doSomething"
/>
Ejemplo
Hasta ahora no he dicho nada que no se pueda ver en los docs de Vue 👀. De hecho, acá explican lo de v-model, aquí lo de $listeners y por último lo de $attrs.
Veamos un ejemplo más concreto para que este post no sea solo una transcripción de esos docs.
Supongamos que tenemos varios inputs en nuestra aplicación y les queremos poner validaciones (indicar que un campo debe tener formato de email, por ejemplo). Además, queremos agregarles un ícono al input para indicar si es válido o tiene algún error.
Usaremos la librería vee-validate, específicamente su componente ValidationProvider.
El código de este componente se podría ver más o menos así, digamos en un validated-input.vue
:
<template>
<ValidationProvider
tag="div"
:rules="rules"
v-slot="{ errors, valid }"
>
<input
:value="value"
v-bind="$attrs"
v-on="inputListeners"
>
<img
v-if="errors[0]"
:src="require('../../assets/images/cross.png')"
>
<img
v-else-if="valid"
:src="require('../../assets/images/tick.png')"
/>
</ValidationProvider>
</template>
<script>
export default {
props: {
rules: { type: String, required: true },
value: { type: String, required: true },
},
computed: {
inputListeners() {
return {
...this.$listeners,
input: event => this.$emit('input', event.target.value),
};
},
},
};
</script>
Luego, lo queremos usar en otro componente. Digamos que queremos que cuando se le haga focus
a un input, los demás se vuelvan menos visibles. Para esto podríamos usarlo de esta manera:
<template>
...
<validated-input
v-model="name"
placeholder="Nombre"
rules="required"
@focus="() => focusedInput = 'name'"
@blur="() => focusedInput = null"
:class="{ 'transparent': focusedInput && focusedInput !== 'name' }"
/>
...
</template>
<script>
import ValidatedInput from './validated-input';
export default {
...
components: { ValidatedInput },
data() {
name: '',
focusedInput: null,
}
...
}
</script>
<style>
.transparent {
opacity: .25;
}
</style>
Agregando un poco de css para los colores y posicionamiento, esto se vería más o menos así:
Con esto se puede ver cómo el atributo placeholder
y los @focus
/@blur
se están asociando al <input>
dentro de nuestro <validated-input>
, así como el v-model
va guardando el valor ingresado en name
. Todo esto para lograr que nuestro componente sea realmente transparente.