materials

Profundizando en Vue

Tabla de contenidos

Computed

Cuando se crea un componente de Vue (o el componente raíz) se le pasa como parámetro un objeto con las opciones con que se creará. Entre ellas tenemos props, _ data_, methods, y también otras como computed y watch.

Hemos visto que en una interpolación o directiva podemos poner una expresión javascript. Pero si la expresión es demasiado compleja hace que nuestro HTML sea más difícil de leer. La solución es crear una expresión calculada que nos permite tener “limpio” el HTML. Por ejemplo un código con expresiones complejas como:

<template>
  <p>Autor: { { author.name + ' ' + author.surname }}</p>
  <p>Ha publicado libros: { { author.books.length > 0 ? 'Sí' : 'No' }}</p>
</template>

<script>
export default {
  name: 'author-item',
  data() {
    return {
      author: {
        name: 'John',
        surname: 'Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  }
}
</script>

se puede simplificar creando propiedades calculadas:

<template>
  <p>Autor: { { fullName }}</p>
  <p>Ha publicado libros: { { hasPublished }}</p>
</template>

<script>
export default {
  name: 'author-item',
  data() {
    return {
      author: {
        name: 'John',
        surname: 'Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    fullName() {
      return this.name + ' ' + this.surname;
    },
    hasPublished() {
      return this.author.books.length > 0 ? '' : 'No'
    }
  }
})
</script>

En lugar de definir computed podríamos haber obtenido el mismo resultado usando métodos, pero la ventaja de las propiedades calculadas es que se cachean por lo que si se vuelven a tener que renderizar en el DOM no vuelven a evaluarse, a menos que cambie el valor de alguna de las variables reactivas que use.

Haz el ejercicio del tutorial de Vue.js

Por defecto las propiedades computed sólo hacen un getter, por lo que no se puede cambiar su valor. Pero podemos si queremos hacerlo definir métodos getter y setter:

  computed: {
    fullName:
      // getter
      get() {
        return this.name + ' ' + this.surname;
      },
      // setter
      set(newValue) {
        const names = newValue.split(' ');
        this.name = names[0];
        this.surname = names[names.length - 1];
      }
    },
  },
})

Si hacemos this.fullName = 'John Doe' estaremos asignando los valores adecuados a las variables name y surname.

Watchers

Vue proporciona una forma genérica de controlar cuándo cambia el valor de una variable reactiva para poder ejecutar código en ese momento poniéndole un watch:

  data() {
    return {
      name: 'John',
      surname: 'Doe',
      fullName: 'John Doe',
    }
  },
  watch: {
    name(newValue, oldValue) {
      this.fullName = newValue + ' ' + this.surname;
    },
    surname(newValue, oldValue) {
      this.fullName = this.name + ' ' + newValue;
    },
  },
})

En este caso no tiene mucho sentido y es más fácil (y más eficiente) usar una propiedad computed como hemos visto antes, pero hay ocasiones en que necesitamos ejecutar código al cambiar una variable y es así donde se usan. Veremos su utilidad cuando trabajemos con vue-router.

NOTA: los watcher son costosos por lo que no debemos abusar de ellos

Haz el ejercicio del tutorial de Vue.js

Acceder al DOM: ‘ref’

Aunque Vue se encarga de la vista por nosotros en alguna ocasión podemos tener que acceder a un elemento del DOM. En ese caso no haremos un document.getElement... sino que le ponemos una referencia al elemento con el atributo ref para poder acceder al mismo desde nuestro script:

<template>
  <form ref="myForm">
    ...
  </form>
</template>

<script>
export default {
  mounted() {
    this.$refs.myForm.setAttribute('novalidate', true)
  }
}
</script>

Desde el código tenemos acceso a todas las referencias desde this.$refs. Hay que tener en cuenta que sólo se puede acceder a un elemento después de montarse el componente (en el hook mounted() o después).

Haz el ejercicio del tutorial de Vue.js

nextTick

Si modificamos una variable reactiva el cambio se refleja automáticamente en el DOM, pero no inmediatamente sino que se espera hasta el evento nextTick en el ciclo de modificación para asegurarse de no cambiar algo que quizá va a volverse a cambiar en este ciclo.

Si accedemos al DOM antes de que se produzca este evento el valor aún será el antiguo. Para obtener el nuevo valor hemos de esperar al nextTick:

<template>
  <p>Contador: <span ref="contador">{ { count }}</span></p>
  <button @click="increment">Incrementa</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      count.value++
      console.log('Contador en el DOM: ' + this.$refs.contador.textContent)
      // Devolverá el valor sin actualizar aún
      nextTick(() => {
        console.log('Contador en el DOM tras nextTick: ' + this.$refs.contador.textContent)
        // Devolverá el valor actualizado
      })
    }
  }
}
</script>

