Ir al contenido
wannacode

El primer componente

En resumen: Un componente de Vue es una plantilla con datos: describe cómo se ve la pantalla según esos datos, y Vue cambia en la página solo la diferencia.

Problema

El campo pierde el foco

El módulo «JavaScript en la página» terminó con un carrito dibujado a partir de los datos por un único render(). Aquí cada cambio del campo de cantidad llama a render(). El cliente quiere 15 botellas de leche: hace clic en el campo, borra el 2 y escribe 15. Pruébalo.

HTML, CSS, JS

Tras borrar, el campo está vacío y ya no tiene el foco: el 1 y el 5 no van a ninguna parte. render() sustituyó el campo por uno nuevo con la primera tecla. El campo nuevo se ve igual, pero es otro elemento, y el foco estaba en el viejo. Tocar a mano cada lugar por separado hace que se olviden lugares. Dibujarlo todo de nuevo hace perder el foco. Hay un tercer camino.

La idea

La interfaz como función de los datos

render() tenía una buena idea: la pantalla se construye solo a partir de los datos. Con los mismos datos la pantalla es la misma, sea lo que sea lo que hubiera antes. El error estaba en el último paso: render() quita y crea los elementos él mismo.

Por eso el trabajo se divide en dos:

  • Tú escribes una función que dice cómo se ve la pantalla para esos datos. No toca la página; devuelve una descripción: aquí hay un <main>, dentro un <h1> con el texto «El carrito», luego un campo con el valor 2.
  • La biblioteca guarda el DOM real. La primera vez lo construye a partir de la descripción. En cada cambio de los datos toma la descripción nueva, la compara con la anterior y cambia en la página solo la diferencia.

Una función así se llama componente. En corto: la pantalla es una función de los datos, y el componente es esa función. React y Vue lo hacen de formas distintas. La idea es la misma, y elegir entre ellos es cosa del equipo, no de una biblioteca «mejor».

Míralo

El primer componente

En Vue un componente es un archivo .vue (Single File Component) con hasta tres partes: <script setup> con los datos y las funciones, <template> con la descripción de la pantalla y, si quieres, <style>.

<script setup>
const product = { name: 'Leche fresca', price: 1.6, stock: 35 };
</script>
<template>
<main>
<h1>{{ product.name }}</h1>
<p>Precio: {{ money(product.price) }}</p>
</main>
</template>
  • Todo lo que declara <script setup> (variables, funciones, componentes importados) lo ve la plantilla.
  • {{ product.name }} es una expresión de JavaScript entre dobles llaves. Su resultado se convierte en texto en la página.
  • El nombre del componente es el nombre del archivo: App.vue es el componente App.
  • El sandbox pone App en la página por sí solo. En un proyecto real lo hace una línea en main.js: createApp(App).mount('#app').
Vue

Cambia el precio o el stock en el objeto product y pulsa «Ejecutar». No tocas la descripción de la pantalla: la pantalla sigue a los datos.

La idea

Las reglas de la plantilla

La plantilla es HTML, pero Vue la compila a JavaScript y lee en ella signos propios. Por eso tiene reglas propias.

  • {{ expresión }} en el texto: <h1>{{ product.name }}</h1>.
  • : delante de un atributo significa «el valor es una expresión»: <img :src="product.image" :alt="product.name" />. :alt es la abreviatura de v-bind:alt. Sin : el valor es el texto «product.name», letra por letra. {{ }} no funciona dentro de un atributo: alt="{{ product.name }}" es el texto {{ product.name }}.
  • @ delante de un evento: @click="add" (abreviatura de v-on:click). También sirve una expresión corta: @click="count++" o @click="add(product)". Vue la envuelve en una función que se ejecuta con el clic.
  • class y for siguen siendo class y for, como en HTML.
  • Se permiten varias raíces: la plantilla puede tener un <header> y un <main> uno junto al otro.
  • Las etiquetas sin contenido se pueden cerrar con />: <input />, <ProductCard />.

Pruébalo

Encuentra el error

Un clic en «Añadir» no escribe nada en la consola.

Elige la línea con el error.

Pruébalo

Completa

El lector de pantalla tiene que leer el nombre del producto como descripción de la imagen.

<img :src="product.image" ="product.name" />

La idea

Componentes hechos de componentes

La página de la tienda tiene partes: la parte de arriba con el menú, la tarjeta del producto, el carrito. Cada parte se convierte en un componente en su propio archivo, y un componente mayor los usa como etiquetas. Así App no es una función larga, sino unas pocas líneas que dicen qué hay en la página.

ShopHeader.vue
<template>
<header>
<a href="/">La tienda</a>
<nav>…</nav>
</header>
</template>
App.vue
<script setup>
import ShopHeader from './ShopHeader.vue';
import ProductCard from './ProductCard.vue';
</script>
<template>
<ShopHeader />
<main>
<ProductCard />
</main>
</template>

La plantilla ve directamente un componente importado. Sin el import, Vue no sabe qué es ShopHeader: la consola dice «[Vue warn]: Failed to resolve component: ShopHeader», y en la página queda una etiqueta desconocida. Por convención el nombre empieza con mayúscula (PascalCase), para distinguirlo de las etiquetas HTML. Vue también acepta <shop-header />.

