Toolmingo
Guides24 min read

Angular Tutorial for Beginners (2025): Learn Angular Step by Step

Complete Angular tutorial for absolute beginners. Learn Angular components, services, routing, forms, and build real projects. Free guide with TypeScript examples.

Angular is a powerful, full-featured framework for building web applications — used by Google, Microsoft, Forbes, and thousands of enterprise companies worldwide. Unlike React or Vue, Angular gives you everything built-in: routing, forms, HTTP, state management, and a CLI. This tutorial takes you from zero to building real Angular applications, step by step.

What you'll learn

Topic What you'll be able to do
Setup Create an Angular app with the CLI
Components Build reusable UI building blocks
Templates Write Angular HTML with directives and bindings
Services Share data and logic across components
Routing Build multi-page SPAs
Forms Handle user input with Template and Reactive Forms
HTTP Fetch data from APIs
Signals Use Angular's modern reactivity system
Projects Build 3 real Angular apps

Angular version used: Angular 17/18 (latest stable as of 2025)

Prerequisites: Basic JavaScript knowledge. TypeScript basics help but aren't required — we'll explain TypeScript features as we go. If you're new to JavaScript, read JavaScript Tutorial for Beginners first.


Part 1 — Why Angular?

Feature Why it matters
Full framework Routing, forms, HTTP, DI — all built in, no library hunting
TypeScript-first Catch errors at compile time, better IDE support
CLI Generate components, services, modules with one command
Opinionated One "right way" makes large team codebases consistent
Enterprise adoption Preferred by large teams: Google, Microsoft, SAP
Performance Ahead-of-Time compilation, tree shaking, lazy loading
Long-term support Google commits to long-term support cycles

Angular vs React vs Vue

Factor Angular React Vue
Type Full framework UI library Progressive framework
Language TypeScript JS/TS JS/TS
Learning curve Steeper Moderate Gentle
Bundle size Larger (CLI optimizes) Smaller Smallest
Opinionated Yes No Somewhat
Best for Large enterprise apps Flexible apps Small-medium apps
Job market Enterprise focus Highest demand Growing
State management Signals/NgRx built-in Redux/Zustand external Pinia external

Part 2 — Setup

Install Angular CLI

npm install -g @angular/cli

# Verify installation
ng version

Create your first Angular app

ng new my-first-app
# Prompts:
# ✔ Which stylesheet format? → CSS
# ✔ Do you want to enable SSR? → No (for now)

cd my-first-app
ng serve

Open http://localhost:4200 — your Angular app is running.

Project structure

my-first-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts      # Root component (logic)
│   │   ├── app.component.html    # Root component (template)
│   │   ├── app.component.css     # Root component (styles)
│   │   └── app.config.ts         # App configuration (Angular 17+)
│   ├── main.ts                   # Entry point
│   └── index.html                # HTML shell
├── angular.json                  # Angular CLI config
├── tsconfig.json                 # TypeScript config
└── package.json                  # Dependencies

Key CLI commands

ng serve                    # Run dev server (localhost:4200)
ng build                    # Build for production
ng generate component foo   # Create a new component (ng g c foo)
ng generate service bar     # Create a new service (ng g s bar)
ng generate pipe my-pipe    # Create a new pipe
ng test                     # Run unit tests
ng lint                     # Lint the project

Part 3 — Your First Component

A component is the basic building block of an Angular app. Every visible piece of UI is a component.

What makes a component

A component has three parts:

  1. TypeScript class — the logic
  2. HTML template — the view
  3. CSS — the styles

Create a component

ng generate component hello

This creates src/app/hello/:

  • hello.component.ts
  • hello.component.html
  • hello.component.css
  • hello.component.spec.ts (tests)

hello.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',        // HTML tag name: <app-hello />
  standalone: true,             // Angular 17+: no NgModule needed
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css']
})
export class HelloComponent {
  name = 'World';
  greeting = 'Hello';
}

hello.component.html

<h1>{{ greeting }}, {{ name }}!</h1>
<p>Welcome to Angular.</p>

