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:
- TypeScript class — the logic
- HTML template — the view
- CSS — the styles
Create a component
ng generate component hello
This creates src/app/hello/:
hello.component.tshello.component.htmlhello.component.csshello.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}¤t=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
- NgRx — Redux-pattern state management for large apps
- Angular Material — Google's UI component library
- Testing — Jasmine, Karma, Spectator for unit/integration tests
- Angular Universal — Server-side rendering (SSR)
- PWA — Progressive Web App with
@angular/pwa - Animations —
@angular/animationsfor transitions - 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.