Pruébalo

Monta la página

ShopHeader y ProductCard están listos en sus archivos. Monta App: ShopHeader arriba, y la tarjeta del producto en main, bajo el título «Lácteos».

Vue

La idea

Solo la diferencia

Aquí está el carrito del problema como componente.

ref('2') es la memoria del componente: un valor que Vue vigila. Cuando cambia, Vue vuelve a dibujar los componentes que lo leen. Es el tema del módulo «Estado». Aquí solo está para que algo pueda cambiar.

Vue

Haz lo mismo que en el problema: clic en el campo de leche, borra el 2 y escribe 15. El campo conserva el foco y lo que escribiste, y el total pasa a 25,20 €. El componente describe toda la pantalla con cada tecla, igual que render(). La diferencia es que la página no se construye de nuevo. La biblioteca ve que en la descripción nueva el campo está en el mismo lugar con la misma etiqueta, y cambia solo su valor y el texto del total.

Míralo

Mira la comparación

<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const milk = ref('2');
const bread = ref('1');
</script>
<template>
<main>
<h1>El carrito</h1>
<label>
Leche fresca
<input type="number" :value="milk" @input="milk = $event.target.value" />
</label>
<label>
Pan blanco
<input type="number" :value="bread" @input="bread = $event.target.value" />
</label>
<p>Total: {{ money(Number(milk) * 1.6 + Number(bread) * 1.2) }}</p>
</main>
</template>
Paso a paso, en palabras
  1. El cliente ha borrado el 2 y ha escrito 1: los datos dicen milk = "1". En el centro está la última descripción que devolvió el componente; a la derecha está el DOM, con el foco en el campo de leche.
  2. El cliente escribe 5. @input escribe "15" en milk: los datos cambian, y Vue sabe que la plantilla lee milk.
  3. Vue compiló la plantilla a una función de render. Como milk cambió, la ejecuta otra vez y devuelve una descripción nueva: un árbol de objetos simples.
  4. La biblioteca compara la descripción nueva con la anterior. Las mismas etiquetas en el mismo orden y dos diferencias: el valor del campo y el texto del total.
  5. Solo las dos diferencias llegan al DOM. El <input> es el mismo elemento de antes, por eso el foco y el cursor se quedan en él.
  6. Tú describes toda la pantalla para los datos dados, como con render(). La biblioteca encuentra qué cambió y toca solo eso. Las dos bibliotecas lo hacen, cada una a su manera.

La descripción es barata: es un árbol de objetos simples, y el componente lo rehace cada vez. El DOM es la parte cara, así que la biblioteca solo toca ahí las diferencias. Tú escribes como con render(): toda la pantalla a partir de los datos. El precio de render() (el foco perdido, miles de elementos nuevos) no se paga.

Más precisamente, para curiosos

DOM virtual. A la descripción se la suele llamar DOM virtual, y a su comparación con la anterior, reconciliación (React) o patching (Vue). Un nodo de la descripción es un objeto como { type: 'h1', props: { children: 'El carrito' } }.

Los dos caminos. La plantilla se compila a una función de render que devuelve los nodos de la descripción. El compilador marca qué partes no cambian nunca, y la comparación las salta. Vue recuerda qué datos lee cada componente (reactividad) y vuelve a ejecutar solo los componentes cuyos datos cambiaron. En los dos casos al final hay una descripción nueva, una comparación y unos pocos cambios en el DOM.

El texto es texto. {{ product.name }} inserta texto, como textContent. Un nombre como <img src=x onerror=alert(1)> se muestra como caracteres y no se convierte en una etiqueta. El HTML de una cadena solo entra con v-html, y el nombre ya avisa de que hay que tener cuidado.

Options API. En código antiguo un componente es un objeto: export default { data() { … }, methods: { … } }. Sigue funcionando, pero el código nuevo usa <script setup> (Composition API).

v-model. El par :value y @input tiene una abreviatura, v-model. Pertenece a la lección sobre formularios.

Ejercicio

La tarjeta del producto

En ProductCard está el objeto product (Queso blanco). Haz la tarjeta: el nombre como título de nivel dos, «Precio: 7,90 €» (con money), «En stock: 12 uds.» y un botón «Añadir» cuyo nombre para el lector de pantalla sea «Añadir Queso blanco». Un clic en el botón escribe «Añadido: Queso blanco» en la consola (la función add ya está hecha). Después monta App: ShopHeader arriba, y la tarjeta en main, bajo el título «Lácteos».

Vue

Test

Ponte a prueba

1. Un componente se dibuja por segunda vez con los mismos datos. ¿Qué muestra?
2. ¿Qué línea de la plantilla muestra el nombre del producto como alt de la imagen?
3. ¿Por qué el foco se queda en el campo dentro del componente, mientras que render() del módulo anterior lo pierde?
4. App.vue usa <ShopHeader />, y la consola dice Failed to resolve component: ShopHeader. ¿Qué falta?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·