Your First Angular App
In this tutorial you will build a Task Manager application from scratch. By the end you will have a working app that lets users add tasks, mark them as complete, and delete them. Along the way you will touch on every core Angular concept: components, templates, services, data binding, and change detection.
What We Are Building
A task list that displays all tasks
An input to add new tasks
A checkbox to toggle task completion
A delete button per task
A task counter showing remaining items
Step 1 — Create the Project
ng new task-manager --standalone --style=css --routing=false --skip-tests cd task-manager ng serve -o
Step 2 — Define the Task Model
Create a TypeScript interface to represent a task. Interfaces keep your data shapes explicit and type-safe.
ng generate interface models/task
// src/app/models/task.ts
export interface Task {
id: number;
title: string;
completed: boolean;
}Step 3 — Create the Task Service
The service manages the task data. Components will ask the service to add, toggle, and delete tasks rather than managing state themselves.
ng generate service services/task
// src/app/services/task.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { Task } from '../models/task';
@Injectable({ providedIn: 'root' })
export class TaskService {
// Signal-based state — reactive and simple
private tasks = signal<Task[]>([
{ id: 1, title: 'Learn Angular basics', completed: true },
{ id: 2, title: 'Build a task manager app', completed: false },
{ id: 3, title: 'Master component lifecycle', completed: false },
]);
// Computed value — auto-updates when tasks signal changes
readonly allTasks = this.tasks.asReadonly();
readonly remainingCount = computed(
() => this.tasks().filter(t => !t.completed).length
);
private nextId = 4;
addTask(title: string): void {
if (!title.trim()) return;
this.tasks.update(tasks => [
...tasks,
{ id: this.nextId++, title: title.trim(), completed: false },
]);
}
toggleTask(id: number): void {
this.tasks.update(tasks =>
tasks.map(t => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}
deleteTask(id: number): void {
this.tasks.update(tasks => tasks.filter(t => t.id !== id));
}
}Step 4 — Create the Task Item Component
ng generate component components/task-item --standalone
// src/app/components/task-item/task-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../../models/task';
@Component({
selector: 'app-task-item',
standalone: true,
template: `
<li [class.completed]="task.completed">
<input
type="checkbox"
[checked]="task.completed"
(change)="onToggle()"
/>
<span>{{ task.title }}</span>
<button (click)="onDelete()">Delete</button>
</li>
`,
styles: [`
li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
li.completed span {
text-decoration: line-through;
color: #999;
}
span { flex: 1; }
button {
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
`],
})
export class TaskItemComponent {
@Input({ required: true }) task!: Task;
@Output() toggle = new EventEmitter<void>();
@Output() delete = new EventEmitter<void>();
onToggle() { this.toggle.emit(); }
onDelete() { this.delete.emit(); }
}Step 5 — Create the Add Task Component
ng generate component components/add-task --standalone
// src/app/components/add-task/add-task.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-add-task',
standalone: true,
template: `
<form (ngSubmit)="onSubmit()">
<input
type="text"
[(ngModel)]="newTaskTitle"
placeholder="What needs to be done?"
name="taskTitle"
/>
<button type="submit" [disabled]="!newTaskTitle.trim()">Add Task</button>
</form>
`,
styles: [`
form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
button {
padding: 0.5rem 1rem;
background: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:disabled { opacity: 0.5; cursor: default; }
`],
})
export class AddTaskComponent {
newTaskTitle = '';
@Output() add = new EventEmitter<string>();
onSubmit() {
if (this.newTaskTitle.trim()) {
this.add.emit(this.newTaskTitle);
this.newTaskTitle = '';
}
}
}[(ngModel)] here — two-way binding with the Forms module. You need to import FormsModule for this to work. We will add that in the next step.Step 6 — Update AppComponent to Wire Everything Together
// src/app/app.component.ts
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TaskService } from './services/task.service';
import { TaskItemComponent } from './components/task-item/task-item.component';
import { AddTaskComponent } from './components/add-task/add-task.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule, TaskItemComponent, AddTaskComponent],
template: `
<main>
<h1>Task Manager</h1>
<p class="counter">
{{ taskService.remainingCount() }} task(s) remaining
</p>
<app-add-task (add)="onAddTask($event)" />
<ul>
@for (task of taskService.allTasks(); track task.id) {
<app-task-item
[task]="task"
(toggle)="onToggle(task.id)"
(delete)="onDelete(task.id)"
/>
} @empty {
<li class="empty">No tasks yet. Add one above!</li>
}
</ul>
</main>
`,
styles: [`
main {
max-width: 600px;
margin: 2rem auto;
padding: 1.5rem;
font-family: system-ui, sans-serif;
}
h1 { color: #333; margin-bottom: 0.25rem; }
.counter { color: #666; margin-bottom: 1.5rem; }
ul { list-style: none; padding: 0; margin: 0; }
.empty { color: #999; text-align: center; padding: 2rem; }
`],
})
export class AppComponent {
taskService = inject(TaskService); // inject() — modern DI syntax
onAddTask(title: string) {
this.taskService.addTask(title);
}
onToggle(id: number) {
this.taskService.toggleTask(id);
}
onDelete(id: number) {
this.taskService.deleteTask(id);
}
}inject(TaskService) is the modern, standalone-friendly way to inject services. It is equivalent to declaring private taskService: TaskService in the constructor, but works outside of constructors too.What Each Angular Concept You Just Used
Concept | Where You Used It | What It Did |
|---|---|---|
@Component | Every component file | Turned a class into an Angular component |
@Input() | TaskItemComponent | Received task data from the parent |
@Output() + EventEmitter | TaskItemComponent, AddTaskComponent | Sent events up to the parent |
[(ngModel)] | AddTaskComponent template | Two-way bound the input field to a class property |
inject() | AppComponent | Injected TaskService without a constructor |
signal() | TaskService | Created reactive state that auto-updates the UI |
computed() | TaskService | Derived remainingCount from tasks signal |
@for / @empty | AppComponent template | Iterated over tasks; showed fallback when empty |
[class.completed] | TaskItemComponent | Conditionally added a CSS class |
(ngSubmit) | AddTaskComponent | Handled form submission event |
Running the App
ng serve -o
➜ Local: http://localhost:4200/
You should see a working task manager. Try:
- Typing in the input and pressing Enter to add a task
- Checking a task's checkbox to mark it complete (watch the counter update!)
- Clicking Delete to remove a task
- Deleting all tasks to see the "@empty" fallback message
Building for Production
ng build
Application bundle generation complete. Initial chunk files | Names | Raw size | Estimated transfer size main-XXXXXXXX.js | main | 48.23 kB | 13.81 kB polyfills-XXXXXXXX.js | polyfills | 34.00 kB | 11.64 kB Build at: dist/task-manager/browser
The dist/ folder contains everything needed to deploy — just upload its contents to any static hosting provider (Netlify, Vercel, GitHub Pages, Firebase Hosting, etc.).
Next Steps
Add localStorage persistence — save tasks between page refreshes
Add filtering — show All / Active / Completed tasks using a computed signal
Add routing — separate pages for Dashboard and About using the Angular Router
Replace in-memory data with a real API using HttpClient
Add form validation with Reactive Forms
Write unit tests for TaskService using Angular TestBed
Complete File List
src/app/models/task.ts — Task interface
src/app/services/task.service.ts — state management with signals
src/app/components/task-item/task-item.component.ts — single task row
src/app/components/add-task/add-task.component.ts — input form
src/app/app.component.ts — root, wires everything together