Realmente es algo que seguramente nunca necesitemos pero así conocemos un poco más cómo funciona Vue internamente.

Clases HTML

Ya hemos visto que en Javascript usamos las clases con mucha frecuencia, normalmente para asignar a elementos estilos definidos en el CSS, pero también para identificar elementos sin usar una id (como hacíamos poniendo a los botones de acciones de los productos las clases subir, bajar, editar o borrar).

En Vue tenemos diferentes formas de asignar clases. La más simple sería bindear el atributo class y gestionarlas directamente en el código, pero no es lo más cómodo:

<div :class="clasesDelDiv"></div>

En este caso tendríamos que asignar a la variables clasesDelDiv las diferentes clases separadas por espacio, lo que es engorroso de mantener.

Sintaxis de objeto

Una forma más sencilla es bindear un objeto donde cada propiedad es el nombre de una posible clase y su valor es un booleano que indica si tendrá o no dicha clase, por ejemplo:

<div 
    class="static"
    :class="{ active: isActive, 'text-danger': hasError }"
></div>

En este caso el <DIV> tendrá las clases:

Para mejorar la legibilidad del HTML podemos poner el objeto de las clases en el Javascript

<div 
    class="static"
    :class="classObject"
></div>
data() {
  return {
    classObject: {
      active: true,
      'text-danger': false
    }
  }
}

Sintaxis de array

Podemos indicar las clases en forma de array de variables que contienen la clase a asignar:

<div :class="[activeClass, errorClass]"></div>
data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}

En este caso el <DIV> tendrá las clases active y text-danger.

Y es posible incluir sintaxis de objeto dentro de la sintaxis de array:

<div :class="[{ active: isActive}, errorClass]"></div>

Asignar clases a un componente

En la etiqueta de un componente podemos ponerle un atributo class que le asignará las clases incluidas y que se sumaran a las que se le asignen dentro del propio componente. Por ejemplo, si el <DIV> del ejemplo anterior es el template de un componente llamado MyComponent puedo poner:

<my-component class="main highligth"></my-component>

En este caso el <DIV> tendrá las clases main, highligth, active si la variable isActive vale true y text-danger.

En Vue3 el template de un componente puede tener varios elementos raíz. En ese caso para indicar a cuál se aplicarán las clases definidas en el padre se usa la propiedad $attr.class:

<template>
    <p :class="$attrs.class">Hi!</p>
    <span>This is a child component</span>
</template>

Asignar estilos directamente

Aunque no es lo recomendable, podemos asignar directamente estilos CSS igual que asignamos clases y también podemos usar la sintaxis de objeto o la de array.

<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() {
  return {
    activeColor: 'red',
    fontSize: 30'
  }
}

Ciclo de vida del componente

El ciclo de vida de un componente

Al crearse la instancia de Vue o un componente la aplicación debe realizar unas tareas como configurar la observación de variables, compilar su plantilla (template), montarla en el DOM o reaccionar ante cambios en las variables volviendo a renderizar las partes del DOM que han cambiado. Además ejecuta funciones definidas por el usuario cuando sucede alguno de estos eventos, llamadas hooks del ciclo de vida.

En la siguiente imagen podéis ver el ciclo de vida de la instancia Vue (y de cualquier componente) y los eventos que se generan y que podemos interceptar:

Ciclo de vida de Vue

NOTA: En Vue2: los métodos beforeDestroyed y destroyed se usan en lugar de beforeUnmounted y unmounted.

IMPORTANTE: no debemos definir estas funciones como arrow functions porque en estas funciones se enlaza en la variable this el componente donde se definen y si hacemos una arrow function no tendríamos this:

// MAL, NO HACER ASÍ
created: () => {
    console.log('instancia creada'); 
}
// BIEN, HACER ASÍ
created() {
    console.log('instancia creada'); 
}

Los principales hooks son:

Haz el ejercicio del tutorial de Vue.js

Componentes asíncronos

En proyectos grandes con centenares de componentes podemos hacer que en cada momento se carguen sólo los componentes necesarios de manera que se ahorra mucho tiempo de carga de la página.

Para que un componente se cargue asíncronamente al registrarlo se hace como un objeto que será una función que importe el componente. Un componente normal (síncrono) se registraría así:

<script>
import ProductItem from './ProductItem.vue'

export default {
    name: 'products-table',
    components: {
        ProductItem,
    },
...
}
</script>

Si queremos que se cargue asíncronamente no lo importamos hasta se registra:

<script>
export default {
    name: 'products-table',
    components: {
        ProductItem: () => import('./ProductItem.vue'),
    },
...
}
</script>

También podemos decirle que espere un tiempo a cargar el componente (delay) e incluso qué componente queremos cargar mientras está cargando el componente o cuál cargar si hay un error al cargarlo:

