AngularJSYour First App

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

Bash
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.

Bash
ng generate interface models/task

TS
// 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.

Bash
ng generate service services/task

TS
// 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));
  }
}
Note
We are using Angular Signals here — the modern reactive state primitive introduced in Angular 16. Signals are simpler than RxJS for local component state.
Step 4 — Create the Task Item Component

Bash
ng generate component components/task-item --standalone

TS
// 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

Bash
ng generate component components/add-task --standalone

TS
// 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 = '';
    }
  }
}
Note
We are using [(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

TS
// 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);
  }
}
Tip
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

Bash
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

Bash
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
  1. Add localStorage persistence — save tasks between page refreshes

  2. Add filtering — show All / Active / Completed tasks using a computed signal

  3. Add routing — separate pages for Dashboard and About using the Angular Router

  4. Replace in-memory data with a real API using HttpClient

  5. Add form validation with Reactive Forms

  6. 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