División de componentes

El App.vue actual ya tiene todas las funciones completas, pero todo el código está amontonado junto.

En el siguiente paso dividiremos los componentes y aprenderás:

  • Cómo escribir subcomponentes altamente reutilizables en el entorno de Tailwind v4.
  • Cómo usar defineProps y defineEmits para pasar datos entre componentes JS.

Ahora, en elsrc/componentsdirectorio, crea los siguientes cuatro archivos:

  • TaskHeader.vue(solo visualización)
  • TaskInput.vue(lógica de entrada)
  • TaskFilter.vue(cambio de estado)
  • TaskItem.vue(tarea individual)

TaskHeader.vue —— Componente de visualización estática

Este componente no necesita ninguna lógica JS, solo mostrar la interfaz de usuario (UI).

En Tailwind v4, seguimos usando la nueva sintaxis de degradados.

Ejemplo

<template>
  <header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
    <h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
    <p class="text-blue-100/80 text-sm">Vue 3 + Tailwind v4 en la práctica</p>
  </header>
</template>

TaskInput.vue —— Uso de defineEmits para enviar datos

Este componente se encarga de obtener la entrada del usuario. Cuando el usuario hace clic en añadir, no modifica los datos directamente, sino que notifica al componente padre mediante emit.

Ejemplo

<script setup>
import { ref } from 'vue';

// Define el nombre del evento que el componente emite hacia afuera
const emit = defineEmits(['add-task']);

const title = ref('');

const handleSubmit = () => {
  const value = title.value.trim();
  if (!value) return;
 
// Envía el evento y pasa los datos como parámetro
  emit('add-task', value);
  title.value = '';
};
</script>

<template>
  <div class="flex gap-2 mb-8">
    <input
     v-model="title"
     @keyup.enter="handleSubmit"
     placeholder="¿Qué quieres completar hoy?"
     class="flex-1 bg-slate-50 border-none rounded-2xl px-4 py-3 focus:ring-2 focus:ring-brand/50 outline-none transition-all"
   />
    <button @click="handleSubmit" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
      +
    </button>
  </div>
</template>

TaskFilter.vue —— Comunicación v-model entre componentes

En Vue 3.4+, se recomienda usar defineModel() para implementar el enlace bidireccional. Esto es muy eficiente en la alternancia de UI en Tailwind v4.

Ejemplo

<script setup>
// Define v-model; el valor enviado por el componente padre se sincroniza automáticamente
const modelValue = defineModel();

const filters = ['all', 'active', 'completed'];
</script>

<template>
  <div class="flex p-1 bg-slate-100 rounded-xl mb-6">
    <button
     v-for="f in filters"
     :key="f"
     @click="modelValue = f"
     :class="[
       'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
       modelValue === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
     ]"

   >
      {{ f }}
    </button>
  </div>
</template>

TaskItem.vue —— Uso de defineProps para recibir datos

Es el subcomponente más central, encargado de mostrar una tarea individual y reportar las operaciones de modificación/eliminación.

Ejemplo

<script setup>
// Recibe el objeto de tarea pasado por el componente padre
defineProps({
  task: {
    type: Object,
    required: true
  }
});

// Define el evento para notificar al componente padre
const emit = defineEmits(['toggle', 'remove']);
</script>

<template>
  <li class="group flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-slate-200 hover:bg-white transition-all">
    <div class="flex items-center gap-3">
      <input
       type="checkbox"
       :checked="task.isCompleted"
       @change="emit('toggle', task.id)"
       class="w-5 h-5 accent-brand cursor-pointer"
     />
      <span :class="['text-slate-700 font-medium', task.isCompleted ? 'line-through text-slate-400 opacity-50' : '']">
        {{ task.title }}
      </span>
    </div>
    <button @click="emit('remove', task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
      ✕
    </button>
  </li>
</template>

App.vue refactorizado (centro de lógica)

El App.vue actual se ha convertido en un coordinador limpio, que solo se encarga de gestionar los datos originales y procesar las instrucciones enviadas por los subcomponentes.

Ejemplo

<script setup>
import { ref, computed } from 'vue';
// Importa subcomponentes
import TaskHeader from './components/TaskHeader.vue';
import TaskInput from './components/TaskInput.vue';
import TaskFilter from './components/TaskFilter.vue';
import TaskItem from './components/TaskItem.vue';

// --- Estado de datos ---
const tasks = ref([
{ id: '1', title: 'Experimenta las nuevas características de Tailwind v4', isCompleted: false },
{ id: '2', title: 'Domina la Composition API', isCompleted: true }
]);
const filter = ref('all');

// --- Lógica de procesamiento ---
const handleAddTask = (title) => {
  tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false });
};

const handleRemoveTask = (id) => {
  tasks.value = tasks.value.filter(t => t.id !== id);
};

const handleToggleTask = (id) => {
  const task = tasks.value.find(t => t.id === id);
  if (task) task.isCompleted = !task.isCompleted;
};

// --- Propiedades computadas ---
const filteredTasks = computed(() => {
  if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
  if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
  return tasks.value;
});
</script>

<template>
  <div class="min-h-screen py-12 px-4">
    <div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
     
      <TaskHeader />

      <main class="p-6">
        <TaskInput @add-task="handleAddTask" />

        <TaskFilter v-model="filter" />

        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
             v-for="task in filteredTasks"
             :key="task.id"
             :task="task"
             @toggle="handleToggleTask"
             @remove="handleRemoveTask"
           />
          </TransitionGroup>
        </ul>

        <div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
No hay tareas relacionadas...
        </div>
      </main>
    </div>
  </div>
</template>

<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>

Resumen de los puntos clave

defineProps(De padre a hijo):

  • TaskItemA través depropsse reciben los datos de la tarea. En Vue, las Props sonde solo lectura. El subcomponente nunca debe modificar directamentepropsel valor de [props], esto es el "flujo de datos unidireccional".

defineEmits(De hijo a padre):

  • Cuando el subcomponente necesita cambiar datos (como eliminar o marcar), lo hace a través deemitemitir una señal, y el componente padre ejecuta la lógica de modificación real. Esto garantiza que solo haya una fuente para modificar los datos, lo que facilita la depuración.

defineModel(La herramienta esencial del enlace bidireccional):

  • EnTaskFilterEn, utilizamos la función introducida en Vue 3.4defineModel. Simplifica enormemente el código de sincronización de estado entre componentes padre e hijo, ya no es necesario escribir manualmentepropsy escuchar@update:modelValue。

Reutilización en Tailwind v4:

  • Incluso al dividir los componentes, los nombres de clases atómicas de Tailwind siguen teniendo efecto. Porque el plugin de Vite escanea automáticamentesrctodos los.vuearchivos y genera los estilos correspondientes.
Otras extensiones