En esta página
Proyecto final: TaskBoard completo
Proyecto final: TaskBoard
En esta leccion final, integraremos todo lo aprendido para construir un TaskBoard completo estilo Kanban. Esta aplicación demuestra los conceptos fundamentales de Angular 21.
Conceptos integrados
Este proyecto utiliza:
| Concepto | Leccion | Uso en el proyecto |
|---|---|---|
| Componentes | L2 | TaskBoard, TaskColumn, TaskCard |
| Templates | L3 | @if, @for, @switch con control flow |
| Signals | L4 | Estado de tareas, filtros |
| Computed | L4 | Estadisticas, filtrado por columna |
| Effects | L5 | Persistencia en localStorage |
| Inputs/Outputs | L6 | Comunicación entre componentes |
| Servicios | L7 | TaskService centralizado |
| Routing | L8 | Navegación entre vistas |
| Formularios | L9 | Creación de tareas |
| Pipes | L12 | Formateo de fechas y texto |
Arquitectura del proyecto
task-board/
task.service.ts # Estado global con signals
task-board.ts # Componente contenedor principal
task-board.html # Template del board
task-column.ts # Columna del kanban (pendiente/progreso/completada)
task-card.ts # Tarjeta individual de tareaTaskService: estado centralizado
El servicio gestiona todo el estado de las tareas. Usa signals para reactividad y computed para derivar las columnas:
// Estado privado
private readonly _tareas = signal<Tarea[]>([]);
// Lecturas publicas
readonly tareas = this._tareas.asReadonly();
readonly pendientes = computed(() =>
this._tareas().filter(t => t.estado === 'pendiente')
);El patrón clave es: estado privado, lectura pública, mutacion via métodos.
TaskColumn: componente presentacional
Cada columna recibe su lista de tareas via input y emite eventos al padre:
@Component({
selector: 'app-task-column',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TaskCard],
template: `
<div class="columna">
<h2>{{ título() }} ({{ tareas().length }})</h2>
@for (tarea of tareas(); track tarea.id) {
<app-task-card
[tarea]="tarea"
(mover)="mover.emit($event)"
(eliminar)="eliminar.emit($event)"
/>
} @empty {
<p class="vacio">Sin tareas</p>
}
</div>
`,
})
export class TaskColumn {
readonly título = input.required<string>();
readonly tareas = input.required<Tarea[]>();
readonly mover = output<{ id: number; estado: Tarea['estado'] }>();
readonly eliminar = output<number>();
}TaskCard: tarjeta individual
Cada tarjeta muestra la información de una tarea con acciones:
@Component({
selector: 'app-task-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<article class="card" [class]="'prioridad-' + tarea().prioridad">
<h3>{{ tarea().título }}</h3>
@if (tarea().descripción) {
<p>{{ tarea().descripción }}</p>
}
<div class="acciones">
@switch (tarea().estado) {
@case ('pendiente') {
<button (click)="mover.emit({ id: tarea().id, estado: 'progreso' })">
Iniciar
</button>
}
@case ('progreso') {
<button (click)="mover.emit({ id: tarea().id, estado: 'completada' })">
Completar
</button>
}
}
<button (click)="eliminar.emit(tarea().id)">Eliminar</button>
</div>
</article>
`,
})
export class TaskCard {
readonly tarea = input.required<Tarea>();
readonly mover = output<{ id: number; estado: Tarea['estado'] }>();
readonly eliminar = output<number>();
}Persistencia con effect
Agrega persistencia local con un effect en el servicio:
constructor() {
// Cargar tareas guardadas
const guardadas = localStorage.getItem('tareas');
if (guardadas) {
this._tareas.set(JSON.parse(guardadas));
}
// Guardar automaticamente cada vez que cambian
effect(() => {
localStorage.setItem('tareas', JSON.stringify(this._tareas()));
});
}Desafios para seguir aprendiendo
- Drag and drop — Implementa arrastrar tarjetas entre columnas usando CDK DragDrop
- Filtros — Agrega filtrado por prioridad usando un signal + computed
- Busqueda — Usa un FormControl con debounceTime para buscar tareas
- API real — Conecta con Firebase Firestore usando HttpClient o @angular/fire
- Tests — Escribe tests unitarios para TaskService con Vitest
- Routing — Agrega una vista de detalle de tarea con parametros de ruta
Felicidades
Has completado el curso Angular para Desarrolladores. Ahora tienes las bases para construir aplicaciones Angular 21 modernas con:
- Componentes standalone y signals
- Control flow nativo en templates
- Servicios con inyección de dependencias
- Formularios reactivos con validación
- HttpClient y RxJS
- SSR y pre-rendering
- Testing básico
Sigue practicando construyendo proyectos reales. Consulta la documentación oficial en angular.dev para profundizar en cada tema.
// === task.service.ts ===
import { Injectable, signal, computed } from '@angular/core';
interface Tarea {
id: number;
título: string;
descripción: string;
estado: 'pendiente' | 'progreso' | 'completada';
prioridad: 'alta' | 'media' | 'baja';
creada: Date;
}
@Injectable({ providedIn: 'root' })
export class TaskService {
private readonly _tareas = signal<Tarea[]>([]);
readonly tareas = this._tareas.asReadonly();
readonly pendientes = computed(
() => this._tareas().filter(t => t.estado === 'pendiente')
);
readonly enProgreso = computed(
() => this._tareas().filter(t => t.estado === 'progreso')
);
readonly completadas = computed(
() => this._tareas().filter(t => t.estado === 'completada')
);
readonly totalTareas = computed(() => this._tareas().length);
readonly porcentajeCompletado = computed(() => {
const total = this._tareas().length;
if (total === 0) return 0;
return Math.round(
(this._tareas().filter(t => t.estado === 'completada').length / total) * 100
);
});
agregar(título: string, descripción: string, prioridad: Tarea['prioridad']): void {
this._tareas.update(lista => [
...lista,
{
id: Date.now(),
título,
descripción,
estado: 'pendiente',
prioridad,
creada: new Date(),
},
]);
}
moverEstado(id: number, estado: Tarea['estado']): void {
this._tareas.update(lista =>
lista.map(t => t.id === id ? { ...t, estado } : t)
);
}
eliminar(id: number): void {
this._tareas.update(lista => lista.filter(t => t.id !== id));
}
}
// === task-board.ts ===
import {
Component, inject, signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { TaskService } from './task.service';
import { TaskColumn } from './task-column';
@Component({
selector: 'app-task-board',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, TaskColumn],
templateUrl: './task-board.html',
})
export class TaskBoard {
readonly taskService = inject(TaskService);
private readonly fb = inject(FormBuilder);
readonly mostrarFormulario = signal(false);
readonly formulario = this.fb.nonNullable.group({
título: ['', [Validators.required, Validators.minLength(3)]],
descripción: [''],
prioridad: ['media' as const, Validators.required],
});
agregarTarea(): void {
if (this.formulario.invalid) return;
const { título, descripción, prioridad } = this.formulario.getRawValue();
this.taskService.agregar(título, descripción, prioridad);
this.formulario.reset({ título: '', descripción: '', prioridad: 'media' });
this.mostrarFormulario.set(false);
}
}
Inicia sesión para guardar tu progreso