{{ }} is interpolation — it displays component properties in the template.

Use the component in app.component.html

// app.component.ts — import and add to imports array
import { Component } from '@angular/core';
import { HelloComponent } from './hello/hello.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [HelloComponent],   // ← add here
  templateUrl: './app.component.html',
})
export class AppComponent {}
<!-- app.component.html -->
<app-hello />

Part 4 — Templates and Data Binding

Angular templates are HTML with special Angular syntax for binding data and handling events.

Interpolation — display data

<p>{{ title }}</p>
<p>{{ 2 + 2 }}</p>
<p>{{ user.name.toUpperCase() }}</p>

Property binding — bind to HTML attributes

<!-- [property]="expression" -->
<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">Submit</button>
<div [class]="cssClass">Styled div</div>
<input [value]="username" />

Event binding — handle DOM events

<!-- (event)="handler()" -->
<button (click)="onClick()">Click me</button>
<input (input)="onInput($event)" />
<form (submit)="onSubmit($event)">...</form>
<div (mouseover)="onHover()">Hover me</div>
export class MyComponent {
  onClick() {
    console.log('clicked!');
  }

  onInput(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    console.log(value);
  }
}

Two-way binding — sync input and property

Requires FormsModule:

import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule],
  // ...
})
<input [(ngModel)]="username" />
<p>Hello, {{ username }}</p>

When user types, username updates. When username changes in code, the input updates.

Summary — binding syntax

Syntax Direction Example
{{ value }} Component → Template Display text
[property]="expr" Component → Template Set attribute
(event)="handler()" Template → Component Handle event
[(ngModel)]="prop" Both directions Form inputs

Part 5 — Directives

Directives extend HTML with new behavior. Angular has two types: structural (change DOM structure) and attribute (change element appearance/behavior).

@if — conditional rendering (Angular 17+)

@if (isLoggedIn) {
  <p>Welcome back!</p>
} @else {
  <p>Please log in.</p>
}

@for — render lists (Angular 17+)

@for (item of items; track item.id) {
  <li>{{ item.name }}</li>
} @empty {
  <li>No items found.</li>
}

track is required — it tells Angular how to identify items for efficient updates (like React's key).

Old syntax (still widely used)

<!-- ngIf -->
<div *ngIf="isLoggedIn; else loggedOut">Welcome!</div>
<ng-template #loggedOut><p>Please log in.</p></ng-template>

<!-- ngFor -->
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
trackById(index: number, item: { id: number }) {
  return item.id;
}

ngClass — dynamic CSS classes

<div [ngClass]="{ 'active': isActive, 'error': hasError }">...</div>
<div [ngClass]="status === 'done' ? 'success' : 'pending'">...</div>

ngStyle — dynamic inline styles

<div [ngStyle]="{ 'color': textColor, 'font-size': fontSize + 'px' }">...</div>

Complete example

import { Component } from '@angular/core';
import { NgClass } from '@angular/common';

interface Task {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  selector: 'app-tasks',
  standalone: true,
  imports: [NgClass],
  template: `
    <h2>Tasks ({{ tasks.length }})</h2>
    @if (tasks.length === 0) {
      <p>No tasks yet!</p>
    } @else {
      <ul>
        @for (task of tasks; track task.id) {
          <li [ngClass]="{ 'done': task.done }">
            {{ task.title }}
          </li>
        }
      </ul>
    }
  `,
  styles: ['.done { text-decoration: line-through; color: gray; }']
})
export class TasksComponent {
  tasks: Task[] = [
    { id: 1, title: 'Learn Angular', done: true },
    { id: 2, title: 'Build a project', done: false },
    { id: 3, title: 'Get a job', done: false },
  ];
}

Part 6 — Component Interaction

Passing data with @Input

Parent passes data to child via @Input():

