En esta página

Template syntax y directivas

14 min lectura TextoCap. 1 — Fundamentos de Vue

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

  1. Directivas condicionales: Crea un componente con una variable de estado autenticado: boolean. Muestra un mensaje de bienvenida si es true y un formulario de login si es false. Añade un botón para alternar el estado.
  2. Lista interactiva: Crea una lista de tareas con v-for. Añade la capacidad de marcar tareas como completadas (con v-model en un checkbox) y eliminar tareas. Usa :key con el ID de cada tarea.
  3. 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().

v-if vs v-show — elige bien
v-if destruye y recrea el elemento del DOM cuando la condición cambia. v-show solo cambia el CSS display:none. Usa v-if cuando la condición raramente cambia (mejor para el DOM inicial). Usa v-show cuando el elemento alterna frecuentemente entre visible e invisible.
Siempre usa :key en v-for
La directiva :key es obligatoria en v-for. Proporciona a Vue un identificador único para cada elemento de la lista, lo que permite actualizaciones eficientes del DOM. Nunca uses el índice del array como key si el orden puede cambiar o si los elementos pueden eliminarse.
Atributos dinámicos con :[nombre]
Vue permite enlazar dinámicamente el nombre del atributo con la sintaxis :[expresión]. Esto es útil cuando el atributo a enlazar depende de la lógica del componente. Por ejemplo, :[tipoProp]='valor' enlazará al atributo cuyo nombre está en la variable tipoProp.
vue
<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>
vue
<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>