Saltar al contenido
schedule 12 min Vue

Template y directivas

En la lección anterior creaste tu primer proyecto con Vue. Tienes la estructura montada, los archivos listos y el servidor de desarrollo corriendo. Ahora viene lo interesante: el sistema de templates de Vue. Piensa en él como HTML con superpoderes — puedes vincular datos, mostrar u ocultar elementos, recorrer listas y reaccionar a lo que hace el usuario, todo de forma declarativa. No necesitas manipular el DOM manualmente como hacías con JavaScript vanilla.

Interpolación de texto con {{ }}

La forma más básica de mostrar datos reactivos en el template es la doble llave @{{ }}. Dentro puedes poner cualquier expresión JavaScript válida:

src/App.vue
<script setup>
import { ref } from 'vue'

const cafeteria = ref('Café Nebulosa')
const precio = ref(4.50)
const enStock = ref(true)
</script>

<template>
    <h1>{{ cafeteria }}</h1>
    <p>Precio del espresso: {{ precio }} €</p>
    <p>Disponible: {{ enStock ? 'Sí' : 'No' }}</p>
    <p>Precio con IVA: {{ (precio * 1.21).toFixed(2) }} €</p>
    <p>Nombre en mayúsculas: {{ cafeteria.toUpperCase() }}</p>
</template>

Dentro de {{ }} puedes usar operadores ternarios, llamar a métodos de string o number, hacer cálculos matemáticos... cualquier expresión JavaScript. Lo que no puedes hacer es declarar variables o usar sentencias como if o for — para eso están las directivas.

Dato clave: cada vez que cambias el valor de un ref(), Vue actualiza automáticamente el template donde se usa. Eso es la reactividad.

v-bind: vincular atributos HTML

La interpolación @{{ }} solo funciona dentro del contenido de un elemento. Para vincular datos a atributos HTML, necesitas la directiva v-bind — o su atajo :, que es lo que usarás siempre:

src/App.vue
<script setup>
import { ref } from 'vue'

const producto = ref({
    nombre: 'Café Interestelar',
    imagen: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=400',
    enlace: 'https://cafenebulosa.com/interestelar',
    agotado: false,
    destacado: true,
    colorAccento: '#4FC08D'
})
</script>

<template>
    <div class="producto-card">
        <!-- :src en vez de v-bind:src (el atajo es más limpio) -->
        <img :src="producto.imagen" :alt="producto.nombre">

        <!-- :href para enlaces dinámicos -->
        <a :href="producto.enlace">Ver detalles</a>

        <!-- :disabled para deshabilitar botones -->
        <button :disabled="producto.agotado">
            {{ producto.agotado ? 'Agotado' : 'Añadir al carrito' }}
        </button>
    </div>
</template>

Clases dinámicas con :class

Una de las cosas que más vas a usar. Puedes pasar un objeto donde las claves son nombres de clase y los valores son booleanos:

src/App.vue
<script setup>
import { ref } from 'vue'

const estaActivo = ref(true)
const tieneError = ref(false)
const esDestacado = ref(true)
const tema = ref('oscuro')
</script>

<template>
    <!-- Sintaxis de objeto: clase se aplica si el valor es true -->
    <div :class="{ active: estaActivo, error: tieneError, destacado: esDestacado }">
        Tarjeta de producto
    </div>

    <!-- Sintaxis de array: combinar clases estáticas y dinámicas -->
    <div :class="['card', tema === 'oscuro' ? 'card--dark' : 'card--light']">
        Contenido
    </div>

    <!-- Mezclar clase estática con :class (se combinan, no se sobreescriben) -->
    <div class="base-card" :class="{ 'card--active': estaActivo }">
        Esto tiene clase "base-card" siempre + "card--active" si estaActivo es true
    </div>
</template>

Estilos dinámicos con :style

src/App.vue
<script setup>
import { ref } from 'vue'

const colorPrincipal = ref('#4FC08D')
const tamanoFuente = ref(18)
</script>