// child.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ title }}</h3>
      <p>{{ description }}</p>
    </div>
  `
})
export class CardComponent {
  @Input() title = '';
  @Input() description = '';
}
// parent.component.ts
import { CardComponent } from './card/card.component';

@Component({
  standalone: true,
  imports: [CardComponent],
  template: `
    <app-card
      title="Angular Basics"
      description="Learn the fundamentals"
    />
  `
})
export class ParentComponent {}

Emitting events with @Output

Child notifies parent via @Output() and EventEmitter:

// button.component.ts
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-button',
  standalone: true,
  template: `<button (click)="handleClick()">{{ label }}</button>`
})
export class ButtonComponent {
  @Input() label = 'Click me';
  @Output() clicked = new EventEmitter<string>();

  handleClick() {
    this.clicked.emit('Button was clicked!');
  }
}
// parent.component.ts
@Component({
  imports: [ButtonComponent],
  template: `
    <app-button
      label="Submit"
      (clicked)="onButtonClicked($event)"
    />
    <p>{{ message }}</p>
  `
})
export class ParentComponent {
  message = '';

  onButtonClicked(msg: string) {
    this.message = msg;
  }
}

Part 7 — Services and Dependency Injection

A service is a class that holds shared logic or data. Dependency Injection (DI) is Angular's system for providing services to components automatically.

Create a service

ng generate service user
// user.service.ts
import { Injectable } from '@angular/core';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'   // ← available everywhere, one instance (singleton)
})
export class UserService {
  private users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  getUsers(): User[] {
    return this.users;
  }

  getUserById(id: number): User | undefined {
    return this.users.find(u => u.id === id);
  }

  addUser(user: Omit<User, 'id'>): User {
    const newUser = { id: Date.now(), ...user };
    this.users.push(newUser);
    return newUser;
  }
}

Inject the service into a component

// users.component.ts
import { Component, inject } from '@angular/core';
import { UserService, User } from '../user.service';

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    <h2>Users</h2>
    <ul>
      @for (user of users; track user.id) {
        <li>{{ user.name }} — {{ user.email }}</li>
      }
    </ul>
  `
})
export class UsersComponent {
  private userService = inject(UserService);  // Angular 14+ inject() function
  users: User[] = [];

  ngOnInit() {
    this.users = this.userService.getUsers();
  }
}

Alternative (constructor injection — classic style):

constructor(private userService: UserService) {}

Both work. inject() is more modern and works outside constructors.


Part 8 — Signals (Modern Reactivity)

Angular 16+ introduced Signals as the new reactivity system. Signals are reactive values that automatically track where they're used and notify consumers when they change.

Basic signals

import { Component, signal, computed, effect } from '@angular/core';

