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
NgModulecho 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ần | Mục đích |
|---|---|
@Component decorator | Metadata — selector, template, styles |
selector | Tag HTML dùng để render component (<app-greeting>) |
template / templateUrl | Cấu trúc HTML (inline hoặc file riêng) |
| Class body | Logic, 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áp | Hướng | Ví 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ùngtrackđể 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ự propkeytrong 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ưnginject()đượ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
| Hook | Chạy khi | Dùng cho |
|---|---|---|
ngOnChanges | Mỗi khi input thay đổi | Phản ứng khi input properties thay đổi |
ngOnInit | Một lần, sau ngOnChanges đầu tiên | API calls, fetch data ban đầu, setup logic |
ngDoCheck | Mỗi change detection cycle | Custom change detection logic (dùng ít thôi!) |
ngAfterContentInit | Một lần, sau content projection | Truy cập projected content (<ng-content>) |
ngAfterViewInit | Một lần, sau khi view render | Truy cập DOM, @ViewChild |
ngAfterViewChecked | Sau mỗi lần check view | Phản ứng khi view thay đổi (dùng ít thôi!) |
ngOnDestroy | Một lần, trước khi component bị xóa | Cleanup: 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
DestroyRefvà helpertakeUntilDestroyedđể đơ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
asyncpipe? Nó xử lý subscription/unsubscription tự động — không memory leaks, không cần cleanup trongngOnDestroy.
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:
- Reference của
@Inputthay đổi (không phải chỉ mutation property) - Event handler trong component được kích hoạt
- Observable liên kết qua
asyncpipe emit giá trị mới ChangeDetectorRef.markForCheck()được gọi thủ công
Best practice: Dùng
OnPushcho 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
| Operator | Mục đích | Ví dụ sử dụng |
|---|---|---|
map | Biến đổi giá trị emit | Convert API response |
filter | Lọc giá trị thỏa điều kiện | Bỏ qua giá trị rỗng |
tap | Side effects (logging, debugging) | tap(x => console.log(x)) |
switchMap | Hủy cái trước, subscribe cái mới | Search, route params |
mergeMap | Subscribe tất cả (song song) | Bulk operations |
concatMap | Subscribe tuần tự (có thứ tự) | Upload files tuần tự |
catchError | Xử lý lỗi | API error handling |
debounceTime | Đợi khoảng dừng giữa emissions | Search input |
distinctUntilChanged | Bỏ qua giá trị liên tiếp trùng | Search input |
takeUntil | Complete khi notifier emit | Component cleanup |
combineLatest | Kết hợp latest từ nhiều nguồn | Multiple 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, customtimeAgo) - 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ạiDynamic 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ó
@Inputsignal cho phần trăm discount - Dùng
OnPushchange 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+switchMapcho search input - Xử lý lỗi với
catchError - Hiển thị repos với
asyncpipe
Bài Tập 3: Custom Directive
Xây dựng directive appAutoFocus:
- Tự động focus element khi render
- Nhận
enabledinput để 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ởiauthGuard - Dashboard module lazy-loaded với
loadChildren - Redirect đến
/loginkhi 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:
| Concept | Key Insight |
|---|---|
| Components | Building blocks — dùng signals cho inputs, standalone là mặc định |
| Templates | Bốn loại binding — ưu tiên cú pháp @if/@for mới |
| DI | Dùng hàm inject(), ưu tiên providedIn: 'root' cho services |
| Lifecycle | ngOnInit để setup, ngOnDestroy để cleanup, takeUntilDestroyed cho cleanup hiện đại |
| Directives | Mở rộng HTML — attribute cho behavior, structural cho layout |
| Pipes | Biến đổi data hiển thị — dùng async pipe cho Observables |
| Change Detection | Dùng OnPush + signals cho performance tốt nhất |
| RxJS | Thiết yếu cho async operations — học map, switchMap, combineLatest |
| Router | Lazy load features, guard routes được bảo vệ, đọc params reactively |
| Reactive Forms | Explicit, 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
- TypeScript Complete Guide — TypeScript fundamentals trước khi dive vào Angular
- RxJS Deep Dive — Master reactive programming patterns dùng trong Angular
- Clean Architecture — Áp dụng clean architecture principles cho Angular apps
- Domain-Driven Design — Cấu trúc Angular features phức tạp với DDD
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.