Angular Mini Project
This lesson combines many earlier topics into one small application: a task manager. Visitors add tasks, mark them done, and remove them. The project uses standalone components, signals, a service, and a reactive form. Building it shows you how the pieces fit together in a real application.
The Building Plan
A builder studies the house plan before laying bricks. This project needs a plan too. The application has one service that stores the tasks and two components that display them. The form component adds tasks, and the list component shows them.
+-------------------------------------------+ | AppComponent | | | | +--------------+ +-----------------+ | | | TaskForm | | TaskList | | | | adds a task | | shows, toggles, | | | | | | removes tasks | | | +------+-------+ +--------+--------+ | | | | | | v v | | +-------------------------+ | | | TaskService | | | | signal list of tasks | | | +-------------------------+ | +-------------------------------------------+
Step 1: Create the Project
Open the terminal and run the CLI commands below. They create the project and the three building blocks.
ng new task-manager
cd task-manager
ng generate service task
ng generate component task-form
ng generate component task-listStep 2: Describe the Data
Create an interface that defines one task. Every part of the application then agrees on the shape of the data.
export interface Task {
id: number;
title: string;
done: boolean;
}Step 3: Build the Service
The service owns the list. It exposes methods that add, toggle, and remove tasks. A computed signal counts the unfinished tasks.
import { Injectable, signal, computed } from '@angular/core';
import { Task } from './task';
@Injectable({ providedIn: 'root' })
export class TaskService {
tasks = signal<Task[]>([]);
remaining = computed(() => this.tasks().filter(t => !t.done).length);
add(title: string) {
const task = { id: Date.now(), title, done: false };
this.tasks.update(list => [...list, task]);
}
toggle(id: number) {
this.tasks.update(list =>
list.map(t => t.id === id ? { ...t, done: !t.done } : t));
}
remove(id: number) {
this.tasks.update(list => list.filter(t => t.id !== id));
}
}Step 4: Build the Form
The form component uses a reactive form with one required field. After a valid submit, it calls the service and clears the box.
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { TaskService } from '../task.service';
@Component({
selector: 'app-task-form',
imports: [ReactiveFormsModule],
template: `
<form (ngSubmit)="save()">
<input [formControl]="title" placeholder="New task">
<button [disabled]="title.invalid">Add</button>
</form>`
})
export class TaskFormComponent {
private service = inject(TaskService);
title = new FormControl('', { nonNullable: true, validators: Validators.required });
save() {
this.service.add(this.title.value);
this.title.reset();
}
}Step 5: Build the List
The list component reads the signal and loops over it. Each row has a checkbox and a remove button.
import { Component, inject } from '@angular/core';
import { TaskService } from '../task.service';
@Component({
selector: 'app-task-list',
template: `
<p>{{ service.remaining() }} tasks left</p>
<ul>
@for (task of service.tasks(); track task.id) {
<li>
<input type="checkbox" [checked]="task.done"
(change)="service.toggle(task.id)">
{{ task.title }}
<button (click)="service.remove(task.id)">Remove</button>
</li>
} @empty {
<li>No tasks yet</li>
}
</ul>`
})
export class TaskListComponent {
service = inject(TaskService);
}Step 6: Join the Parts
Import both components into the root component and place their selectors in the template.
@Component({
selector: 'app-root',
imports: [TaskFormComponent, TaskListComponent],
template: `
<h2>Task Manager</h2>
<app-task-form></app-task-form>
<app-task-list></app-task-list>`
})
export class AppComponent {}Run ng serve and open the browser. Add a task, tick it, and watch the remaining count change.
How the Data Flows
The form calls the service, and the service updates the signal. The list reads the same signal, so it refreshes without any direct link to the form. This design keeps both components independent. You can replace either one without touching the other.
Why This Design Works
The service holds the only copy of the task list. Components never keep their own copies, so they never disagree about what exists. Think of a shared whiteboard in a classroom. Every student reads the same board, and one change appears for everyone. The same idea prevents many bugs in larger applications.
Testing the Project
Try several actions to confirm that the application behaves correctly. Add a task with an empty box and check that the button stays disabled. Add three tasks, finish one, and verify that the counter shows two. Remove a task and confirm that the list updates at once. Refresh the page and notice that the tasks disappear. That behavior explains why the extension ideas below include local storage.
Ideas for Extension
- Save tasks to local storage with an effect.
- Add a filter that shows all, active, or finished tasks.
- Load starter tasks from an API with the HTTP client.
- Add routing for a separate About page.
- Write a unit test for each service method.
Key Points
- A service with signals can drive several components at once.
- Reactive forms add clean input handling.
- Small standalone components keep each file easy to read.
- Extend the project to practice routing, HTTP, and testing.
