Directivas en Vue: v-on
v-on conecta eventos del DOM o de componentes con funciones de tu app.
Cuando el usuario hace clic, escribe o pulsa una tecla, v-on dispara la lógica que definiste.
Por qué importa
Sin v-on, una interfaz no responde a la interacción del usuario.
Con v-on, puedes:
- Capturar acciones del usuario sin manipular el DOM manualmente.
- Mantener la interacción declarativa y legible en el template.
- Separar estado, renderizado y comportamiento de forma consistente con Vue.
v-ones la base de botones, formularios, atajos de teclado y comunicación entre componentes vía eventos.
Concepto base
La forma larga:
<button v-on:click="increment">Sumar</button>La forma corta recomendada:
<button @click="increment">Sumar</button>v-on admite:
- Eventos nativos (
click,input,submit,keydown). - Eventos emitidos por componentes (
@save,@close). - Modificadores de evento (
.prevent,.stop,.once,.self). - Modificadores de teclado (
.enter,.esc, combinaciones conctrl,shift, etc.).
Cuándo usarlo
Usa v-on cuando necesites reaccionar a acciones del usuario o a eventos de componentes.
Casos típicos:
- Botones de acción (
@click="createTask"). - Formularios (
@submit.prevent="submitForm"). - Inputs en tiempo real (
@input="handleSearch"). - Atajos de teclado (
@keydown.ctrl.enter="publish"). - Eventos personalizados desde componentes hijos (
@save="persistTask").
Cuándo evitarlo
Evita v-on en estos casos:
- Cuando no hay interacción real y el contenido es completamente estático.
- Cuando colocas demasiada lógica inline en el template; es mejor moverla a funciones o
computed. - Cuando intentas “resolver” seguridad con eventos del frontend: la validación real debe ocurrir en backend.
También evita encadenar muchos modificadores sin una intención clara, porque dificulta el mantenimiento.
Errores comunes
1) Ejecutar la función en vez de referenciarla
Incorrecto:
<button @click="saveTask()">Guardar</button>Esto es válido, pero si no necesitas argumentos, suele ser más limpio:
<button @click="saveTask">Guardar</button>2) Olvidar .prevent en formularios
Incorrecto:
<form @submit="submitForm">Correcto:
<form @submit.prevent="submitForm">Sin
.prevent, el navegador recarga la página por defecto.
3) Poner demasiada lógica dentro del template
Evitar:
<button @click="isAdmin && canEdit && !isLocked ? publishNow() : showWarning()">
Publicar
</button>Mejor:
<button @click="handlePublishClick">Publicar</button>Y mover la decisión a una función clara en el script.
4) Usar event sin declararlo
Incorrecto:
<input @input="onInput(event)" />Correcto:
<input @input="onInput($event)" />O mejor aún, tipar el evento y leer target de forma segura en TypeScript.
Ejemplos prácticos
1) Click simple para actualizar estado
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">Clicks: {{ count }}</button>
</template><script lang="ts">
export default {
data() {
return {
count: 0,
};
},
};
</script>
<template>
<button @click="count++">Clicks: {{ count }}</button>
</template>2) Submit de formulario con .prevent
<script setup lang="ts">
import { ref } from "vue";
const email = ref("");
function submitForm() {
if (!email.value.trim()) return;
console.log("Enviar:", email.value);
}
</script>
<template>
<form @submit.prevent="submitForm">
<input v-model="email" type="email" placeholder="tu@email.com" />
<button type="submit">Enviar</button>
</form>
</template><script lang="ts">
export default {
data() {
return {
email: "",
};
},
methods: {
submitForm() {
if (!this.email.trim()) return;
console.log("Enviar:", this.email);
},
},
};
</script>
<template>
<form @submit.prevent="submitForm">
<input v-model="email" type="email" placeholder="tu@email.com" />
<button type="submit">Enviar</button>
</form>
</template>3) Atajo de teclado con modificadores
<script setup lang="ts">
import { ref } from "vue";
const note = ref("");
function saveDraft() {
console.log("Borrador guardado:", note.value);
}
</script>
<template>
<textarea v-model="note" @keydown.ctrl.enter.prevent="saveDraft" />
</template><script lang="ts">
export default {
data() {
return {
note: "",
};
},
methods: {
saveDraft() {
console.log("Borrador guardado:", this.note);
},
},
};
</script>
<template>
<textarea v-model="note" @keydown.ctrl.enter.prevent="saveDraft" />
</template>4) Evento personalizado desde un componente hijo
<script setup lang="ts">
import TaskForm from "./TaskForm.vue";
function handleSave(taskTitle: string) {
console.log("Nueva tarea:", taskTitle);
}
</script>
<template>
<TaskForm @save="handleSave" />
</template><script lang="ts">
import TaskForm from "./TaskForm.vue";
export default {
components: { TaskForm },
methods: {
handleSave(taskTitle) {
console.log("Nueva tarea:", taskTitle);
},
},
};
</script>
<template>
<TaskForm @save="handleSave" />
</template>Ejemplo completo
Componente de lista de tareas con:
@submit.preventpara crear tareas.@clickpara marcar como completada.@keydown.enterpara envío rápido.
<script setup lang="ts">
import { computed, ref } from "vue";
type Task = {
id: number;
title: string;
done: boolean;
};
const draft = ref("");
const tasks = ref<Task[]>([]);
const remaining = computed(() => tasks.value.filter((task) => !task.done).length);
function addTask() {
const title = draft.value.trim();
if (!title) return;
tasks.value.push({
id: Date.now(),
title,
done: false,
});
draft.value = "";
}
function toggleTask(id: number) {
const task = tasks.value.find((item) => item.id === id);
if (!task) return;
task.done = !task.done;
}
</script>
<template>
<section>
<h2>Tareas</h2>
<p>Pendientes: {{ remaining }}</p>
<form @submit.prevent="addTask">
<input
v-model="draft"
type="text"
placeholder="Escribe una tarea y pulsa Enter"
@keydown.enter.prevent="addTask"
/>
<button type="submit">Agregar</button>
</form>
<ul>
<li v-for="task in tasks" :key="task.id">
<button @click="toggleTask(task.id)">
{{ task.done ? "Reabrir" : "Completar" }}
</button>
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
</li>
</ul>
</section>
</template><script lang="ts">
export default {
data() {
return {
draft: "",
tasks: [],
};
},
computed: {
remaining() {
return this.tasks.filter((task) => !task.done).length;
},
},
methods: {
addTask() {
const title = this.draft.trim();
if (!title) return;
this.tasks.push({
id: Date.now(),
title,
done: false,
});
this.draft = "";
},
toggleTask(id) {
const task = this.tasks.find((item) => item.id === id);
if (!task) return;
task.done = !task.done;
},
},
};
</script>
<template>
<section>
<h2>Tareas</h2>
<p>Pendientes: {{ remaining }}</p>
<form @submit.prevent="addTask">
<input
v-model="draft"
type="text"
placeholder="Escribe una tarea y pulsa Enter"
@keydown.enter.prevent="addTask"
/>
<button type="submit">Agregar</button>
</form>
<ul>
<li v-for="task in tasks" :key="task.id">
<button @click="toggleTask(task.id)">
{{ task.done ? "Reabrir" : "Completar" }}
</button>
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
</li>
</ul>
</section>
</template>Resumen
v-on es la directiva que convierte plantillas estáticas en interfaces interactivas.
Si la usas con handlers claros y modificadores correctos, reduces bugs y mejoras legibilidad.
Puntos clave para recordar:
- Usa
@como shorthand dev-on. - Prefiere funciones del script sobre expresiones complejas inline.
- Aprovecha modificadores (
.prevent,.stop,.once) cuando sean necesarios. - Mantén consistencia entre Composition API y Options API para facilitar mantenimiento.
