What You'll Learn
Introduction to Angular
Angular is a powerful, open-source web application framework developed by Google. It's built with TypeScript and follows a component-based architecture, making it perfect for building large-scale enterprise applications.
Why Angular?
Enterprise Ready: Angular provides a complete solution out of the box - routing, forms, HTTP client, testing, and more. Used by Google, Microsoft, Forbes, and BMW!
Components
Reusable UI building blocks
Dependency Injection
Built-in DI system
Two-Way Binding
Sync data & UI automatically
TypeScript
Type-safe development
- Modules: Organize application into cohesive blocks
- Components: UI building blocks with template, logic, and styles
- Services: Shared logic and data across components
- Directives: Extend HTML with custom attributes and elements
Installation & Setup
Install Node.js
Make sure you have Node.js 18+ installed. Check with node --version
Install Angular CLI
Angular CLI is the official command-line tool for Angular development
Create New Project
Use Angular CLI to scaffold a new project
Angular Project Created!
Open http://localhost:4200 in your browser to see your app!
Project Structure
Understanding the project structure is crucial for effective Angular development.
Folder Organization
As your app grows, organize by feature: src/app/features/,
src/app/shared/, src/app/core/
Components
Components are the fundamental building blocks of Angular applications. Each component consists of a TypeScript class, an HTML template, and optional CSS styles.
Creating a Component
Component Structure
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-header', // HTML tag name templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { // Properties title: string = 'My Angular App'; isLoggedIn: boolean = false; user: { name: string; email: string } | null = null; // Constructor constructor() { } // Lifecycle hook - runs after component initializes ngOnInit(): void { console.log('Header component initialized'); } // Methods login(): void { this.isLoggedIn = true; this.user = { name: 'Om Pandey', email: '[email protected]' }; } logout(): void { this.isLoggedIn = false; this.user = null; } }
<header class="app-header"> <h1>{{ title }}</h1> <nav> <a routerLink="/">Home</a> <a routerLink="/about">About</a> <a routerLink="/contact">Contact</a> </nav> <div class="auth-section"> <!-- Conditional rendering --> <div *ngIf="isLoggedIn; else loginButton"> <span>Welcome, {{ user?.name }}</span> <button (click)="logout()">Logout</button> </div> <ng-template #loginButton> <button (click)="login()">Login</button> </ng-template> </div> </header>
Component Communication
Components communicate using @Input() and @Output() decorators.
import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-user-card', template: ` <div class="user-card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> <button (click)="onSelect()">Select User</button> </div> ` }) export class UserCardComponent { // Input - receive data from parent @Input() user!: { name: string; email: string }; // Output - send events to parent @Output() userSelected = new EventEmitter<string>(); onSelect(): void { this.userSelected.emit(this.user.name); } }
<!-- Parent component using child --> <app-user-card [user]="currentUser" (userSelected)="handleUserSelection($event)" ></app-user-card> <!-- Loop through users --> <app-user-card *ngFor="let user of users" [user]="user" (userSelected)="handleUserSelection($event)" ></app-user-card>
Lifecycle Hooks
import { Component, OnInit, OnDestroy, OnChanges, AfterViewInit, SimpleChanges } from '@angular/core'; @Component({...}) export class LifecycleComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { // Called when input properties change ngOnChanges(changes: SimpleChanges): void { console.log('Input changed:', changes); } // Called once after component is initialized ngOnInit(): void { console.log('Component initialized'); // Perfect for API calls, subscriptions } // Called after view is fully initialized ngAfterViewInit(): void { console.log('View initialized'); // Access @ViewChild elements here } // Called when component is destroyed ngOnDestroy(): void { console.log('Component destroyed'); // Cleanup subscriptions, timers, etc. } }
Templates & Data Binding
Angular templates use special syntax to bind data between the component and the view.
Types of Data Binding
<!-- 1. INTERPOLATION - Display data {{ }} --> <h1>{{ title }}</h1> <p>{{ user.name }} - {{ user.email }}</p> <p>{{ 2 + 2 }}</p> <p>{{ getFullName() }}</p> <!-- 2. PROPERTY BINDING - [ ] --> <img [src]="imageUrl" [alt]="imageAlt"> <button [disabled]="isLoading">Submit</button> <div [class.active]="isActive">...</div> <div [style.color]="textColor">...</div> <div [ngClass]="{'active': isActive, 'disabled': isDisabled}">...</div> <div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">...</div> <!-- 3. EVENT BINDING - ( ) --> <button (click)="handleClick()">Click Me</button> <button (click)="handleClick($event)">With Event</button> <input (input)="onInput($event)"> <input (keyup.enter)="onEnter()"> <form (submit)="onSubmit($event)">...</form> <div (mouseenter)="onHover()" (mouseleave)="onLeave()">...</div> <!-- 4. TWO-WAY BINDING - [( )] (Banana in a Box) --> <input [(ngModel)]="username"> <p>Hello, {{ username }}!</p> <!-- Equivalent to: --> <input [ngModel]="username" (ngModelChange)="username = $event">
FormsModule Required!
To use [(ngModel)], import FormsModule in your module.
Template Reference Variables
<!-- Create reference with # --> <input #nameInput type="text" placeholder="Enter name"> <button (click)="greet(nameInput.value)">Greet</button> <!-- Display input length --> <p>Length: {{ nameInput.value.length }}</p> <!-- Access in component with @ViewChild -->
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; @Component({...}) export class MyComponent implements AfterViewInit { @ViewChild('nameInput') inputRef!: ElementRef<HTMLInputElement>; ngAfterViewInit(): void { // Focus input on load this.inputRef.nativeElement.focus(); } greet(name: string): void { alert(`Hello, ${name}!`); } }
Directives
Directives are instructions in the DOM. Angular has three types: Components, Structural directives, and Attribute directives.
Structural Directives
<!-- *ngIf - Conditional rendering --> <div *ngIf="isLoggedIn">Welcome back!</div> <!-- *ngIf with else --> <div *ngIf="isLoggedIn; else loginTemplate"> Welcome, {{ userName }}! </div> <ng-template #loginTemplate> <button>Please Login</button> </ng-template> <!-- *ngIf with then and else --> <div *ngIf="isLoading; then loadingTpl; else contentTpl"></div> <ng-template #loadingTpl>Loading...</ng-template> <ng-template #contentTpl>Content loaded!</ng-template> <!-- *ngFor - Loop through arrays --> <ul> <li *ngFor="let item of items">{{ item }}</li> </ul> <!-- *ngFor with index and other variables --> <div *ngFor="let user of users; let i = index; let first = first; let last = last; let even = even"> <p [class.highlight]="even"> {{ i + 1 }}. {{ user.name }} <span *ngIf="first">(First)</span> <span *ngIf="last">(Last)</span> </p> </div> <!-- *ngFor with trackBy for performance --> <div *ngFor="let item of items; trackBy: trackById"> {{ item.name }} </div> <!-- ngSwitch --> <div [ngSwitch]="status"> <p *ngSwitchCase="'active'">User is active</p> <p *ngSwitchCase="'inactive'">User is inactive</p> <p *ngSwitchCase="'pending'">User is pending</p> <p *ngSwitchDefault>Unknown status</p> </div>
Custom Directive
import { Directive, ElementRef, HostListener, Input } from '@angular/core'; @Directive({ selector: '[appHighlight]' }) export class HighlightDirective { @Input() appHighlight: string = 'yellow'; @Input() defaultColor: string = 'transparent'; constructor(private el: ElementRef) {} @HostListener('mouseenter') onMouseEnter(): void { this.highlight(this.appHighlight || 'yellow'); } @HostListener('mouseleave') onMouseLeave(): void { this.highlight(this.defaultColor); } private highlight(color: string): void { this.el.nativeElement.style.backgroundColor = color; } }
<!-- Default highlight (yellow) --> <p appHighlight>Hover over me!</p> <!-- Custom highlight color --> <p [appHighlight]="'lightblue'">Blue highlight</p> <!-- With default color --> <p appHighlight="pink" defaultColor="lightgray">Pink highlight</p>
Services & Dependency Injection
Services are classes that handle business logic, data fetching, and shared functionality. Angular's Dependency Injection system makes services available throughout your app.
Creating a Service
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, BehaviorSubject } from 'rxjs'; export interface User { id: number; name: string; email: string; } @Injectable({ providedIn: 'root' // Available app-wide }) export class UserService { private apiUrl = 'https://api.example.com/users'; private currentUserSubject = new BehaviorSubject<User | null>(null); // Observable for components to subscribe currentUser$ = this.currentUserSubject.asObservable(); constructor(private http: HttpClient) {} // Get all users getUsers(): Observable<User[]> { return this.http.get<User[]>(this.apiUrl); } // Get user by ID getUserById(id: number): Observable<User> { return this.http.get<User>(`${this.apiUrl}/${id}`); } // Create user createUser(user: Omit<User, 'id'>): Observable<User> { return this.http.post<User>(this.apiUrl, user); } // Update user updateUser(id: number, user: Partial<User>): Observable<User> { return this.http.put<User>(`${this.apiUrl}/${id}`, user); } // Delete user deleteUser(id: number): Observable<void> { return this.http.delete<void>(`${this.apiUrl}/${id}`); } // Set current user setCurrentUser(user: User): void { this.currentUserSubject.next(user); } }
Using Service in Component
import { Component, OnInit, OnDestroy } from '@angular/core'; import { UserService, User } from '../services/user.service'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html' }) export class UserListComponent implements OnInit, OnDestroy { users: User[] = []; loading = false; error: string | null = null; private subscription!: Subscription; // Inject service via constructor constructor(private userService: UserService) {} ngOnInit(): void { this.loadUsers(); } loadUsers(): void { this.loading = true; this.subscription = this.userService.getUsers().subscribe({ next: (users) => { this.users = users; this.loading = false; }, error: (err) => { this.error = 'Failed to load users'; this.loading = false; console.error(err); } }); } selectUser(user: User): void { this.userService.setCurrentUser(user); } deleteUser(id: number): void { this.userService.deleteUser(id).subscribe({ next: () => { this.users = this.users.filter(u => u.id !== id); }, error: (err) => console.error(err) }); } ngOnDestroy(): void { // Clean up subscription if (this.subscription) { this.subscription.unsubscribe(); } } }
Routing & Navigation
Angular Router enables navigation between views and supports features like lazy loading, route guards, and nested routes.
Route Configuration
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { AboutComponent } from './about/about.component'; import { UserListComponent } from './user-list/user-list.component'; import { UserDetailComponent } from './user-detail/user-detail.component'; import { NotFoundComponent } from './not-found/not-found.component'; import { AuthGuard } from './guards/auth.guard'; const routes: Routes = [ // Basic routes { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, // Route with parameter { path: 'users', component: UserListComponent }, { path: 'users/:id', component: UserDetailComponent }, // Protected route with guard { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, // Lazy loaded module { path: 'admin', loadChildren: () => import('./admin/admin.module') .then(m => m.AdminModule), canActivate: [AuthGuard] }, // Redirect { path: 'home', redirectTo: '', pathMatch: 'full' }, // Wildcard - 404 page { path: '**', component: NotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Navigation
<!-- Router outlet - where routed components render --> <router-outlet></router-outlet> <!-- Navigation links --> <nav> <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}"> Home </a> <a routerLink="/about" routerLinkActive="active">About</a> <a routerLink="/users" routerLinkActive="active">Users</a> <!-- Dynamic route --> <a [routerLink]="['/users', userId]">User Profile</a> <!-- With query params --> <a [routerLink]="['/users']" [queryParams]="{page: 1, sort: 'name'}"> Users Page 1 </a> </nav>
import { Router, ActivatedRoute } from '@angular/router'; export class UserDetailComponent implements OnInit { userId!: string; constructor( private router: Router, private route: ActivatedRoute ) {} ngOnInit(): void { // Get route parameter this.userId = this.route.snapshot.paramMap.get('id') || ''; // Subscribe to param changes this.route.paramMap.subscribe(params => { this.userId = params.get('id') || ''; }); // Get query params this.route.queryParamMap.subscribe(params => { const page = params.get('page'); }); } goToUsers(): void { this.router.navigate(['/users']); } goToUserWithParams(): void { this.router.navigate(['/users', 123], { queryParams: { tab: 'profile' } }); } }
Route Guards
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; import { AuthService } from '../services/auth.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private authService: AuthService, private router: Router ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean | UrlTree> | boolean | UrlTree { if (this.authService.isLoggedIn()) { return true; } // Redirect to login with return URL return this.router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); } }
Forms
Angular provides two approaches to forms: Template-driven and Reactive forms.
Template-Driven Forms
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)"> <div class="form-group"> <label for="name">Name</label> <input type="text" id="name" name="name" [(ngModel)]="user.name" required minlength="3" #name="ngModel" > <div *ngIf="name.invalid && name.touched" class="error"> <span *ngIf="name.errors?.['required']">Name is required</span> <span *ngIf="name.errors?.['minlength']">Min 3 characters</span> </div> </div> <div class="form-group"> <label for="email">Email</label> <input type="email" id="email" name="email" [(ngModel)]="user.email" required email #email="ngModel" > <div *ngIf="email.invalid && email.touched" class="error"> Please enter a valid email </div> </div> <button type="submit" [disabled]="userForm.invalid">Submit</button> </form>
Reactive Forms
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-reactive-form', templateUrl: './reactive-form.component.html' }) export class ReactiveFormComponent implements OnInit { userForm!: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit(): void { this.userForm = this.fb.group({ name: ['', [Validators.required, Validators.minLength(3)]], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], address: this.fb.group({ street: [''], city: ['', Validators.required], zipCode: ['', Validators.pattern(/^\d{5}$/)] }) }); } // Convenience getters get name() { return this.userForm.get('name'); } get email() { return this.userForm.get('email'); } get password() { return this.userForm.get('password'); } onSubmit(): void { if (this.userForm.valid) { console.log(this.userForm.value); } } resetForm(): void { this.userForm.reset(); } }
<form [formGroup]="userForm" (ngSubmit)="onSubmit()"> <div class="form-group"> <label>Name</label> <input formControlName="name" type="text"> <div *ngIf="name?.invalid && name?.touched" class="error"> <span *ngIf="name?.errors?.['required']">Name is required</span> <span *ngIf="name?.errors?.['minlength']">Min 3 characters</span> </div> </div> <div class="form-group"> <label>Email</label> <input formControlName="email" type="email"> </div> <!-- Nested form group --> <div formGroupName="address"> <input formControlName="street" placeholder="Street"> <input formControlName="city" placeholder="City"> <input formControlName="zipCode" placeholder="Zip Code"> </div> <button type="submit" [disabled]="userForm.invalid">Submit</button> </form>
Import ReactiveFormsModule
Don't forget to import ReactiveFormsModule in your module!
HTTP & APIs
Angular's HttpClient module provides a simplified API for HTTP functionality.
HTTP Client Setup
import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ BrowserModule, HttpClientModule, // Add this ... ], ... }) export class AppModule { }
API Service with HTTP Methods
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, retry, map } from 'rxjs/operators'; interface Post { id: number; title: string; body: string; userId: number; } @Injectable({ providedIn: 'root' }) export class ApiService { private baseUrl = 'https://jsonplaceholder.typicode.com'; private httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }) }; constructor(private http: HttpClient) {} // GET - Fetch all posts getPosts(): Observable<Post[]> { return this.http.get<Post[]>(`${this.baseUrl}/posts`).pipe( retry(2), catchError(this.handleError) ); } // GET with query params getPostsByUser(userId: number): Observable<Post[]> { const params = new HttpParams().set('userId', userId.toString()); return this.http.get<Post[]>(`${this.baseUrl}/posts`, { params }); } // GET single item getPost(id: number): Observable<Post> { return this.http.get<Post>(`${this.baseUrl}/posts/${id}`); } // POST - Create createPost(post: Omit<Post, 'id'>): Observable<Post> { return this.http.post<Post>( `${this.baseUrl}/posts`, post, this.httpOptions ); } // PUT - Update updatePost(id: number, post: Partial<Post>): Observable<Post> { return this.http.put<Post>( `${this.baseUrl}/posts/${id}`, post, this.httpOptions ); } // DELETE deletePost(id: number): Observable<void> { return this.http.delete<void>(`${this.baseUrl}/posts/${id}`); } // Error handling private handleError(error: any): Observable<never> { console.error('An error occurred:', error); return throwError(() => new Error('Something went wrong')); } }
HTTP Interceptors
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable() export class AuthInterceptor implements HttpInterceptor { intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // Get token from storage const token = localStorage.getItem('token'); // Clone request and add auth header if (token) { const authReq = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next.handle(authReq).pipe( catchError(this.handleError) ); } return next.handle(req); } private handleError(error: HttpErrorResponse): Observable<never> { if (error.status === 401) { // Redirect to login console.log('Unauthorized - redirecting to login'); } return throwError(() => error); } }
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './interceptors/auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule { }
Pipes
Pipes transform data in templates. Angular provides built-in pipes and allows custom pipe creation.
Built-in Pipes
<!-- String pipes --> <p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD --> <p>{{ 'HELLO WORLD' | lowercase }}</p> <!-- hello world --> <p>{{ 'hello world' | titlecase }}</p> <!-- Hello World --> <!-- Date pipe --> <p>{{ today | date }}</p> <!-- Jan 25, 2025 --> <p>{{ today | date:'short' }}</p> <!-- 1/25/25, 10:30 AM --> <p>{{ today | date:'fullDate' }}</p> <!-- Saturday, January 25, 2025 --> <p>{{ today | date:'yyyy-MM-dd' }}</p> <!-- 2025-01-25 --> <p>{{ today | date:'HH:mm:ss' }}</p> <!-- 10:30:45 --> <!-- Number pipes --> <p>{{ 3.14159 | number:'1.2-2' }}</p> <!-- 3.14 --> <p>{{ 1234567 | number }}</p> <!-- 1,234,567 --> <!-- Currency pipe --> <p>{{ 99.99 | currency }}</p> <!-- $99.99 --> <p>{{ 99.99 | currency:'EUR' }}</p> <!-- €99.99 --> <p>{{ 99.99 | currency:'INR':'symbol' }}</p> <!-- ₹99.99 --> <!-- Percent pipe --> <p>{{ 0.25 | percent }}</p> <!-- 25% --> <p>{{ 0.2567 | percent:'1.2-2' }}</p> <!-- 25.67% --> <!-- JSON pipe (useful for debugging) --> <pre>{{ user | json }}</pre> <!-- Slice pipe --> <p>{{ 'Hello World' | slice:0:5 }}</p> <!-- Hello --> <li *ngFor="let item of items | slice:0:5">{{ item }}</li> <!-- Async pipe (auto subscribes/unsubscribes) --> <p>{{ user$ | async }}</p> <div *ngIf="users$ | async as users"> <p *ngFor="let user of users">{{ user.name }}</p> </div> <!-- KeyValue pipe (for objects) --> <div *ngFor="let item of myObject | keyvalue"> {{ item.key }}: {{ item.value }} </div>
Custom Pipe
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'truncate' }) export class TruncatePipe implements PipeTransform { transform( value: string, limit: number = 50, ellipsis: string = '...' ): string { if (!value) return ''; if (value.length <= limit) return value; return value.substring(0, limit) + ellipsis; } }
<!-- Default: 50 chars with ... --> <p>{{ longText | truncate }}</p> <!-- Custom limit --> <p>{{ longText | truncate:100 }}</p> <!-- Custom limit and ellipsis --> <p>{{ longText | truncate:30:' [Read More]' }}</p>
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'timeAgo' }) export class TimeAgoPipe implements PipeTransform { transform(value: Date | string): string { const date = new Date(value); const now = new Date(); const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (seconds < 60) return 'Just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`; if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`; return date.toLocaleDateString(); } }
Advanced Topics
RxJS Observables
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Observable, Subject, BehaviorSubject, Subscription, interval, fromEvent, of } from 'rxjs'; import { map, filter, debounceTime, distinctUntilChanged, switchMap, catchError, takeUntil, tap } from 'rxjs/operators'; @Component({...}) export class RxjsExamplesComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); searchResults$!: Observable<any[]>; ngOnInit(): void { // Example 1: Transform data with map of(1, 2, 3, 4, 5).pipe( map(x => x * 2), filter(x => x > 4) ).subscribe(val => console.log(val)); // 6, 8, 10 // Example 2: Search with debounce this.searchResults$ = this.searchInput$.pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.searchService.search(term)), catchError(err => of([])) ); // Example 3: Auto-unsubscribe with takeUntil interval(1000).pipe( takeUntil(this.destroy$) ).subscribe(val => console.log(val)); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }
State Management with NgRx
import { createAction, props } from '@ngrx/store'; import { User } from '../models/user.model'; export const loadUsers = createAction('[User] Load Users'); export const loadUsersSuccess = createAction( '[User] Load Users Success', props<{ users: User[] }>() ); export const loadUsersFailure = createAction( '[User] Load Users Failure', props<{ error: string }>() );
import { createReducer, on } from '@ngrx/store'; import * as UserActions from './user.actions'; export interface UserState { users: User[]; loading: boolean; error: string | null; } const initialState: UserState = { users: [], loading: false, error: null }; export const userReducer = createReducer( initialState, on(UserActions.loadUsers, (state) => ({ ...state, loading: true })), on(UserActions.loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false })), on(UserActions.loadUsersFailure, (state, { error }) => ({ ...state, error, loading: false })) );
Lazy Loading Modules
const routes: Routes = [ { path: '', component: HomeComponent }, // Lazy loaded feature modules { path: 'admin', loadChildren: () => import('./admin/admin.module') .then(m => m.AdminModule) }, { path: 'products', loadChildren: () => import('./products/products.module') .then(m => m.ProductsModule) }, // Lazy load with preloading strategy { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module') .then(m => m.DashboardModule), data: { preload: true } } ];
Production Build & Deployment
export const environment = { production: true, apiUrl: 'https://api.production.com', analyticsKey: 'PROD_ANALYTICS_KEY' };
Deployment Options
Deploy your Angular app to: Vercel, Netlify, Firebase Hosting, AWS S3 + CloudFront, Azure Static Web Apps, or any static hosting service.
Congratulations!
You've mastered Angular from zero to hero! You're now ready to build powerful, scalable frontend applications. Keep practicing and building projects!