<template>
    <!-- Objeto de estilos (propiedades en camelCase) -->
    <h2 :style="{ color: colorPrincipal, fontSize: tamanoFuente + 'px' }">
        Texto con estilo dinámico
    </h2>
</template>

Usa :style solo cuando necesites valores realmente dinámicos (calculados en tiempo de ejecución). Para estilos estáticos o condicionales simples, prefiere :class con clases CSS — es más limpio y más fácil de mantener.

v-if, v-else-if y v-else: renderizado condicional

Estas directivas controlan si un elemento existe o no en el DOM. Si la condición es falsa, Vue ni siquiera crea el elemento:

src/App.vue
<script setup>
import { ref } from 'vue'

const stock = ref(15)
const suscripcion = ref('premium') // 'free', 'premium', 'business'
</script>

<template>
    <!-- Ejemplo: estado de stock -->
    <div class="stock-badge">
        <span v-if="stock === 0" class="sin-stock">Agotado</span>
        <span v-else-if="stock <= 5" class="poco-stock">¡Solo quedan {{ stock }}!</span>
        <span v-else class="en-stock">En stock</span>
    </div>

    <!-- Ejemplo: contenido según suscripción -->
    <div class="contenido">
        <div v-if="suscripcion === 'business'">
            <h3>Acceso Business</h3>
            <p>Todos los cursos + soporte prioritario + certificados</p>
        </div>
        <div v-else-if="suscripcion === 'premium'">
            <h3>Acceso Premium</h3>
            <p>Todos los cursos + certificados</p>
        </div>
        <div v-else>
            <h3>Acceso Gratuito</h3>
            <p>Cursos básicos disponibles</p>
        </div>
    </div>
</template>

Importante: los elementos con v-else-if y v-else deben ir inmediatamente después del elemento con v-if (o del anterior v-else-if). Si metes otro elemento entre medias, Vue no los conectará y tendrás un error.

v-show: alternar visibilidad

v-show parece hacer lo mismo que v-if, pero funciona de forma diferente por debajo:

src/App.vue
<script setup>
import { ref } from 'vue'

const mostrarDetalles = ref(false)
</script>

<template>
    <button @click="mostrarDetalles = !mostrarDetalles">
        {{ mostrarDetalles ? 'Ocultar' : 'Mostrar' }} detalles
    </button>

    <!-- v-show: el elemento SIEMPRE está en el DOM, solo cambia display: none -->
    <div v-show="mostrarDetalles">
        <p>Estos son los detalles del producto...</p>
        <p>Origen: Colombia. Tueste: medio. Notas: chocolate y nuez.</p>
    </div>
</template>

v-if vs v-show:

  • v-if: añade y elimina el elemento del DOM. Tiene un coste de renderizado cada vez que cambia. Úsalo cuando la condición cambie pocas veces.
  • v-show: solo cambia el CSS display: none. El elemento siempre existe en el DOM. Úsalo cuando la condición cambie frecuentemente (como un toggle de mostrar/ocultar).
Regla práctica: si el usuario va a hacer toggle muchas veces (menú, panel, modal), usa v-show. Si es algo que se muestra una vez según una condición (contenido por rol, estado de error), usa v-if.

v-for: recorrer listas

La directiva más potente para renderizar listas. Siempre usa :key con un identificador único — Vue lo necesita para rastrear cada elemento y actualizar el DOM de forma eficiente:

src/App.vue
<script setup>
import { ref } from 'vue'

const playlist = ref([
    { id: 1, titulo: 'Stellar Echoes', artista: 'Nebula', duracion: '3:42', favorita: true },
    { id: 2, titulo: 'Dark Matter', artista: 'Cosmos DJ', duracion: '4:15', favorita: false },
    { id: 3, titulo: 'Gravity Falls', artista: 'Orbit', duracion: '2:58', favorita: true },
    { id: 4, titulo: 'Solar Wind', artista: 'Helio', duracion: '5:01', favorita: false },
])
</script>

<template>
    <h2>Mi Playlist Galáctica</h2>
    <ul>
        <!-- SIEMPRE usa :key con un id único -->
        <li v-for="cancion in playlist" :key="cancion.id">
            <span>{{ cancion.titulo }}</span> —
            <span>{{ cancion.artista }}</span>
            <span>({{ cancion.duracion }})</span>
            <span v-if="cancion.favorita">⭐</span>
        </li>
    </ul>
</template>

Acceder al índice

<template>
    <!-- (elemento, índice) in array -->
    <ol>
        <li v-for="(cancion, index) in playlist" :key="cancion.id">
            {{ index + 1 }}. {{ cancion.titulo }} — {{ cancion.artista }}
        </li>
    </ol>
</template>

Iterar sobre propiedades de un objeto

src/App.vue
<script setup>
import { ref } from 'vue'

const stats = ref({
    cursos: 42,
    horas: 156,
    certificados: 8,
    proyectos: 12
})
</script>

<template>
    <div v-for="(valor, clave) in stats" :key="clave">
        <strong>{{ clave }}:</strong> {{ valor }}
    </div>
</template>

v-for con v-if: el patrón correcto

Nunca pongas v-if en el mismo elemento que v-for. Vue 3 da prioridad a v-if, lo que puede causar errores porque v-if no tendrá acceso a la variable del bucle. La solución es usar una propiedad computada para filtrar antes de iterar:

src/App.vue
<script setup>
import { ref, computed } from 'vue'

const playlist = ref([
    { id: 1, titulo: 'Stellar Echoes', artista: 'Nebula', favorita: true },
    { id: 2, titulo: 'Dark Matter', artista: 'Cosmos DJ', favorita: false },
    { id: 3, titulo: 'Gravity Falls', artista: 'Orbit', favorita: true },
])

// ✅ Filtra con computed, luego itera sobre el resultado
const favoritas = computed(() => playlist.value.filter(c => c.favorita))
</script>

<template>
    <!-- ❌ MAL: v-if y v-for en el mismo elemento -->
    <!--
    <li v-for="cancion in playlist" v-if="cancion.favorita" :key="cancion.id">
        {{ cancion.titulo }}
    </li>
    -->

    <!-- ✅ BIEN: iterar sobre la lista ya filtrada -->
    <li v-for="cancion in favoritas" :key="cancion.id">
        {{ cancion.titulo }} ⭐
    </li>
</template>

v-model: vinculación bidireccional

v-model conecta un input del formulario con una variable reactiva en ambas direcciones: si cambias la variable, el input se actualiza; si el usuario escribe en el input, la variable se actualiza. Es magia pura:

src/App.vue
<script setup>
import { ref } from 'vue'

const nombre = ref('')
const aceptaTerminos = ref(false)
const generoFavorito = ref('rock-espacial')
const biografia = ref('')
</script>

<template>
    <form>
        <!-- Input de texto -->
        <label>
            Nombre de DJ:
            <input v-model="nombre" type="text" placeholder="Tu nombre artístico">
        </label>
        <p>Vista previa: {{ nombre || 'Escribe tu nombre...' }}</p>

        <!-- Checkbox -->
        <label>
            <input v-model="aceptaTerminos" type="checkbox">
            Acepto las normas de la estación espacial
        </label>
        <p>Términos aceptados: {{ aceptaTerminos }}</p>

        <!-- Select -->
        <label>
            Género musical:
            <select v-model="generoFavorito">
                <option value="rock-espacial">Rock Espacial</option>
                <option value="synth-nebula">Synth Nebula</option>
                <option value="ambient-orbital">Ambient Orbital</option>
                <option value="techno-solar">Techno Solar</option>
            </select>
        </label>
        <p>Género seleccionado: {{ generoFavorito }}</p>

        <!-- Textarea -->
        <label>
            Biografía:
            <textarea v-model="biografia" rows="3" placeholder="Cuéntanos tu historia..."></textarea>
        </label>
        <p>Caracteres: {{ biografia.length }}</p>
    </form>