<script>
export default {
    name: 'products-table',
    components: {
        ProductItem: () => ({
            component: import('./ProductItem.vue'),
            delay: 500,       // en milisegundos
            timeout: 6000,
            loading: compLoading,   // componente que cargará mientras se está cargando
            error: compError,       // componente que cargará si hay un error,
        })
    },
...
}
</script>

Custom Directives

Podemos crear nuestras propias directivas para usar en los elementos que queramos. Se definen en un fichero .js con Vue.directive y le pasamos su nombre y un objeto con los estados en que queremos que reaccione. Por ejemplo vamos a hacer una directiva para que se le asigne el foco al elemento al que se la pongamos, que será de tipo input:

import Vue from 'vue'

Vue.directive('focus', {
  mounted(el) {
    el.focus();
  }
})

Para usarla en un componente la importamos y ya podemos usarla en el template:

<template>
  ...
  <input v-focus type="text" name="nombre">
  ...
</template>

<script>
import focus from './focus.js'
...
}
</script>

Si queremos utilizarla en muchos componentes podemos importarla en el main.js y así estará disponible para todos los componentes.

Los estados de la directiva en los que podemos actuar son:

Imágenes

Si se trata de imágenes estáticas lo más sencillo es ponerlas dentro de la carpeta public y hacer referencia a ellas usando ruta absoluta. Todo lo que está en public se referencia como si estuviera en la raíz de nuestra aplicación:

  <img src="/img/elPatitoFeo.jpeg" height="100px" alt="El Patito Feo">

También podemos poner las imágenes en la carpeta assets, pero antes de usarlas deberemos imnportarlas. Ejemplo:

<script>
  import imgUrl from './assets/img/elPatitoFeo.jpeg'
  ...
</script>

<template>
  ...
  <img :src="imgUrl" height="100px" alt="El Patito Feo">
  ...
</template>

NOTA: Si usamos webpack en lugar de Vite, en lugar de importarlas usaremos en su atributo src la función require con la URL de la imagen:

  <img :src="require('../assets/img/elPatitoFeo.jpeg')" height="100px" alt="El Patito Feo">

Con Vite también podemos importarlas usando import.meta.url (más información en la documentación de Vite):

<script>
export default {
  data() {
    return {
      imgUrl = new URL('./assets/elPatitoFeo.png', import.meta.url).href
    }
  },
  ...
}
</script>

<template>
  ...
  <img :src="imgUrl" height="100px" alt="El Patito Feo">
  ...
</template>

Esto nos permite también importar las imágenes dinámicamente:

<script>
export default {
  methods: {
    function getImageUrl(name) {
      return new URL(`./dir/${name}`, import.meta.url).href
    }
  }
  ...
}
</script>

<template>
  ...
  <img :src="getImageUrl(imgName)" height="100px">
  ...
</template>

Esto permitiría mostrar también imágenes obtenidas de una API.

Transiciones

Vue permite controlar transiciones en nuestra aplicación poniendo el código CSS correspondiente y añadiéndole al elemento el atributo transition. Podemos encontrar más información en la documentación oficial de Vue.

Entornos

En Vue tenemos normalmente 3 entornos o modos, el de development, el de test y el de production. Las variables de entorno las guardaremos en uno de los siguientes ficheros:

En contenido de estos ficheros son variables en forma clave=valor:

// fichero .env
TITULO=Mi proyecto
VITE_API=https://localhost/api

Si el nombre de la variable comienza por VITE_ será accesible desde el código a través de import.meta.env.nombreVariable:

// <script> de componente
console.log(process.env.VITE_API);

Podemos saber en qué entorno se está ejecutando la aplicación consultando el valor de la variable import.meta.env.MODE.

Si no estamos usando Vite sino webpack el nombre de las variables debe comenzar por VUE_APP_ y será accesible desde el código con process.env.nombreVariable:

// <script> de componente
console.log(process.env.VUE_APP_API);

Guards del router

Son hooks que podemos controlar en distintos momentos, algunos desde el componente y otros desde el router. Podemos ponerlos para todas las rutas, para una ruta en concreto o en el componente.

La mayoría reciben 3 parámetros:

En el router tenemos estos guards:

Para aplicarlos en nuestro router lo asignamos a una variable que exportamos:

let router = new Router({
  routes: [
    {
      path: '/',
      component: 'MyComponent',
      beforeEnter(to, from, next) {
        console.log('Vengo de ' + from + ' y voy a ' + to);
        next();
      },
...
})

router.beforeEach(to, from, next) {
  console.log('Vengo de ' + from + ' y voy a ' + to);
  next();
}

export default router

En un componente también puedo definir los hooks: