Back to blog

Angular Core Concepts: Hướng Dẫn Toàn Diện

angulartypescriptfrontendweb-developmentframework
Angular Core Concepts: Hướng Dẫn Toàn Diện

Angular là một trong những frontend framework mạnh mẽ và "có ý kiến" nhất hiện nay. Khác với React — cho bạn tự do chọn tool riêng, Angular đi kèm đủ đồ nghề — routing, forms, HTTP client, dependency injection, và nhiều hơn nữa — tất cả đã built-in và hoạt động ăn khớp với nhau. Nếu bạn đã từng làm việc với backend framework như Spring Boot hay .NET, architecture của Angular sẽ khiến bạn thấy rất quen thuộc.

Trong bài viết này, chúng ta sẽ deep-dive vào các core concepts của Angular mà mọi developer đều phải nắm vững để build được những ứng dụng scalable và dễ maintain.

Bạn Sẽ Học Được Gì

✅ Hiểu kiến trúc component-based của Angular
✅ Master dependency injection — feature mạnh nhất của Angular
✅ Nắm vững component lifecycle hooks
✅ Xây dựng custom directives và pipes
✅ Làm việc với templates, data binding, và change detection
✅ Bắt đầu với RxJS và reactive patterns trong Angular
✅ Điều hướng giữa các trang với Angular Router
✅ Xây dựng forms type-safe với Reactive Forms

Prerequisites: TypeScript fundamentals, kiến thức HTML/CSS cơ bản
Thời gian: 3–5 ngày, 1–2 giờ mỗi ngày

Tổng Quan Angular Architecture

Trước khi đi vào từng concept, hãy hiểu cách ứng dụng Angular được tổ chức:

Angular tổ chức code thành modules chứa components, services, directives, và pipes. Mỗi component có template, styles, và logic. Services được chia sẻ thông qua dependency injection.

Lưu ý: Từ Angular 17, standalone components là mặc định. Bạn không cần NgModule cho hầu hết các trường hợp nữa. Chúng ta sẽ dùng standalone components xuyên suốt guide này.

1. Components — Viên Gạch Xây Dựng

Components là building blocks cơ bản của ứng dụng Angular. Mọi phần UI đều là component — từ cả trang cho đến một button nhỏ.

Giải Phẫu Một Component

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `
    <div class="greeting">
      <h2>Hello, {{ name }}!</h2>
      <p>Welcome to Angular</p>
    </div>
  `,
  styles: [`
    .greeting {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 8px;
    }
  `]
})
export class GreetingComponent {
  name = 'Developer';
}

Một component gồm bốn phần:

PhầnMục đích
@Component decoratorMetadata — selector, template, styles
selectorTag HTML dùng để render component (<app-greeting>)
template / templateUrlCấu trúc HTML (inline hoặc file riêng)
Class bodyLogic, properties, và methods của component

Component Communication

Components giao tiếp với nhau qua inputs và outputs:

import { Component, input, output } from '@angular/core';
 
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card" (click)="cardClicked.emit(userId())">
      <h3>{{ userName() }}</h3>
      <span>{{ role() }}</span>
    </div>
  `
})
export class UserCardComponent {
  // Signal-based inputs (Angular 17+)
  userName = input.required<string>();
  role = input<string>('member');
  userId = input.required<number>();
 
  // Signal-based output
  cardClicked = output<number>();
}

Sử dụng trong parent component:

<app-user-card
  [userName]="'Alice'"
  [role]="'admin'"
  [userId]="42"
  (cardClicked)="onCardClick($event)"
/>

Content Projection

Angular hỗ trợ content projection (tương tự children trong React):

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[card-title]" />
      </div>
      <div class="card-body">
        <ng-content />
      </div>
      <div class="card-footer">
        <ng-content select="[card-actions]" />
      </div>
    </div>
  `
})
export class CardComponent {}
<app-card>
  <h2 card-title>My Title</h2>
  <p>Phần này nằm trong body (default slot)</p>
  <button card-actions>Save</button>
</app-card>

2. Templates và Data Binding

Template syntax của Angular cung cấp nhiều cách mạnh mẽ để kết nối logic component với DOM.

Bốn Loại Data Binding

@Component({
  selector: 'app-binding-demo',
  standalone: true,
  template: `
    <!-- 1. Interpolation: Component → View -->
    <h1>{{ title }}</h1>
    <p>{{ getFullName() }}</p>
 
    <!-- 2. Property Binding: Component → View -->
    <img [src]="imageUrl" [alt]="imageAlt">
    <button [disabled]="isLoading">Submit</button>
 
    <!-- 3. Event Binding: View → Component -->
    <button (click)="onSubmit()">Submit</button>
    <input (keyup.enter)="onSearch($event)">
 
    <!-- 4. Two-Way Binding: Component ↔ View -->
    <input [(ngModel)]="searchQuery">
    <p>Đang tìm: {{ searchQuery }}</p>
  `
})
export class BindingDemoComponent {
  title = 'Data Binding Demo';
  imageUrl = '/assets/logo.png';
  imageAlt = 'Logo';
  isLoading = false;
  searchQuery = '';
 
  getFullName() {
    return 'John Doe';
  }
 
  onSubmit() {
    this.isLoading = true;
  }
 
  onSearch(event: KeyboardEvent) {
    console.log('Searching:', this.searchQuery);
  }
}

Tóm tắt:

Cú phápHướngVí dụ
{{ value }}Component → View{{ title }}
[property]="value"Component → View[disabled]="isLoading"
(event)="handler()"View → Component(click)="onSave()"
[(ngModel)]="value"Hai chiều[(ngModel)]="name"

Control Flow (Cú Pháp Mới Từ Angular 17+)

Angular 17 giới thiệu cú pháp control flow mới, gọn gàng hơn hẳn:

<!-- Render có điều kiện -->
@if (user) {
  <h2>Chào mừng, {{ user.name }}!</h2>
} @else if (isLoading) {
  <p>Đang tải...</p>
} @else {
  <p>Vui lòng đăng nhập</p>
}
 
<!-- Vòng lặp -->
@for (item of items; track item.id) {
  <app-item-card [item]="item" />
} @empty {
  <p>Không tìm thấy item nào</p>
}
 
<!-- Switch -->
@switch (status) {
  @case ('active') {
    <span class="badge green">Active</span>
  }
  @case ('inactive') {
    <span class="badge red">Inactive</span>
  }
  @default {
    <span class="badge gray">Unknown</span>
  }
}

Tại sao cần track? Angular dùng track để nhận diện từng item trong list. Nhờ đó Angular có thể update DOM hiệu quả khi items thay đổi, thêm, hoặc xóa — tương tự prop key trong React.

3. Dependency Injection — Siêu Năng Lực Của Angular

Dependency Injection (DI) có thể nói là feature mạnh nhất của Angular. Nếu bạn từng làm Spring Boot hoặc .NET, bạn sẽ thấy quen ngay. Nếu chưa, hãy nghĩ DI như một hệ thống tự động cung cấp cho components những services mà chúng cần.

Tại Sao DI Quan Trọng

Không có DI:

// ❌ Tight coupling — khó test, khó swap implementation
export class UserComponent {
  private userService = new UserService(new HttpClient(), new AuthService());
  private logger = new LoggerService();
}

Có DI:

// ✅ Loose coupling — dễ test, dễ swap
export class UserComponent {
  constructor(
    private userService: UserService,
    private logger: LoggerService
  ) {}
}

Tạo Service

import { Injectable } from '@angular/core';
 
@Injectable({
  providedIn: 'root' // Singleton — một instance cho toàn bộ app
})
export class UserService {
  private apiUrl = '/api/users';
 
  constructor(private http: HttpClient) {}
 
  getUsers() {
    return this.http.get<User[]>(this.apiUrl);
  }
 
  getUserById(id: number) {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
 
  createUser(user: CreateUserDto) {
    return this.http.post<User>(this.apiUrl, user);
  }
}

Hàm inject() (Cách Hiện Đại)

Angular 14+ giới thiệu hàm inject() thay thế cho constructor injection:

import { Component, inject } from '@angular/core';
 
@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @for (user of users; track user.id) {
      <div>{{ user.name }}</div>
    }
  `
})
export class UserListComponent {
  private userService = inject(UserService);
  private router = inject(Router);
 
  users: User[] = [];
 
  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

inject() vs constructor injection: Cả hai đều hoạt động, nhưng inject() được ưu tiên trong Angular hiện đại. Nó ngắn gọn hơn, hoạt động trong functional guards/resolvers, và không cần boilerplate constructor parameters.

Provider Scopes

DI scope kiểm soát số lượng instance của một service:

// Root scope — Singleton (phổ biến nhất)
@Injectable({ providedIn: 'root' })
export class AuthService {}
 
// Component scope — Instance mới cho mỗi component
@Component({
  selector: 'app-editor',
  providers: [EditorStateService], // Mỗi <app-editor> nhận instance riêng
  template: `...`
})
export class EditorComponent {}

Injection Tokens và Interfaces

Vì TypeScript interfaces không tồn tại lúc runtime, dùng InjectionToken cho non-class dependencies:

import { InjectionToken } from '@angular/core';
 
// Define token
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
 
// Provide value
// Trong app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: API_BASE_URL, useValue: 'https://api.example.com' },
    { provide: APP_CONFIG, useFactory: () => loadConfig() },
  ]
};
 
// Inject value
@Injectable({ providedIn: 'root' })
export class ApiService {
  private baseUrl = inject(API_BASE_URL);
}

Các Loại Provider

Angular hỗ trợ nhiều cách provide dependencies:

providers: [
  // useClass — Provide một class (mặc định)
  { provide: LoggerService, useClass: ConsoleLoggerService },
 
  // useValue — Provide một giá trị tĩnh
  { provide: API_URL, useValue: 'https://api.example.com' },
 
  // useFactory — Provide qua factory function
  {
    provide: DataService,
    useFactory: (http: HttpClient, config: AppConfig) => {
      return config.useMock
        ? new MockDataService()
        : new RealDataService(http);
    },
    deps: [HttpClient, APP_CONFIG]
  },
 
  // useExisting — Alias sang provider khác
  { provide: AbstractLogger, useExisting: ConsoleLoggerService },
]

4. Lifecycle Hooks

Mỗi component trong Angular đều trải qua một lifecycle — từ lúc tạo ra đến khi bị hủy. Angular cung cấp hooks cho phép bạn can thiệp vào các thời điểm quan trọng trong lifecycle này.

Toàn Bộ Lifecycle

Phân Tích Từng Hook

import {
  Component,
  OnInit,
  OnDestroy,
  OnChanges,
  AfterViewInit,
  SimpleChanges,
  input
} from '@angular/core';
 
@Component({
  selector: 'app-lifecycle-demo',
  standalone: true,
  template: `<p>{{ message() }}</p>`
})
export class LifecycleDemoComponent
  implements OnInit, OnDestroy, OnChanges, AfterViewInit {
 
  message = input<string>('');
  private subscription?: Subscription;
 
  // Gọi TRƯỚC ngOnInit, mỗi khi input properties thay đổi
  ngOnChanges(changes: SimpleChanges) {
    if (changes['message']) {
      console.log(
        'Previous:', changes['message'].previousValue,
        'Current:', changes['message'].currentValue
      );
    }
  }
 
  // Gọi MỘT LẦN sau ngOnChanges đầu tiên
  // Nơi tốt nhất cho: API calls, khởi tạo ban đầu
  ngOnInit() {
    console.log('Component đã khởi tạo');
    this.subscription = this.dataService.getData().subscribe(/* ... */);
  }
 
  // Gọi sau khi view (và child views) được khởi tạo
  // Nơi tốt nhất cho: Thao tác DOM, truy cập ViewChild
  ngAfterViewInit() {
    console.log('View đã sẵn sàng');
  }
 
  // Gọi ngay trước khi Angular hủy component
  // Nơi tốt nhất cho: Cleanup — unsubscribe, clear timers, detach listeners
  ngOnDestroy() {
    console.log('Đang dọn dẹp');
    this.subscription?.unsubscribe();
  }
}

Khi Nào Dùng Hook Nào

HookChạy khiDùng cho
ngOnChangesMỗi khi input thay đổiPhản ứng khi input properties thay đổi
ngOnInitMột lần, sau ngOnChanges đầu tiênAPI calls, fetch data ban đầu, setup logic
ngDoCheckMỗi change detection cycleCustom change detection logic (dùng ít thôi!)
ngAfterContentInitMột lần, sau content projectionTruy cập projected content (<ng-content>)
ngAfterViewInitMột lần, sau khi view renderTruy cập DOM, @ViewChild
ngAfterViewCheckedSau mỗi lần check viewPhản ứng khi view thay đổi (dùng ít thôi!)
ngOnDestroyMột lần, trước khi component bị xóaCleanup: unsubscribe, clear timers, detach listeners

Quy Tắc Vàng: Luôn Dọn Dẹp

export class DashboardComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
 
  ngOnInit() {
    // Dùng takeUntil để auto-unsubscribe
    this.dataService.getUpdates()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => this.updateDashboard(data));
 
    this.notificationService.getNotifications()
      .pipe(takeUntil(this.destroy$))
      .subscribe(notification => this.showNotification(notification));
  }
 
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Cách hiện đại hơn: Angular 16+ giới thiệu DestroyRef và helper takeUntilDestroyed để đơn giản hóa cleanup:

export class DashboardComponent {
  private destroyRef = inject(DestroyRef);
 
  ngOnInit() {
    this.dataService.getUpdates()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(data => this.updateDashboard(data));
  }
}

5. Directives

Directives cho phép bạn mở rộng HTML với behavior tùy chỉnh. Hãy nghĩ chúng như "chỉ thị" mà Angular áp dụng lên DOM elements.

Các Loại Directives

Built-in Attribute Directives

<!-- ngClass — Thêm/xóa CSS classes động -->
<div [ngClass]="{ 'active': isActive, 'error': hasError }">
  Conditional classes
</div>
 
<!-- ngStyle — Set inline styles động -->
<div [ngStyle]="{ 'font-size': fontSize + 'px', 'color': textColor }">
  Dynamic styles
</div>

Custom Attribute Directive

Hãy xây dựng một directive highlight thay đổi background khi hover:

import { Directive, ElementRef, HostListener, input } from '@angular/core';
 
@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  appHighlight = input<string>('#ffffcc');
 
  constructor(private el: ElementRef) {}
 
  @HostListener('mouseenter')
  onMouseEnter() {
    this.setBackground(this.appHighlight() || '#ffffcc');
  }
 
  @HostListener('mouseleave')
  onMouseLeave() {
    this.setBackground('');
  }
 
  private setBackground(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

Sử dụng:

<p appHighlight>Hover vào đây — highlight vàng mặc định</p>
<p [appHighlight]="'lightblue'">Hover vào đây — highlight xanh</p>

Custom Structural Directive

Một directive chỉ render content cho user roles cụ thể:

import { Directive, TemplateRef, ViewContainerRef, input, effect } from '@angular/core';
 
@Directive({
  selector: '[appIfRole]',
  standalone: true
})
export class IfRoleDirective {
  appIfRole = input.required<string>();
  private authService = inject(AuthService);
  private hasView = false;
 
  constructor(
    private templateRef: TemplateRef<unknown>,
    private viewContainer: ViewContainerRef
  ) {
    effect(() => {
      const requiredRole = this.appIfRole();
      const hasRole = this.authService.hasRole(requiredRole);
 
      if (hasRole && !this.hasView) {
        this.viewContainer.createEmbeddedView(this.templateRef);
        this.hasView = true;
      } else if (!hasRole && this.hasView) {
        this.viewContainer.clear();
        this.hasView = false;
      }
    });
  }
}

Sử dụng:

<button appIfRole="admin">Delete User</button>
<div appIfRole="editor">
  <app-content-editor />
</div>

6. Pipes — Biến Đổi Dữ Liệu Hiển Thị

Pipes biến đổi data trong templates. Chúng nhận một giá trị, xử lý, và trả về output đã format — tất cả mà không thay đổi data gốc.

Built-in Pipes

<!-- Format ngày -->
<p>{{ today | date:'fullDate' }}</p>
<!-- Output: Monday, March 2, 2026 -->
 
<p>{{ today | date:'shortTime' }}</p>
<!-- Output: 10:30 AM -->
 
<!-- Tiền tệ -->
<p>{{ price | currency:'USD' }}</p>
<!-- Output: $1,299.99 -->
 
<!-- Biến đổi text -->
<p>{{ 'hello world' | uppercase }}</p>
<!-- Output: HELLO WORLD -->
 
<p>{{ 'Hello World' | lowercase }}</p>
<!-- Output: hello world -->
 
<p>{{ 'hello world' | titlecase }}</p>
<!-- Output: Hello World -->
 
<!-- Hiển thị JSON (rất tốt để debug) -->
<pre>{{ user | json }}</pre>
 
<!-- Async pipe — auto-subscribe Observables -->
<div>{{ userData$ | async }}</div>
 
<!-- Slice (array hoặc string) -->
<p>{{ items | slice:0:5 }}</p>

async Pipe — Người Bạn Thân Nhất

async pipe tự động subscribe và unsubscribe khỏi Observables:

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      @for (user of users; track user.id) {
        <div>{{ user.name }}</div>
      }
    } @else {
      <p>Đang tải users...</p>
    }
  `
})
export class UserListComponent {
  users$ = inject(UserService).getUsers();
}

Tại sao dùng async pipe? Nó xử lý subscription/unsubscription tự động — không memory leaks, không cần cleanup trong ngOnDestroy.

Custom Pipe

Một pipe hiển thị thời gian tương đối ("2 giờ trước", "hôm qua"):

import { Pipe, PipeTransform } from '@angular/core';
 
@Pipe({
  name: 'timeAgo',
  standalone: true
})
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);
 
    const intervals: [number, string][] = [
      [31536000, 'year'],
      [2592000, 'month'],
      [86400, 'day'],
      [3600, 'hour'],
      [60, 'minute'],
    ];
 
    for (const [secondsInUnit, unit] of intervals) {
      const count = Math.floor(seconds / secondsInUnit);
      if (count >= 1) {
        return count === 1
          ? `1 ${unit} ago`
          : `${count} ${unit}s ago`;
      }
    }
 
    return 'just now';
  }
}

Sử dụng:

<p>Đã đăng {{ post.createdAt | timeAgo }}</p>
<!-- Output: "3 hours ago", "2 days ago", v.v. -->

Pure vs Impure Pipes

// Pure pipe (mặc định) — chỉ chạy khi input reference thay đổi
@Pipe({ name: 'filter', pure: true })
export class FilterPipe implements PipeTransform {
  transform(items: Item[], query: string): Item[] {
    return items.filter(item => item.name.includes(query));
  }
}
 
// Impure pipe — chạy MỖI change detection cycle
// ⚠️ Dùng ít thôi — ảnh hưởng performance!
@Pipe({ name: 'filterImpure', pure: false })
export class FilterImpurePipe implements PipeTransform {
  transform(items: Item[], query: string): Item[] {
    return items.filter(item => item.name.includes(query));
  }
}

Nguyên tắc: Luôn dùng pure pipes. Nếu bạn nghĩ cần impure pipe, có lẽ bạn cần suy nghĩ lại data flow.

7. Change Detection

Change detection là cách Angular giữ view đồng bộ với data của component. Hiểu rõ nó rất quan trọng để build ứng dụng có performance tốt.

Change Detection Hoạt Động Thế Nào

Mặc định, Angular check mọi component trong tree từ trên xuống dưới mỗi khi có bất kỳ điều gì xảy ra. Đây gọi là change detection strategy Default.

OnPush Strategy — Tối Ưu Performance

@Component({
  selector: 'app-user-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="card">
      <h3>{{ user().name }}</h3>
      <p>{{ user().email }}</p>
    </div>
  `
})
export class UserCardComponent {
  user = input.required<User>();
}

Với OnPush, Angular chỉ check component khi:

  1. Reference của @Input thay đổi (không phải chỉ mutation property)
  2. Event handler trong component được kích hoạt
  3. Observable liên kết qua async pipe emit giá trị mới
  4. ChangeDetectorRef.markForCheck() được gọi thủ công

Best practice: Dùng OnPush cho tất cả components. Kết hợp với signals (Angular 17+), bạn sẽ có performance xuất sắc mặc định.

Signals — Tương Lai Của Change Detection

Angular 17+ giới thiệu Signals như một reactive primitive:

import { Component, signal, computed, effect } from '@angular/core';
 
@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">+1</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  // Writable signal
  count = signal(0);
 
  // Computed signal — derived từ signals khác
  doubleCount = computed(() => this.count() * 2);
 
  constructor() {
    // Effect — chạy side effects khi signals thay đổi
    effect(() => {
      console.log('Count đã thay đổi:', this.count());
    });
  }
 
  increment() {
    this.count.update(c => c + 1);
  }
 
  reset() {
    this.count.set(0);
  }
}

Signals cung cấp fine-grained reactivity — Angular biết chính xác phần nào của template cần update, không cần check toàn bộ component tree.

8. RxJS và Reactive Programming

Angular sử dụng RxJS (Reactive Extensions for JavaScript) rộng rãi để xử lý các tác vụ bất đồng bộ. Hiểu cơ bản là điều bắt buộc.

Core RxJS Concepts

Các Pattern Phổ Biến Trong Angular

HTTP requests:

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private apiUrl = '/api/products';
 
  // Trả về Observable
  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl);
  }
 
  // Dùng operators để biến đổi data
  getActiveProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl).pipe(
      map(products => products.filter(p => p.isActive)),
      catchError(error => {
        console.error('Không thể tải products:', error);
        return of([]); // Trả về mảng rỗng khi lỗi
      })
    );
  }
}

Search với debounce:

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule, AsyncPipe],
  template: `
    <input [formControl]="searchControl" placeholder="Tìm kiếm...">
    @for (result of results$ | async; track result.id) {
      <div>{{ result.name }}</div>
    }
  `
})
export class SearchComponent {
  private searchService = inject(SearchService);
  searchControl = new FormControl('');
 
  results$ = this.searchControl.valueChanges.pipe(
    debounceTime(300),        // Đợi 300ms sau lần gõ cuối
    distinctUntilChanged(),   // Bỏ qua nếu giá trị giống nhau
    filter(query => !!query && query.length >= 2), // Tối thiểu 2 ký tự
    switchMap(query =>        // Hủy request trước đó
      this.searchService.search(query!)
    )
  );
}

Các RxJS Operators Thiết Yếu

OperatorMục đíchVí dụ sử dụng
mapBiến đổi giá trị emitConvert API response
filterLọc giá trị thỏa điều kiệnBỏ qua giá trị rỗng
tapSide effects (logging, debugging)tap(x => console.log(x))
switchMapHủy cái trước, subscribe cái mớiSearch, route params
mergeMapSubscribe tất cả (song song)Bulk operations
concatMapSubscribe tuần tự (có thứ tự)Upload files tuần tự
catchErrorXử lý lỗiAPI error handling
debounceTimeĐợi khoảng dừng giữa emissionsSearch input
distinctUntilChangedBỏ qua giá trị liên tiếp trùngSearch input
takeUntilComplete khi notifier emitComponent cleanup
combineLatestKết hợp latest từ nhiều nguồnMultiple data sources

Subjects — Observable + Observer

@Injectable({ providedIn: 'root' })
export class NotificationService {
  // BehaviorSubject — giữ giá trị hiện tại, emit cho subscribers mới
  private notificationsSubject = new BehaviorSubject<Notification[]>([]);
  notifications$ = this.notificationsSubject.asObservable();
 
  addNotification(notification: Notification) {
    const current = this.notificationsSubject.value;
    this.notificationsSubject.next([...current, notification]);
  }
 
  clearAll() {
    this.notificationsSubject.next([]);
  }
}

9. Ghép Tất Cả Lại — Ví Dụ Thực Tế

Hãy kết hợp mọi thứ vào một feature todo list thực tế:

// todo.model.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}
 
// todo.service.ts
@Injectable({ providedIn: 'root' })
export class TodoService {
  private http = inject(HttpClient);
  private todosUrl = '/api/todos';
 
  getTodos(): Observable<Todo[]> {
    return this.http.get<Todo[]>(this.todosUrl);
  }
 
  addTodo(title: string): Observable<Todo> {
    return this.http.post<Todo>(this.todosUrl, { title, completed: false });
  }
 
  toggleTodo(todo: Todo): Observable<Todo> {
    return this.http.patch<Todo>(
      `${this.todosUrl}/${todo.id}`,
      { completed: !todo.completed }
    );
  }
 
  deleteTodo(id: number): Observable<void> {
    return this.http.delete<void>(`${this.todosUrl}/${id}`);
  }
}
 
// todo-list.component.ts
@Component({
  selector: 'app-todo-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [AsyncPipe, TimeAgoPipe, HighlightDirective],
  template: `
    <h2>My Todos</h2>
 
    <form (submit)="addTodo($event)">
      <input
        #todoInput
        placeholder="Cần làm gì?"
        appHighlight="'#e8f5e9'"
      >
      <button type="submit">Thêm</button>
    </form>
 
    @if (todos$ | async; as todos) {
      @for (todo of todos; track todo.id) {
        <div
          class="todo-item"
          [class.completed]="todo.completed"
        >
          <input
            type="checkbox"
            [checked]="todo.completed"
            (change)="toggle(todo)"
          >
          <span>{{ todo.title }}</span>
          <small>{{ todo.createdAt | timeAgo }}</small>
          <button (click)="delete(todo.id)">Xóa</button>
        </div>
      } @empty {
        <p>Chưa có todo nào. Thêm một cái ở trên!</p>
      }
    } @else {
      <p>Đang tải...</p>
    }
  `
})
export class TodoListComponent {
  private todoService = inject(TodoService);
 
  private refresh$ = new BehaviorSubject<void>(undefined);
 
  todos$ = this.refresh$.pipe(
    switchMap(() => this.todoService.getTodos())
  );
 
  @ViewChild('todoInput') todoInput!: ElementRef<HTMLInputElement>;
 
  addTodo(event: Event) {
    event.preventDefault();
    const title = this.todoInput.nativeElement.value.trim();
    if (!title) return;
 
    this.todoService.addTodo(title).subscribe(() => {
      this.todoInput.nativeElement.value = '';
      this.refresh$.next();
    });
  }
 
  toggle(todo: Todo) {
    this.todoService.toggleTodo(todo).subscribe(() => {
      this.refresh$.next();
    });
  }
 
  delete(id: number) {
    this.todoService.deleteTodo(id).subscribe(() => {
      this.refresh$.next();
    });
  }
}

Ví dụ này demo:

  • Components với template binding và event handling
  • Services với HTTP calls và DI qua inject()
  • Pipes (async, custom timeAgo)
  • Directives (custom appHighlight)
  • RxJS patterns (BehaviorSubject, switchMap)
  • OnPush change detection
  • Angular hiện đại (standalone, cú pháp @if/@for)

10. Angular Router — Điều Hướng Giữa Các Trang

Angular Router là thư viện navigation chính thức, tích hợp sâu với framework. Mọi Angular app hơn một trang đều cần nó.

Cài Đặt Cơ Bản (Standalone API)

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { NotFoundComponent } from './not-found.component';
 
export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: '**', component: NotFoundComponent }, // Wildcard — 404
];
 
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
  ]
});
<!-- app.component.html — router outlet render component khớp -->
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/products">Products</a>
</nav>
 
<router-outlet />

Đọc Route Parameters

@Component({
  selector: 'app-product-detail',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (product$ | async; as product) {
      <h1>{{ product.name }}</h1>
      <p>{{ product.description }}</p>
    }
  `
})
export class ProductDetailComponent {
  private route = inject(ActivatedRoute);
  private productService = inject(ProductService);
 
  // Đọc route param reactively — update khi param thay đổi
  product$ = this.route.paramMap.pipe(
    map(params => params.get('id')!),
    switchMap(id => this.productService.getProduct(id))
  );
}

Lazy Loading — Chia Nhỏ Bundle

Lazy loading là killer feature của Angular Router: chỉ load feature module khi user navigate đến.

// app.routes.ts — lazy load admin feature
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.adminRoutes),
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./products/product-list.component').then(m => m.ProductListComponent),
  },
];
 
// admin/admin.routes.ts — chỉ load khi user navigate đến /admin
export const adminRoutes: Routes = [
  { path: '', component: AdminDashboardComponent },
  { path: 'users', component: AdminUsersComponent },
  { path: 'settings', component: AdminSettingsComponent },
];

Điều này giữ initial bundle nhỏ — users chỉ download code cho trang họ thực sự truy cập.

Route Guards — Bảo Vệ Routes

Guards quyết định navigation có được phép không:

// auth.guard.ts
export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);
 
  return auth.isLoggedIn$.pipe(
    map(isLoggedIn => {
      if (isLoggedIn) return true;
      return router.createUrlTree(['/login']); // Redirect đến login
    })
  );
};
 
// Áp dụng cho routes được bảo vệ
export const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
    canActivate: [authGuard],
  },
];

Điều Hướng Bằng Code

@Component({ standalone: true })
export class LoginComponent {
  private router = inject(Router);
  private auth = inject(AuthService);
 
  login(credentials: Credentials) {
    this.auth.login(credentials).subscribe(() => {
      this.router.navigate(['/dashboard']);
      // Với query params:
      this.router.navigate(['/products'], { queryParams: { sort: 'price' } });
    });
  }
}

11. Reactive Forms — Xử Lý Form Type-Safe

Angular cung cấp hai cách tiếp cận forms: Template-driven forms (đơn giản, directive-based) và Reactive Forms (explicit, scalable, testable). Với bất cứ thứ gì phức tạp hơn một input đơn giản, hãy dùng Reactive Forms.

Xây Dựng Một Form

@Component({
  selector: 'app-register',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label>Tên</label>
      <input formControlName="name" />
      @if (form.get('name')?.invalid && form.get('name')?.touched) {
        <span class="error">Tên là bắt buộc (tối thiểu 2 ký tự)</span>
      }
 
      <label>Email</label>
      <input formControlName="email" type="email" />
      @if (form.get('email')?.errors?.['email'] && form.get('email')?.touched) {
        <span class="error">Nhập email hợp lệ</span>
      }
 
      <label>Mật khẩu</label>
      <input formControlName="password" type="password" />
 
      <label>Xác nhận mật khẩu</label>
      <input formControlName="confirmPassword" type="password" />
      @if (form.errors?.['passwordMismatch'] && form.touched) {
        <span class="error">Mật khẩu không khớp</span>
      }
 
      <button type="submit" [disabled]="form.invalid">Đăng ký</button>
    </form>
  `
})
export class RegisterComponent {
  private fb = inject(FormBuilder);
 
  form = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmPassword: ['', Validators.required],
  }, { validators: this.passwordMatchValidator });
 
  private passwordMatchValidator(group: FormGroup) {
    const password = group.get('password')?.value;
    const confirm = group.get('confirmPassword')?.value;
    return password === confirm ? null : { passwordMismatch: true };
  }
 
  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value); // Fully typed!
    }
  }
}

Typed Forms (Angular 14+)

Angular 14 giới thiệu strongly typed forms — form.value và form.controls được type đầy đủ:

const profileForm = new FormGroup({
  name: new FormControl('', { nonNullable: true }),
  email: new FormControl('', { nonNullable: true }),
  age: new FormControl<number | null>(null),
});
 
profileForm.value.name;   // string
profileForm.value.age;    // number | null
profileForm.value.xyz;    // ❌ TypeScript error — field không tồn tại

Dynamic Forms — FormArray

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form">
      <div formArrayName="skills">
        @for (control of skills.controls; track $index) {
          <div>
            <input [formControlName]="$index" placeholder="Skill" />
            <button type="button" (click)="removeSkill($index)">Xóa</button>
          </div>
        }
      </div>
      <button type="button" (click)="addSkill()">Thêm Skill</button>
    </form>
  `
})
export class ProfileFormComponent {
  private fb = inject(FormBuilder);
 
  form = this.fb.group({
    name: ['', Validators.required],
    skills: this.fb.array([this.fb.control('')]),
  });
 
  get skills() {
    return this.form.get('skills') as FormArray;
  }
 
  addSkill() { this.skills.push(this.fb.control('')); }
  removeSkill(index: number) { this.skills.removeAt(index); }
}

Custom Validators

// Sync validator
export function noSpacesValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const hasSpaces = /\s/.test(control.value);
    return hasSpaces ? { noSpaces: true } : null;
  };
}
 
// Async validator (kiểm tra username có available không)
export function usernameAvailableValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return timer(300).pipe(
      switchMap(() => userService.checkUsername(control.value)),
      map(available => available ? null : { usernameTaken: true }),
      catchError(() => of(null))
    );
  };
}
 
// Sử dụng
form = this.fb.group({
  username: [
    '',
    [Validators.required, noSpacesValidator()],
    [usernameAvailableValidator(this.userService)],
  ],
});

Bài Tập Thực Hành

Bài Tập 1: Component Với Signals

Tạo component shopping cart:

  • Dùng signal() cho cart items
  • Dùng computed() cho tổng giá và số lượng sản phẩm
  • Có @Input signal cho phần trăm discount
  • Dùng OnPush change detection

Bài Tập 2: Service Với HTTP và RxJS

Xây dựng GithubService:

  • Fetch repos của user từ GitHub API (https://api.github.com/users/:username/repos)
  • Dùng debounceTime + switchMap cho search input
  • Xử lý lỗi với catchError
  • Hiển thị repos với async pipe

Bài Tập 3: Custom Directive

Xây dựng directive appAutoFocus:

  • Tự động focus element khi render
  • Nhận enabled input để bật/tắt behavior
  • Hoạt động trên bất kỳ <input> hoặc <textarea> nào

Bài Tập 4: Route Guard + Lazy Loading

Cài đặt feature module với:

  • Route /dashboard được bảo vệ bởi authGuard
  • Dashboard module lazy-loaded với loadChildren
  • Redirect đến /login khi chưa đăng nhập
  • Điều hướng bằng code sau khi login thành công

Bài Tập 5: Reactive Form Với Validation

Tạo checkout form với:

  • Tên, email, số thẻ tín dụng (16 chữ số), ngày hết hạn
  • Custom validator cho định dạng thẻ tín dụng
  • Async validator mô phỏng network check (dùng timer(500))
  • Hiển thị lỗi field chỉ sau khi field được touched

Tổng Kết và Key Takeaways

Angular là framework toàn diện cung cấp mọi thứ bạn cần để xây dựng enterprise-grade applications. Đây là những gì chúng ta đã cover:

ConceptKey Insight
ComponentsBuilding blocks — dùng signals cho inputs, standalone là mặc định
TemplatesBốn loại binding — ưu tiên cú pháp @if/@for mới
DIDùng hàm inject(), ưu tiên providedIn: 'root' cho services
LifecyclengOnInit để setup, ngOnDestroy để cleanup, takeUntilDestroyed cho cleanup hiện đại
DirectivesMở rộng HTML — attribute cho behavior, structural cho layout
PipesBiến đổi data hiển thị — dùng async pipe cho Observables
Change DetectionDùng OnPush + signals cho performance tốt nhất
RxJSThiết yếu cho async operations — học map, switchMap, combineLatest
RouterLazy load features, guard routes được bảo vệ, đọc params reactively
Reactive FormsExplicit, type-safe, testable — dùng cho bất cứ thứ gì phức tạp hơn input đơn giản

Tiếp Theo Là Gì?

Khi đã thoải mái với các core concepts này, hãy khám phá:

  • NgRx hoặc Signal Store — State management cho ứng dụng phức tạp
  • Angular Material — Thư viện UI component có sẵn
  • Server-Side Rendering — Angular Universal / Angular SSR
  • Testing — Unit tests với Jasmine/Karma, E2E với Cypress/Playwright

Bài Viết Liên Quan


Tiếp theo: Angular Router Deep Dive — Guards, resolvers, lazy loading strategies

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.