En esta página
Template syntax y directivas
Interpolación de texto con mustache
La forma más básica de mostrar datos en el template es la sintaxis mustache (dobles llaves):
<template>
<p>Hola, {{ nombre }}</p>
<p>El año actual es {{ anio }}</p>
<p>El precio es {{ (precio * 1.21).toFixed(2) }}</p>
</template>Dentro de las llaves puedes escribir cualquier expresión JavaScript simple: operaciones aritméticas, llamadas a métodos de string/array, operadores ternarios, etc. No puedes usar sentencias (if, for, declaraciones de variables), solo expresiones.
El texto se escapa por defecto: si nombre contiene <b>hola</b>, se mostrará literalmente como texto, no como HTML. Para renderizar HTML sin escapar, usa v-html (con precaución, nunca con contenido del usuario):
<template>
<!-- Peligroso con contenido del usuario — riesgo de XSS -->
<div v-html="htmlConfiable"></div>
</template>v-bind — enlazar atributos
La directiva v-bind (abreviada como :) enlaza dinámicamente un atributo HTML a una expresión:
<template>
<!-- Equivalentes -->
<img v-bind:src="urlImagen" v-bind:alt="descripcion" />
<img :src="urlImagen" :alt="descripcion" />
<!-- Enlazar un objeto entero de atributos -->
<div v-bind="{ id: 'panel', class: 'activo', 'aria-label': 'panel principal' }">
</div>
</template>Enlazar clases y estilos
Enlazar class y style tiene soporte especial en Vue:
<script setup lang="ts">
const activo = ref(true)
const error = ref(false)
const tamano = ref(16)
</script>
<template>
<!-- class como objeto -->
<div :class="{ activo: activo, error: error }">Componente</div>
<!-- class como array -->
<div :class="[activo ? 'activo' : '', 'base']">Componente</div>
<!-- style como objeto -->
<p :style="{ fontSize: tamano + 'px', color: '#333' }">Texto</p>
</template>v-on — manejar eventos
La directiva v-on (abreviada como @) escucha eventos del DOM o del componente:
<template>
<!-- Expresión inline -->
<button type="button" @click="contador++">+</button>
<!-- Función handler -->
<button type="button" @click="manejarClic">Clic</button>
<!-- Función con argumento explícito -->
<button type="button" @click="eliminar(producto.id)">Eliminar</button>
<!-- Acceder al evento nativo con $event -->
<button type="button" @click="handler($event)">Con evento</button>
</template>Modificadores de eventos
Vue ofrece modificadores que son açúcar sintáctico para patrones comunes:
<template>
<!-- Previene el comportamiento por defecto (ej: submit de formulario) -->
<form @submit.prevent="enviar">...</form>
<!-- Detiene la propagación del evento -->
<div @click.stop="handler">...</div>
<!-- Solo dispara una vez -->
<button type="button" @click.once="handler">Acción única</button>
<!-- Modificadores de teclado -->
<input @keyup.enter="buscar" @keyup.escape="limpiar" />
<!-- Modificadores de ratón -->
<div @click.right.prevent="menuContexto">...</div>
</template>v-if, v-else-if y v-else
Las directivas condicionales controlan qué elementos se renderizan:
<template>
<div v-if="estado === 'cargando'">
<p>Cargando...</p>
</div>
<div v-else-if="estado === 'error'">
<p>Error al cargar los datos.</p>
</div>
<div v-else>
<p>Datos cargados correctamente.</p>
</div>
</template>Para aplicar condiciones a múltiples elementos sin añadir un nodo extra al DOM, usa <template> como elemento envolvente:
<template>
<template v-if="autenticado">
<nav>Menú de usuario</nav>
<aside>Panel lateral</aside>
</template>
</template>v-show — visibilidad sin destruir
v-show simplemente añade o quita display: none:
<template>
<!-- El elemento SIEMPRE está en el DOM, solo cambia su visibilidad -->
<div v-show="menuAbierto">Menú desplegable</div>
</template>Úsalo cuando el elemento alterna frecuentemente, como modales, tooltips o menús desplegables.
v-for — renderizar listas
v-for itera sobre arrays, objetos o rangos numéricos:
<script setup lang="ts">
interface Producto {
id: number
nombre: string
precio: number
}
const productos = ref<Producto[]>([...])
</script>
<template>
<!-- Iterar array -->
<li v-for="producto in productos" :key="producto.id">
{{ producto.nombre }}
</li>
<!-- Con índice -->
<li v-for="(producto, index) in productos" :key="producto.id">
#{{ index + 1 }} - {{ producto.nombre }}
</li>
<!-- Iterar objeto -->
<li v-for="(valor, clave) in objeto" :key="clave">
{{ clave }}: {{ valor }}
</li>
<!-- Rango numérico -->
<span v-for="n in 5" :key="n">{{ n }}</span>
</template>Por qué el key es crucial
Cuando Vue actualiza una lista, necesita identificar qué elementos cambiaron, cuáles se añadieron y cuáles se eliminaron. Sin :key, Vue usa la posición del elemento, lo que puede causar bugs visuales cuando el orden cambia o elementos se eliminan del medio.
<!-- ❌ Nunca hagas esto si los elementos pueden reordenarse o eliminarse -->
<li v-for="(item, i) in lista" :key="i">{{ item.nombre }}</li>
<!-- ✅ Usa un ID único del dominio -->
<li v-for="item in lista" :key="item.id">{{ item.nombre }}</li>v-model — enlace bidireccional
v-model crea un enlace bidireccional entre un campo de formulario y una variable reactiva. Es açúcar sintáctico sobre :value + @input:
<script setup lang="ts">
const nombre = ref('')
const edad = ref(0)
const activo = ref(false)
const opcion = ref('a')
const tags = ref<string[]>([])
</script>
<template>
<!-- Input de texto -->
<input v-model="nombre" type="text" />
<!-- Input numérico (modifica el tipo automáticamente) -->
<input v-model.number="edad" type="number" />
<!-- Checkbox -->
<input v-model="activo" type="checkbox" />
<!-- Select -->
<select v-model="opcion">
<option value="a">Opción A</option>
<option value="b">Opción B</option>
</select>
<!-- Múltiples checkboxes — enlaza a array -->
<input v-model="tags" type="checkbox" value="vue" />
<input v-model="tags" type="checkbox" value="react" />
</template>Modificadores de v-model
<template>
<!-- .trim — elimina espacios al inicio y final -->
<input v-model.trim="nombre" />
<!-- .lazy — actualiza en blur en lugar de input -->
<input v-model.lazy="nombre" />
<!-- .number — convierte a número automáticamente -->
<input v-model.number="edad" type="number" />
</template>Atributos dinámicos con :[nombre]
Puedes hacer dinámico incluso el nombre del atributo:
<script setup lang="ts">
const tipoAtributo = ref<'href' | 'src'>('href')
const valorAtributo = ref('https://vuejs.org')
</script>
<template>
<!-- El nombre del atributo es dinámico -->
<a :[tipoAtributo]="valorAtributo">Enlace dinámico</a>
</template>v-once y v-memo — optimización
Para contenido que nunca cambia, v-once renderiza el elemento solo una vez:
<template>
<!-- Renderizado una sola vez, ignora cambios futuros -->
<h1 v-once>{{ tituloFijo }}</h1>
</template>v-memo memoriza el subárbol del template y solo lo rerenderiza cuando sus dependencias cambian:
<template>
<!-- Solo rerenderiza cuando id o activo cambian -->
<div v-memo="[item.id, item.activo]">
<!-- Contenido complejo -->
</div>
</template>Práctica
- Directivas condicionales: Crea un componente con una variable de estado
autenticado: boolean. Muestra un mensaje de bienvenida si estruey un formulario de login si esfalse. Añade un botón para alternar el estado. - Lista interactiva: Crea una lista de tareas con
v-for. Añade la capacidad de marcar tareas como completadas (conv-modelen un checkbox) y eliminar tareas. Usa:keycon el ID de cada tarea. - Formulario con v-model: Crea un formulario con campos de texto, número y select, todos enlazados con
v-model. Muestra en tiempo real los valores debajo del formulario usando interpolación mustache.
En la siguiente lección aprenderemos el sistema de reactividad de Vue: ref() y reactive().
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(true)
const modo = ref<'a' | 'b' | 'c'>('a')
const productos = ref([
{ id: 1, nombre: 'Teclado', precio: 45 },
{ id: 2, nombre: 'Ratón', precio: 25 },
{ id: 3, nombre: 'Monitor', precio: 320 },
])
const busqueda = ref('')
// v-bind dinámico
const claseActiva = ref('resaltado')
const atributoDinamico = ref('href')
const valorDinamico = ref('https://vuejs.org')
</script>
<template>
<!-- Interpolación de texto -->
<p>Bienvenido, {{ busqueda || 'visitante' }}</p>
<!-- v-bind — enlace de atributos -->
<a :[atributoDinamico]="valorDinamico">Vue.js oficial</a>
<!-- v-if / v-else-if / v-else -->
<div v-if="modo === 'a'">Modo A activo</div>
<div v-else-if="modo === 'b'">Modo B activo</div>
<div v-else>Otro modo</div>
<!-- v-show — mantiene en DOM, solo cambia display -->
<div v-show="visible">Este elemento siempre está en el DOM</div>
<!-- v-for con key -->
<ul>
<li
v-for="producto in productos"
:key="producto.id"
:class="claseActiva"
>
{{ producto.nombre }} — ${{ producto.precio }}
</li>
</ul>
<!-- v-model — enlace bidireccional -->
<input v-model="busqueda" placeholder="Buscar..." />
<p>Buscando: {{ busqueda }}</p>
</template>
<style scoped>
.resaltado { background: #fef9c3; }
</style>
<script setup lang="ts">
import { ref } from 'vue'
const texto = ref('')
const log = ref<string[]>([])
function manejarInput(evento: Event): void {
const target = evento.target as HTMLInputElement
texto.value = target.value
}
function manejarTecla(evento: KeyboardEvent): void {
if (evento.key === 'Enter') {
log.value.push(`Enter presionado con texto: ${texto.value}`)
}
}
</script>
<template>
<!-- Evento con handler inline -->
<button type="button" @click="log.push('clic!')">Clic rápido</button>
<!-- Evento con función -->
<input
:value="texto"
@input="manejarInput"
@keyup.enter="manejarTecla"
@keyup.escape="texto = ''"
/>
<!-- Modificadores de eventos -->
<form @submit.prevent="log.push('formulario enviado')">
<button type="submit">Enviar</button>
</form>
<!-- Log de eventos -->
<ul>
<li v-for="(entrada, i) in log" :key="i">{{ entrada }}</li>
</ul>
</template>
Inicia sesión para guardar tu progreso