Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
Los primeros tres posts de esta serie fueron, cada uno, sobre una pieza suelta: cómo se instala y arranca Angular (angular-desde-cero), cómo está armado por dentro un componente standalone (primer-componente-angular), y cómo se conectan datos y plantillas con binding y el nuevo control de flujo (data-binding-control-flujo-angular). Ninguno de los tres mostró esas piezas juntas, funcionando a la vez. Este post hace eso: arma una mini lista de tareas usando solo lo que ya se explicó -nada de signals, nada de servicios, nada de comunicación entre componentes-, la corre de verdad con ng serve y va mostrando capturas reales de cada paso, no solo el código.
Es un solo componente, standalone, sin dividir en padre e hijo: separarlo en dos hubiera necesitado pasar datos entre componentes (@Input/@Output o sus equivalentes en signals), y eso todavía no tiene su post en la serie.
Paso 1: generar el componente
Como en el segundo post, el componente sale con ng generate component, y desde la versión 20 del CLI el nombre de archivo y de clase no llevan el sufijo .component:
Ojo con esto: si te tira un error de versión de Node al correr el comando (The Angular CLI requires a minimum Node.js version of...), es porque las versiones nuevas de la CLI van subiendo el mínimo de Node bastante seguido. Actualizá Node a lo que pida el error o, si no podés en el momento, usá una versión anterior de la CLI (npx @angular/cli@21 generate component ..., por ejemplo): todo lo que se explica acá vale igual desde varias versiones atrás, porque no usa nada que haya cambiado entre medio.
El comando reemplaza el <p>lista-tareas works!</p> que trae por defecto, y deja el componente con la misma anatomía que ya vimos en el post 2: selector, templateUrl, styleUrl, sin NgModule. Documentación del comando: ng generate component.
Paso 2: el modelo de datos
Nada de signals acá: el estado va en propiedades de clase comunes, como ya se hizo en los posts anteriores de la serie.
Y el estado inicial del componente, con tres tareas de ejemplo ya cargadas para que la primera captura no arranque con la lista vacía:
lista-tareas.ts (estado)
protected tareas: Tarea[] = [ { id: 1, texto: 'Terminar el post de data binding', hecha: true }, { id: 2, texto: 'Armar la mini app de la lista de tareas', hecha: false }, { id: 3, texto: 'Repasar el temario antes del post de signals', hecha: false },];protected nuevaTarea = '';protected filtro: 'todas' | 'pendientes' | 'completadas' = 'todas';private siguienteId = 4;
Paso 3: agregar tareas
El input y el botón usan lo mismo que ya se vio en el post 3: property binding para el valor y para deshabilitar el botón, event binding para reaccionar a lo que escribe el usuario.
lista-tareas.html (agregar)
<input type="text" placeholder="¿Qué hay que hacer?" [value]="nuevaTarea" (input)="nuevaTarea = $any($event.target).value" (keyup.enter)="agregarTarea()"/><button type="button" [disabled]="!nuevaTarea.trim()" (click)="agregarTarea()"> Agregar</button>
El $any($event.target) es el mismo escape hatch de tipos que se mencionó en la sección de expresiones de template del post anterior: $event.target llega tipado como EventTarget, que no tiene .value, así que hace falta ese cast para leer el texto escrito. No se usa [(ngModel)] a propósito: el two-way binding se mencionó de pasada en el post 3, pero se explica en profundidad recién en el post de formularios, más adelante en la serie.
Marcar una tarea como hecha es otro event binding, y el tachado depende de un class binding puntual ([class.completada]), la misma herramienta que ya se usó en el post 3.
Antes de meter el filtro, la versión más simple de la lista es el mismo patrón que ya apareció en lista-tareas.html del post 3: @for con track obligatorio, y @empty para cuando no hay nada que mostrar.
lista-tareas.html (listado simple)
<ul class="tareas"> @for (tarea of tareas; track tarea.id) { <li [class.completada]="tarea.hecha"> <!-- ...el contenido del paso anterior... --> </li> } @empty { <li class="vacio">Todavía no agregaste ninguna tarea.</li> }</ul>
Paso 6: filtrar con @switch
Para elegir entre "todas", "pendientes" y "completadas" entra @switch/@case/@default, el mismo bloque que se explicó en filtro.html del post anterior. Cada caso tiene su propio @for/@empty, alimentado por un getter distinto:
lista-tareas.ts (getters de filtro)
protected get pendientesLista(): Tarea[] { return this.tareas.filter((tarea) => !tarea.hecha);}protected get completadasLista(): Tarea[] { return this.tareas.filter((tarea) => tarea.hecha);}
Repetir el @for tres veces (uno por caso) es un poco más largo que tener un único getter tareasFiltradas, pero deja cada rama con su propio mensaje de @empty, que es justo lo que hace falta acá: "no hay pendientes" no es lo mismo que "no hay completadas". Los botones de filtro son tres event bindings que cambian la propiedad filtro, con un class binding para marcar cuál está activo:
El objeto host del decorador, la forma recomendada desde el post 2, sirve acá para marcar el propio elemento <app-lista-tareas> cuando la lista está completamente vacía (sin ninguna tarea, más allá del filtro):
Nada de esto es nuevo -el encapsulamiento por componente y el selector :host ya se explicaron en el post 2-, pero es el CSS el que hace que las capturas de abajo se vean así y no como HTML sin estilar, así que va completo en vez de dejarlo afuera:
Dos detalles que valen una mención rápida: el :host.sin-tareas es el que engancha con el host binding del paso anterior ([class.sin-tareas]), y el rojo (#dd0031) es el mismo rojo de marca que ya usaron los posts anteriores en las tablas de versiones y comandos, reutilizado acá para el contador y el botón de agregar.
Paso 9: correr ng serve y mirarlo andar
Acá está el punto central del post. Todo lo de arriba compiló con un ng build real -no el stub de tsc que se usó para verificar los fragmentos de los posts anteriores, sin un compilador de Angular real disponible en esa verificación-, y después se corrió con ng serve de verdad:
terminal
$ ng serve --port 4300Application bundle generation complete.Local: http://localhost:4300/
Estado inicial
Con las tres tareas de ejemplo precargadas y el filtro en "Todas":
Agregar una tarea
Escribiendo en el input y apretando Enter (el mismo modificador de tecla keyup.enter del post 3):
Completarla
Un click en el círculo la marca como hecha, dispara el [class.completada] y el contador de pendientes baja:
Filtrar
El botón "Pendientes" cambia filtro y el @switch muestra solo esa rama:
Lo mismo con "Completadas":
Y cuando no hay nada
Eliminando todas las tareas completadas mientras ese filtro sigue activo aparece la rama @empty correspondiente, no un mensaje genérico:
Lo que quedó afuera a propósito
Esta mini app podría ser mucho más prolija con lo que viene después en la serie: el estado en signal() en vez de propiedades comunes, un servicio con inject() en vez de tener toda la lógica en el propio componente, y un componente TareaItem separado usando input()/output() en vez de resolver cada <li> inline. Nada de eso está acá a propósito: son, en ese orden, los próximos posts de la serie, y este post existe justamente para mostrar que lo que ya se explicó alcanza para armar algo que funciona de punta a punta antes de sumar la próxima capa.
Lo que compila con un stub de tsc y lo que corre de verdad con ng serve no siempre son la misma garantía: acá se pudo usar ng build real, la verificación más fuerte que tuvo esta serie hasta ahora. Si estás siguiendo estos posts para aprender Angular, vale la pena hacer lo mismo con cada mini ejemplo que veas: no te quedes en que compile, corré la app y mirala andar.