@Component({
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ double() }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
  `
})
export class CounterComponent {
  count = signal(0);                          // writable signal
  double = computed(() => this.count() * 2); // computed (derived) signal

  constructor() {
    effect(() => {
      console.log('Count changed to:', this.count());
    });
  }

  increment() { this.count.update(v => v + 1); }
  decrement() { this.count.update(v => v - 1); }
}

Signal methods

Method Use case
signal(value) Create a writable signal
signal.set(value) Replace the value
signal.update(fn) Update based on current value
computed(() => expr) Derived read-only signal
effect(() => { }) Side effects when signals change
signal() Read the signal value (call it like a function)

Signals vs traditional change detection

// OLD WAY — zone.js (still works)
export class OldComponent {
  count = 0;                      // plain property
  increment() { this.count++; }  // Angular detects change via zone.js
}

// NEW WAY — Signals (Angular 16+)
export class NewComponent {
  count = signal(0);              // signal
  increment() { this.count.update(v => v + 1); }
}

Signals provide fine-grained reactivity — only components that use a signal re-render when it changes.


Part 9 — Routing

Angular Router enables navigation between views without full page reloads (Single Page Application).

Set up routes

// app.config.ts (Angular 17+ standalone)
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { UserDetailComponent } from './user-detail/user-detail.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'users/:id', component: UserDetailComponent },
  { path: '**', redirectTo: '' }   // wildcard → redirect to home
];

Add router outlet

<!-- app.component.html -->
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
</nav>

<router-outlet />   <!-- routed component renders here -->
// app.component.ts — import RouterOutlet and RouterLink
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  imports: [RouterOutlet, RouterLink],
  // ...
})

Read route parameters

// user-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  standalone: true,
  template: `<p>User ID: {{ userId }}</p>`
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  userId = this.route.snapshot.paramMap.get('id');
}

Navigate programmatically

import { Router } from '@angular/router';

export class LoginComponent {
  private router = inject(Router);

  onLogin() {
    // ... authenticate
    this.router.navigate(['/dashboard']);
    // or: this.router.navigateByUrl('/dashboard');
  }
}

Lazy loading (performance)

Load feature modules only when navigated to:

// app.routes.ts
export const routes: Routes = [
  {
    path: 'admin',
    loadComponent: () =>
      import('./admin/admin.component').then(m => m.AdminComponent)
  }
];

Part 10 — Forms

Angular has two approaches to forms: Template-driven (simple) and Reactive (powerful).

Template-driven forms

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [FormsModule],
  template: `
    <form (ngSubmit)="onSubmit()" #myForm="ngForm">
      <input
        [(ngModel)]="name"
        name="name"
        required
        minlength="3"
        #nameField="ngModel"
      />
      @if (nameField.invalid && nameField.touched) {
        <span>Name must be at least 3 characters</span>
      }

      <input
        type="email"
        [(ngModel)]="email"
        name="email"
        required
        email
        #emailField="ngModel"
      />

      <button type="submit" [disabled]="myForm.invalid">Submit</button>
    </form>
  `
})
export class ContactFormComponent {
  name = '';
  email = '';

  onSubmit() {
    console.log({ name: this.name, email: this.email });
  }
}

Reactive forms

More control, better for complex validation:

import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="name" placeholder="Name" />
      @if (form.get('name')?.invalid && form.get('name')?.touched) {
        <span>Name is required (min 3 chars)</span>
      }

      <input formControlName="email" placeholder="Email" />
      @if (form.get('email')?.errors?.['email']) {
        <span>Enter a valid email</span>
      }

      <input formControlName="password" type="password" placeholder="Password" />
      @if (form.get('password')?.errors?.['minlength']) {
        <span>Password must be at least 8 characters</span>
      }

      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>
  `
})
export class RegistrationComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(3)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
  });

  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}

Template-driven vs Reactive forms

Factor Template-driven Reactive
Complexity Simple More code
Power Limited Full control
Validation HTML attributes TypeScript validators
Testing Harder Easier
Dynamic forms Awkward Easy
Best for Login/contact forms Complex wizards

Part 11 — HTTP Client

Angular's HttpClient fetches data from APIs. It returns Observables (from RxJS).

Configure HTTP

// app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient()   // ← add this
  ]
};

Create a data service

// post.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

@Injectable({ providedIn: 'root' })
export class PostService {
  private http = inject(HttpClient);
  private apiUrl = 'https://jsonplaceholder.typicode.com/posts';

  getPosts(): Observable<Post[]> {
    return this.http.get<Post[]>(this.apiUrl);
  }

  getPost(id: number): Observable<Post> {
    return this.http.get<Post>(`${this.apiUrl}/${id}`);
  }

  createPost(post: Omit<Post, 'id'>): Observable<Post> {
    return this.http.post<Post>(this.apiUrl, post);
  }

  updatePost(id: number, post: Partial<Post>): Observable<Post> {
    return this.http.patch<Post>(`${this.apiUrl}/${id}`, post);
  }

  deletePost(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

Use the service in a component

// posts.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { PostService, Post } from '../post.service';

@Component({
  standalone: true,
  template: `
    @if (loading) {
      <p>Loading...</p>
    } @else if (error) {
      <p>Error: {{ error }}</p>
    } @else {
      <ul>
        @for (post of posts; track post.id) {
          <li>{{ post.title }}</li>
        }
      </ul>
    }
  `
})
export class PostsComponent implements OnInit {
  private postService = inject(PostService);

  posts: Post[] = [];
  loading = true;
  error = '';

  ngOnInit() {
    this.postService.getPosts().subscribe({
      next: (posts) => {
        this.posts = posts.slice(0, 10);
        this.loading = false;
      },
      error: (err) => {
        this.error = err.message;
        this.loading = false;
      }
    });
  }
}

Modern approach with toSignal

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { PostService } from '../post.service';

@Component({
  standalone: true,
  template: `
    @if (posts()) {
      @for (post of posts()!; track post.id) {
        <li>{{ post.title }}</li>
      }
    }
  `
})
export class PostsComponent {
  private postService = inject(PostService);
  posts = toSignal(this.postService.getPosts());  // Observable → Signal
}

Part 12 — Pipes

Pipes transform data in templates.

Built-in pipes

<!-- date -->
<p>{{ today | date:'medium' }}</p>       <!-- Jul 16, 2025, 3:45:00 PM -->
<p>{{ today | date:'shortDate' }}</p>   <!-- 7/16/25 -->

<!-- currency -->
<p>{{ price | currency:'USD' }}</p>    <!-- $42.99 -->
<p>{{ price | currency:'EUR' }}</p>    <!-- €42.99 -->

<!-- number -->
<p>{{ ratio | percent }}</p>            <!-- 42% -->
<p>{{ bigNum | number:'1.0-2' }}</p>   <!-- 1,234.56 -->

<!-- string -->
<p>{{ title | uppercase }}</p>
<p>{{ title | lowercase }}</p>
<p>{{ title | titlecase }}</p>

<!-- array/string -->
<p>{{ longText | slice:0:100 }}</p>

<!-- object -->
<pre>{{ data | json }}</pre>

<!-- async (handles Observable/Promise) -->
<p>{{ user$ | async }}</p>

Built-in pipes reference

Pipe Example Output
date {{ d | date:'short' }} 7/16/25, 3:45 PM
currency {{ 9.99 | currency }} $9.99
percent {{ 0.42 | percent }} 42%
number {{ 1234.5 | number:'1.0-1' }} 1,234.5
uppercase {{ 'hi' | uppercase }} HI
lowercase {{ 'HI' | lowercase }} hi
titlecase {{ 'hello world' | titlecase }} Hello World
json {{ obj | json }} JSON string
slice {{ arr | slice:1:3 }} elements 1–2
async {{ obs$ | async }} unwrapped value

Create a custom pipe

ng generate pipe truncate
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 50, trail = '...'): string {
    if (value.length <= limit) return value;
    return value.substring(0, limit) + trail;
  }
}
<!-- Usage -->
<p>{{ article.body | truncate:100 }}</p>
<p>{{ article.body | truncate:50:'…' }}</p>

Part 13 — Lifecycle Hooks

Lifecycle hooks let you run code at specific points in a component's life.

import {
  Component, OnInit, OnChanges, OnDestroy,
  AfterViewInit, Input, SimpleChanges
} from '@angular/core';

@Component({ standalone: true, template: '' })
export class LifecycleComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
  @Input() data = '';

  // Called once after first render
  ngOnInit() {
    console.log('Component initialized');
    // Good place to fetch data
  }

  // Called when @Input() changes
  ngOnChanges(changes: SimpleChanges) {
    if (changes['data']) {
      console.log('data changed:', changes['data'].currentValue);
    }
  }

  // Called after view (and children) are rendered
  ngAfterViewInit() {
    // Good place to access DOM elements via @ViewChild
  }

  // Called just before component is destroyed
  ngOnDestroy() {
    // Clean up: unsubscribe, clear timers
    console.log('Component destroyed');
  }
}

Lifecycle hooks order

Hook When Common use
ngOnChanges Before init + on input changes React to input changes
ngOnInit After first ngOnChanges Fetch data, initialize
ngAfterViewInit After view renders Access DOM (ViewChild)
ngOnDestroy Before removal Cleanup subscriptions

Part 14 — RxJS Observables

Angular uses RxJS Observables for async operations. You don't need to master RxJS — just learn the basics.