</template>

Fíjate en que no necesitas escuchar eventos ni actualizar valores manualmente. v-model hace todo eso por ti. Escribe en cualquiera de los inputs y verás cómo los párrafos de "vista previa" se actualizan en tiempo real.

v-on: manejar eventos

La directiva v-on (atajo: @) conecta eventos del DOM con funciones de tu componente. Aquí tienes los más comunes:

src/App.vue
<script setup>
import { ref } from 'vue'

const contador = ref(0)
const mensaje = ref('')

function incrementar() {
    contador.value++
}

function enviarFormulario() {
    if (mensaje.value.trim()) {
        alert(`Mensaje enviado: ${mensaje.value}`)
        mensaje.value = ''
    }
}
</script>

<template>
    <!-- @click: clic del ratón -->
    <button @click="incrementar">Clics: {{ contador }}</button>

    <!-- También puedes escribir la lógica directamente (para cosas simples) -->
    <button @click="contador = 0">Resetear</button>

    <!-- @submit.prevent: enviar formulario sin recargar la página -->
    <form @submit.prevent="enviarFormulario">
        <input v-model="mensaje" type="text" placeholder="Escribe un mensaje">
        <button type="submit">Enviar</button>
    </form>

    <!-- @keyup.enter: ejecutar al pulsar Enter -->
    <input
        v-model="mensaje"
        @keyup.enter="enviarFormulario"
        placeholder="Pulsa Enter para enviar"
    >
</template>

El .prevent en @submit.prevent es un modificador — equivale a llamar a event.preventDefault(). Vue tiene varios modificadores útiles como .stop, .once o .self. Los verás en detalle en la lección de eventos.

Resumen

  • {{ }} — interpolación de texto. Muestra datos reactivos en el template.
  • :atributo (v-bind) — vincula atributos HTML a datos. Especialmente útil con :class y :style.
  • v-if / v-else-if / v-else — renderizado condicional. Añade o elimina elementos del DOM.
  • v-show — alternar visibilidad con CSS. El elemento siempre está en el DOM.
  • v-for — recorrer arrays y objetos. Siempre con :key. Nunca con v-if en el mismo elemento.
  • v-model — vinculación bidireccional para formularios.
  • @evento (v-on) — manejar eventos del DOM.

Estas directivas son el vocabulario fundamental de Vue. Con ellas ya puedes construir interfaces interactivas sin tocar el DOM directamente. En la siguiente lección profundizaremos en las propiedades computadas y watchers — la clave para crear lógica derivada de forma eficiente.

code

Mini catálogo de productos galácticos

Medio schedule 20 min

Crea un componente Vue que funcione como un mini catálogo de productos de un café espacial. Debe incluir:

  • Un array de al menos 5 productos con id, nombre, precio, categoria (bebida/comida), stock (número) e imagen (puedes usar URLs de placeholder).
  • Un input de búsqueda con v-model que filtre los productos por nombre en tiempo real (usa una propiedad computada para el filtrado).
  • Usa v-for con :key para renderizar la lista de productos filtrados.
  • Usa v-if para mostrar una etiqueta de "Agotado" cuando el stock sea 0, y v-else-if para "¡Últimas unidades!" cuando el stock sea menor a 3.
  • Usa :class para aplicar una clase destacado a los productos con stock mayor a 10 y una clase agotado a los que tengan stock 0.
  • Usa :disabled en el botón de "Añadir al carrito" para los productos sin stock.
  • Muestra un mensaje con v-if cuando no haya resultados de búsqueda: "No se encontraron productos".
lightbulb Pistas

Define el array de productos con ref(). Crea un ref para el término de búsqueda y una propiedad computed que filtre con .filter() y .includes() (convierte a minúsculas con .toLowerCase() para que la búsqueda no sea sensible a mayúsculas). Recuerda acceder al valor del ref con .value dentro de <script setup>, pero en el template Vue lo desenvuelve automáticamente.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.