Observable basics

import { Observable, of, from } from 'rxjs';
import { map, filter, tap, catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';

// Create
const nums$ = of(1, 2, 3, 4, 5);
const arr$ = from([10, 20, 30]);

// Transform
nums$.pipe(
  filter(n => n % 2 === 0),     // keep even numbers
  map(n => n * 10),              // multiply by 10
  tap(n => console.log(n))       // side effect (logging)
).subscribe(n => console.log(n)); // 20, 40

HTTP with operators

import { catchError, map, finalize } from 'rxjs/operators';
import { EMPTY } from 'rxjs';

this.http.get<Post[]>('/api/posts').pipe(
  map(posts => posts.slice(0, 5)),        // take first 5
  catchError(err => {
    console.error(err);
    return EMPTY;                          // return empty on error
  }),
  finalize(() => this.loading = false)    // always runs (like finally)
).subscribe(posts => this.posts = posts);

Essential RxJS operators for Angular

Operator What it does
map Transform each value
filter Keep values matching condition
tap Side effect without changing value
catchError Handle errors
switchMap Cancel previous, switch to new Observable
mergeMap Run all Observables concurrently
debounceTime Wait for pause before emitting
distinctUntilChanged Only emit when value changes
takeUntilDestroyed Unsubscribe when component destroys
finalize Run code when Observable completes

Auto-unsubscribe (Angular 16+)

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class MyComponent {
  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed())  // auto-cleans on destroy
      .subscribe(n => console.log(n));
  }
}

Part 15 — Project 1: Todo App

Build a simple todo app with add, complete, and delete functionality.

ng new todo-app --standalone
cd todo-app
ng generate component todo-list
ng generate service todo
// todo.service.ts
import { Injectable, signal, computed } from '@angular/core';

export interface Todo {
  id: number;
  text: string;
  done: boolean;
}

@Injectable({ providedIn: 'root' })
export class TodoService {
  private todos = signal<Todo[]>([
    { id: 1, text: 'Learn Angular', done: false },
    { id: 2, text: 'Build an app', done: false },
  ]);

  all = computed(() => this.todos());
  done = computed(() => this.todos().filter(t => t.done));
  pending = computed(() => this.todos().filter(t => !t.done));

  add(text: string) {
    this.todos.update(todos => [
      ...todos,
      { id: Date.now(), text, done: false }
    ]);
  }

  toggle(id: number) {
    this.todos.update(todos =>
      todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  }

  delete(id: number) {
    this.todos.update(todos => todos.filter(t => t.id !== id));
  }
}
// app.component.ts
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgClass } from '@angular/common';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, NgClass],
  template: `
    <div class="app">
      <h1>Todo App</h1>

      <form (ngSubmit)="addTodo()">
        <input
          [(ngModel)]="newText"
          name="newText"
          placeholder="Add a todo..."
          required
        />
        <button type="submit" [disabled]="!newText.trim()">Add</button>
      </form>

      <p>{{ todos.pending().length }} pending · {{ todos.done().length }} done</p>

      <ul>
        @for (todo of todos.all(); track todo.id) {
          <li [ngClass]="{ 'done': todo.done }">
            <input
              type="checkbox"
              [checked]="todo.done"
              (change)="todos.toggle(todo.id)"
            />
            {{ todo.text }}
            <button (click)="todos.delete(todo.id)">✕</button>
          </li>
        }
      </ul>
    </div>
  `,
  styles: [`
    .app { max-width: 400px; margin: 40px auto; font-family: sans-serif; }
    .done { text-decoration: line-through; opacity: 0.6; }
    li { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
    button { cursor: pointer; }
  `]
})
export class AppComponent {
  todos = inject(TodoService);
  newText = '';

  addTodo() {
    if (this.newText.trim()) {
      this.todos.add(this.newText.trim());
      this.newText = '';
    }
  }
}

Part 16 — Project 2: Weather App with HTTP

Fetch weather data from an API.

ng new weather-app --standalone
ng generate service weather
ng generate component weather-display
// weather.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface WeatherData {
  city: string;
  temp: number;
  feels_like: number;
  description: string;
  humidity: number;
}

@Injectable({ providedIn: 'root' })
export class WeatherService {
  private http = inject(HttpClient);

  // Using open-meteo.com (free, no API key)
  getWeather(lat: number, lon: number): Observable<WeatherData> {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code`;
    return this.http.get<any>(url).pipe(
      map(data => ({
        city: `${lat.toFixed(2)}, ${lon.toFixed(2)}`,
        temp: data.current.temperature_2m,
        feels_like: data.current.apparent_temperature,
        description: this.codeToDescription(data.current.weather_code),
        humidity: data.current.relative_humidity_2m,
      }))
    );
  }

  private codeToDescription(code: number): string {
    if (code === 0) return 'Clear sky';
    if (code <= 3) return 'Partly cloudy';
    if (code <= 48) return 'Foggy';
    if (code <= 67) return 'Rainy';
    if (code <= 77) return 'Snowy';
    if (code <= 82) return 'Showery';
    return 'Stormy';
  }
}
// app.component.ts
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { WeatherService, WeatherData } from './weather.service';
import { provideHttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule],
  template: `
    <div class="app">
      <h1>🌤 Weather</h1>

      <div class="search">
        <input [(ngModel)]="lat" name="lat" placeholder="Latitude" type="number" />
        <input [(ngModel)]="lon" name="lon" placeholder="Longitude" type="number" />
        <button (click)="search()" [disabled]="loading()">
          {{ loading() ? 'Loading...' : 'Get Weather' }}
        </button>
      </div>

      @if (error()) {
        <p class="error">{{ error() }}</p>
      }

      @if (weather()) {
        <div class="card">
          <h2>{{ weather()!.city }}</h2>
          <p class="temp">{{ weather()!.temp }}°C</p>
          <p>{{ weather()!.description }}</p>
          <p>Feels like {{ weather()!.feels_like }}°C</p>
          <p>Humidity {{ weather()!.humidity }}%</p>
        </div>
      }
    </div>
  `
})
export class AppComponent {
  private weatherService = inject(WeatherService);

  lat = 51.5;
  lon = -0.12;
  weather = signal<WeatherData | null>(null);
  loading = signal(false);
  error = signal('');

  search() {
    this.loading.set(true);
    this.error.set('');
    this.weatherService.getWeather(this.lat, this.lon).subscribe({
      next: (data) => {
        this.weather.set(data);
        this.loading.set(false);
      },
      error: (err) => {
        this.error.set('Failed to fetch weather: ' + err.message);
        this.loading.set(false);
      }
    });
  }
}

Part 17 — Project 3: Multi-Page Blog App

A blog with routing, HTTP, and a detail page.

ng new blog-app --standalone
ng generate component home
ng generate component post-detail
ng generate service post
// post.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

@Injectable({ providedIn: 'root' })
export class PostService {
  private http = inject(HttpClient);
  private url = 'https://jsonplaceholder.typicode.com/posts';

  getPosts() { return this.http.get<Post[]>(this.url); }
  getPost(id: number) { return this.http.get<Post>(`${this.url}/${id}`); }
}
// home.component.ts
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { PostService } from '../post.service';
import { map } from 'rxjs/operators';

@Component({
  standalone: true,
  imports: [RouterLink],
  template: `
    <h1>Blog</h1>
    @if (posts()) {
      @for (post of posts(); track post.id) {
        <article>
          <h2><a [routerLink]="['/post', post.id]">{{ post.title }}</a></h2>
          <p>{{ post.body | slice:0:100 }}...</p>
        </article>
      }
    } @else {
      <p>Loading...</p>
    }
  `
})
export class HomeComponent {
  private postService = inject(PostService);
  posts = toSignal(this.postService.getPosts().pipe(map(p => p.slice(0, 10))));
}
// post-detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { PostService } from '../post.service';
import { switchMap } from 'rxjs/operators';

@Component({
  standalone: true,
  imports: [RouterLink],
  template: `
    <a routerLink="/">← Back to blog</a>
    @if (post()) {
      <article>
        <h1>{{ post()!.title }}</h1>
        <p>{{ post()!.body }}</p>
      </article>
    } @else {
      <p>Loading...</p>
    }
  `
})
export class PostDetailComponent {
  private route = inject(ActivatedRoute);
  private postService = inject(PostService);

  post = toSignal(
    this.route.paramMap.pipe(
      switchMap(params => this.postService.getPost(Number(params.get('id'))))
    )
  );
}
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },
  { path: 'post/:id', loadComponent: () => import('./post-detail/post-detail.component').then(m => m.PostDetailComponent) },
];

Learning path

Stage Topics Time
1 — Fundamentals Components, templates, bindings 1–2 weeks
2 — Services & routing DI, HttpClient, Router 1–2 weeks
3 — Forms Template-driven, Reactive 1 week
4 — Signals Modern reactivity, toSignal 1 week
5 — State management NgRx or NgRx Signals 2–3 weeks
6 — Testing Jasmine, TestBed, Spectator 2 weeks
7 — Advanced Animations, PWA, SSR (Angular Universal) Ongoing

Common mistakes

Mistake Problem Fix
Not unsubscribing Memory leaks Use takeUntilDestroyed() or async pipe
Direct DOM manipulation Breaks change detection Use bindings and @ViewChild
Forgetting standalone: true Module error Add standalone: true to @Component
Using any type Loses TypeScript benefits Define proper interfaces
Mutating arrays/objects directly Change detection misses it Return new arrays/objects
Forgetting track in @for Performance warning Always add track item.id
Over-using ngOnChanges Complex code Prefer computed signals
Importing components in two modules Error With standalone, just import in imports: []

Angular vs related terms

Term What it is
Angular Full frontend framework by Google (TypeScript)
AngularJS Old Angular 1.x (completely different, legacy)
Angular CLI Command-line tool: ng new, ng serve, ng build
TypeScript Typed JavaScript — Angular's primary language
RxJS Reactive Extensions — Angular uses for async
Signals Angular's modern reactivity system (v16+)
NgRx Redux-style state management for Angular
NgModule Old way to organize Angular (pre-standalone)
Standalone Modern Angular without NgModules (v17 default)
Angular Universal Server-Side Rendering for Angular

What to learn next

  1. NgRx — Redux-pattern state management for large apps
  2. Angular Material — Google's UI component library
  3. Testing — Jasmine, Karma, Spectator for unit/integration tests
  4. Angular Universal — Server-side rendering (SSR)
  5. PWA — Progressive Web App with @angular/pwa
  6. Animations@angular/animations for transitions
  7. Nx — Monorepo tooling for multiple Angular apps

Frequently asked questions

Do I need to learn AngularJS before Angular? No. AngularJS (version 1.x) and Angular (2+) are completely different frameworks. Skip AngularJS entirely — it's legacy.

Should I learn React or Angular first? React has higher job demand and a gentler learning curve. Angular is better for enterprise environments and large teams. If unsure, start with React. If you're targeting enterprise jobs or already work at a company using Angular, go with Angular.

Do I need to learn TypeScript first? You'll pick up TypeScript as you learn Angular — they're taught together. Basic JavaScript knowledge is enough to start.

What's the difference between standalone components and NgModules? NgModules were the old way (Angular 2–16) to group components, directives, and pipes. Standalone components (Angular 17+ default) don't need NgModules — simpler and more modern. Use standalone for all new projects.

What are Signals and do I need to learn them? Signals are Angular's new reactivity system (Angular 16+). They're simpler than RxJS for most use cases and will be the primary way to handle state in future Angular. Learn them — they're the future of Angular.

Is Angular good for beginners? Angular has a steeper learning curve than React or Vue because it includes more concepts upfront (TypeScript, DI, decorators, RxJS). But once you understand the structure, everything follows consistent patterns. Plan for 4–6 weeks to feel comfortable.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools