1. Giới thiệu
Nếu đã từng làm việc với Angular phiên bản cũ, bạn có thể quen thuộc với việc phải tạo một NgModule để khai báo các component, directive và pipe trước khi sử dụng chúng.
Ví dụ, khi tạo một component mới, chúng ta thường phải khai báo nó trong declarations của một NgModule.
Đây là cách tổ chức ứng dụng Angular truyền thống. Tuy nhiên, từ Angular 14, Angular đã giới thiệu một cách tiếp cận mới mang tên Standalone Components.
Standalone giúp component có thể tự quản lý các dependency cần thiết cho template của mình mà không cần phải khai báo trong một NgModule.
Đến Angular 19, Standalone đã trở thành mặc định. Vì vậy, khi tạo một project mới bằng Angular 22, chúng ta có thể phát triển ứng dụng mà không cần tự tạo AppModule như trước đây.
Trong bài viết này, chúng ta sẽ tìm hiểu:
- Standalone Components là gì?
- Vì sao Angular chuyển sang Standalone?
- Tạo project Angular 22.
- Xây dựng một Homepage sử dụng Standalone Components.
- Tạo một component theo cách truyền thống với NgModule.
- So sánh hai cách tiếp cận.
2. Standalone Components là gì?
Trong Angular, Component là đơn vị cơ bản để xây dựng giao diện người dùng. Mỗi component thường bao gồm:
- TypeScript: xử lý logic và dữ liệu.
- HTML: định nghĩa giao diện.
- SCSS/CSS: định nghĩa giao diện trình bày.
Trước Angular 14, một component thông thường phải thuộc về một NgModule. NgModule chịu trách nhiệm khai báo và quản lý các component, directive, pipe liên quan.
Ví dụ kiến trúc truyền thống:
flowchart TD
A["AppModule"] --> B["AppComponent"]
A --> C["HomeComponent"]
A --> D["HeaderComponent"]
A --> E["FooterComponent"]
style A fill:#dbeafe,stroke:#2563eb
Với Standalone Components, mỗi component có thể tự khai báo dependency của mình thông qua thuộc tính imports trong @Component.
Ví dụ:
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
imports: [],
templateUrl: './home.html',
styleUrl: './home.scss'
})
export class HomeComponent {
}
Bạn có thể nhận thấy component không cần khai báo trong một NgModule nào cả.
Lưu ý về lịch sử:
- Angular 14: giới thiệu Standalone Components.
- Angular 15: Standalone Components trở thành stable.
- Angular 19: Standalone trở thành mặc định cho component, directive và pipe.
- Angular 22: tiếp tục sử dụng Standalone làm cách tiếp cận mặc định cho ứng dụng mới.
Điều đó có nghĩa là khi sử dụng Angular 22, chúng ta không cần khai báo standalone: true trong decorator nữa. Nếu muốn tạo component theo cách cũ, chúng ta phải khai báo rõ standalone: false.
3. Standalone Components có lợi thế gì?
3.1. Giảm sự phụ thuộc vào NgModule
Trong kiến trúc truyền thống, khi tạo một component mới, chúng ta thường phải thực hiện thêm bước khai báo trong NgModule.
Ví dụ:
@NgModule({
declarations: [
HomeComponent,
HeaderComponent,
FooterComponent
],
imports: [
CommonModule
]
})
export class HomeModule {
}
Với Standalone, component có thể được sử dụng trực tiếp mà không cần bước khai báo này.
3.2. Dependency rõ ràng hơn
Một component Standalone khai báo trực tiếp những component, directive hoặc pipe mà nó sử dụng trong template.
Ví dụ:
@Component({
selector: 'app-home',
imports: [
HeaderComponent,
FooterComponent,
ReadingStatsComponent
],
templateUrl: './home.html'
})
export class HomeComponent {
}
Khi đọc component, chúng ta có thể biết ngay nó đang phụ thuộc vào những thành phần nào.
3.3. Hỗ trợ Lazy Loading
Standalone kết hợp rất tốt với Angular Router. Chúng ta có thể tải một component khi người dùng truy cập vào route tương ứng.
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () =>
import('./pages/dashboard/dashboard')
.then(m => m.DashboardComponent)
},
{
path: 'books',
loadComponent: () =>
import('./pages/books/books')
.then(m => m.BooksComponent)
}
];
Thay vì phải tải tất cả giao diện ngay từ đầu, Angular có thể tải component theo nhu cầu.
Lưu ý: Lazy Loading không phải chỉ Standalone mới hỗ trợ. NgModule cũng hỗ trợ Lazy Loading thông qua loadChildren. Điểm thuận lợi của Standalone là có thể tải trực tiếp một component bằng loadComponent.
3.4. Dễ tổ chức và tái sử dụng
Mỗi component có thể hoạt động độc lập và được import vào nhiều component khác nhau.
Điều này giúp việc chia nhỏ giao diện, phát triển theo feature và bảo trì ứng dụng trở nên thuận tiện hơn.
4. Tạo project Angular 22
4.1. Kiểm tra môi trường
Trước tiên, chúng ta cần cài đặt Node.js và Angular CLI.
Kiểm tra phiên bản Node.js:
node -v
npm -v
Tham khảo yêu cầu Node.js hiện hành trong tài liệu Angular 22 trước khi cài đặt. Tài liệu cài đặt Angular hiện yêu cầu Node.js 22.22.3 trở lên.
Cài Angular CLI phiên bản 22:
npm install -g @angular/cli@22
Kiểm tra Angular CLI:
ng version
4.2. Khởi tạo project
Chúng ta sẽ xây dựng một ứng dụng nhỏ có tên ReadingDashboard, dùng để theo dõi mục tiêu đọc sách.
ng new reading-dashboard --style=scss
Angular CLI sẽ hỏi một số tùy chọn khi tạo project. Chúng ta có thể chọn cấu hình mặc định, sử dụng Standalone và không cần bật SSR cho bài thực hành này.
Sau khi tạo project thành công:
cd reading-dashboard
ng serve --open
Trình duyệt sẽ mở ứng dụng tại:
http://localhost:4200
4.3. Cấu trúc project
Với cấu hình Angular CLI hiện đại, cấu trúc thư mục có thể trông như sau:
reading-dashboard/
├── public/
├── src/
│ ├── app/
│ │ ├── app.config.ts
│ │ ├── app.routes.ts
│ │ ├── app.ts
│ │ ├── app.html
│ │ └── app.scss
│ ├── index.html
│ ├── main.ts
│ └── styles.scss
├── angular.json
├── package.json
└── tsconfig.json
Một điểm cần lưu ý: Angular CLI phiên bản mới có thể sử dụng cách đặt tên file ngắn như app.ts, thay vì app.component.ts như các phiên bản hoặc cấu hình cũ.
Đây chỉ là sự khác biệt về quy ước đặt tên file, không làm thay đổi bản chất của Component.
5. Xây dựng Homepage với Standalone Components
🧭 Tư duy cấu trúc luồng chạy: Index → App → Home → Sub-components
Trước khi tạo code, hãy làm rõ một thắc mắc phổ biến: "Một trang Homepage trong Angular sẽ chạy và phân rã cấu trúc như thế nào?"
Câu trả lời là một luồng khởi tạo và phân cấp Component (Component Tree) gồm 4 tầng:
-
Tầng 1:
index.html(HTML Shell)Trình duyệt nạp file
index.html, chứa thẻ gốc<app-root></app-root>. Đây là vị trí Angular sẽ render Root Component vào DOM. -
Tầng 2:
AppComponent(Root Layout)Angular khởi tạo
AppComponentthông quabootstrapApplication(AppComponent)trongmain.ts.Với cấu hình
selector: 'app-root', Angular xác định vị trí render component tương ứng trongindex.html.Đây là nơi quản lý bộ khung toàn cục của ứng dụng như Header, Footer và
<router-outlet />để hiển thị các trang theo URL. -
Tầng 3:
HomePageComponent(Page Component)Đây là component đại diện cho toàn bộ nội dung trang chủ.
Nếu sử dụng Angular Router, khi người dùng truy cập URL trang chủ, Router sẽ render
HomePageComponentthông qua<router-outlet />trongAppComponent.Nếu chưa sử dụng Router,
AppComponentcũng có thể import và gọi trực tiếp<app-home-page />. -
Tầng 4: Sub-components (Feature Blocks)
Để tránh
HomePageComponenttrở nên quá lớn, chúng ta chia giao diện thành những component nhỏ, độc lập theo chức năng:-
ReadingStatsComponent: Hiển thị thống kê số liệu. -
WeeklyGoalComponent: Hiển thị tiến độ mục tiêu.
HomePageComponentđóng vai trò điều phối bố cục, khai báo các component con trong mảngimports: [...]và sử dụng selector tương ứng trong template. -
index.html → main.ts →
AppComponent → Angular Router →
HomePageComponent → Sub-components.
Lưu ý: Angular Router là thành phần tùy chọn. Nếu ứng dụng không sử dụng Router, Root Component có thể gọi trực tiếp Page Component.
Chúng ta sẽ xây dựng một trang Personal Reading Dashboard với những chức năng cơ bản:
- Hiển thị tổng số sách đã đọc.
- Hiển thị số sách đang đọc.
- Theo dõi mục tiêu đọc sách trong tuần.
- Hiển thị danh sách sách đang đọc.
Giao diện sẽ được chia thành các component nhỏ để dễ quản lý.
5.1. Thiết kế component tree
flowchart TD
A["AppComponent"] --> B["HomePageComponent"]
B --> C["ReadingStatsComponent"]
B --> D["WeeklyGoalComponent"]
B --> E["Book List"]
B --> F["LegacyNoticeComponent"]
E --> G["Book Item"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#dcfce7,stroke:#16a34a
Trong ví dụ này, HomePageComponent sẽ là component điều phối giao diện. Các component con chịu trách nhiệm hiển thị từng phần nội dung.
5.2. Tạo các component
Chúng ta đứng tại thư mục gốc của project Angular và chạy các lệnh:
ng generate component pages/home-page
ng generate component components/reading-stats
ng generate component components/weekly-goal
Hoặc dùng câu lệnh rút gọn tạo Home page
ng g c pages/home
pages/home: Angular sẽ tự tạo một thư mục con src/app/pages/home chứa 4 file tương ứng (.ts, .html, .scss, .spec.ts).
Vì Angular 22 mặc định tạo Standalone Component nên chúng ta không cần thêm --standalone.
Ví dụ cấu trúc thư mục:
src/app/
├── components/
│ ├── reading-stats/
│ │ ├── reading-stats.ts
│ │ ├── reading-stats.html
│ │ └── reading-stats.scss
│ └── weekly-goal/
│ ├── weekly-goal.ts
│ ├── weekly-goal.html
│ └── weekly-goal.scss
├── pages/
│ └── home-page/
│ ├── home-page.html
│ ├── home-page.scss
│ ├── home-page.spec.ts
│ └── home-page.ts
├── app.ts
└── app.html
Tên file thực tế có thể khác nếu project sử dụng cấu hình naming convention khác.
5.3. Tạo ReadingStatsComponent
Component này chịu trách nhiệm hiển thị thống kê đọc sách.
reading-stats.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-reading-stats',
imports: [],
templateUrl: './reading-stats.html',
styleUrl: './reading-stats.scss'
})
export class ReadingStatsComponent {
totalBooks = 24;
currentlyReading = 3;
readingHours = 42;
}
reading-stats.html
<section>
<h2>Reading Statistics</h2>
<div>
<h3>{{ totalBooks }}</h3>
<p>Books Completed</p>
</div>
<div>
<h3>{{ currentlyReading }}</h3>
<p>Currently Reading</p>
</div>
<div>
<h3>{{ readingHours }}</h3>
<p>Reading Hours</p>
</div>
</section>
Ở đây, component sử dụng dữ liệu nội bộ để hiển thị thống kê. Trong ứng dụng thực tế, dữ liệu có thể được truyền từ component cha hoặc lấy từ service/API.
5.4. Tạo WeeklyGoalComponent
Component này hiển thị tiến độ mục tiêu đọc sách hàng tuần.
weekly-goal.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-weekly-goal',
imports: [],
templateUrl: './weekly-goal.html',
styleUrl: './weekly-goal.scss'
})
export class WeeklyGoalComponent {
weeklyTarget = 5;
booksRead = 3;
get progress(): number {
return Math.round(
(this.booksRead / this.weeklyTarget) * 100
);
}
}
weekly-goal.html
<section>
<h2>Weekly Reading Goal</h2>
<p>
{{ booksRead }} / {{ weeklyTarget }} books completed
</p>
<p>Progress: {{ progress }}%</p>
<p>
Keep going! You are making great progress.
</p>
</section>
5.5. Tạo HomePageComponent
Đây là component quan trọng nhất trong ví dụ. Nó sẽ kết hợp các component con thành một trang hoàn chỉnh.
home-page.ts
import { Component, signal } from '@angular/core';
import { ReadingStatsComponent } from
'../../components/reading-stats/reading-stats';
import { WeeklyGoalComponent } from
'../../components/weekly-goal/weekly-goal';
interface Book {
id: number;
title: string;
author: string;
status: string;
}
@Component({
selector: 'app-home-page',
imports: [
ReadingStatsComponent,
WeeklyGoalComponent
],
templateUrl: './home-page.html',
styleUrl: './home-page.scss'
})
export class HomePageComponent {
books = signal<Book[]>([
{
id: 1,
title: 'Atomic Habits',
author: 'James Clear',
status: 'Reading'
},
{
id: 2,
title: 'Deep Work',
author: 'Cal Newport',
status: 'Reading'
},
{
id: 3,
title: 'The Psychology of Money',
author: 'Morgan Housel',
status: 'Reading'
}
]);
}
home-page.html
<main>
<header>
<h1>My Reading Dashboard</h1>
<p>Track your reading journey and build better habits.</p>
</header>
<app-reading-stats />
<app-weekly-goal />
<section>
<h2>Currently Reading</h2>
@for (book of books(); track book.id) {
<article>
<h3>{{ book.title }}</h3>
<p>Author: {{ book.author }}</p>
<p>Status: {{ book.status }}</p>
</article>
} @empty {
<p>No books found.</p>
}
</section>
</main>
Chúng ta có thể thấy một số điểm đáng chú ý:
importskhai báo hai component con mà Homepage sử dụng.signal()quản lý danh sách sách có thể thay đổi.@forduyệt qua danh sách sách.track book.idgiúp Angular nhận diện từng phần tử trong danh sách.@emptyhiển thị nội dung khi danh sách không có dữ liệu.
Đây là cú pháp Control Flow hiện đại của Angular, không cần import CommonModule chỉ để sử dụng vòng lặp này.
5.6. Kết nối Homepage với AppComponent
Bây giờ chúng ta cần hiển thị Homepage trong component gốc của ứng dụng.
app.ts
import { Component } from '@angular/core';
import { HomePageComponent } from
'./pages/home-page/home-page';
@Component({
selector: 'app-root',
imports: [HomePageComponent],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {
}
app.html
<app-home-page />
Như vậy, chúng ta đã hoàn thành một Homepage được xây dựng từ nhiều Standalone Components.
5.7. Kết quả kiến trúc
flowchart TD
A["App"] --> B["HomePage"]
B --> C["ReadingStats"]
B --> D["WeeklyGoal"]
B --> E["Currently Reading"]
E --> F["Book 1"]
E --> G["Book 2"]
E --> H["Book 3"]
Điểm quan trọng là mỗi component có thể được phát triển và kiểm thử tương đối độc lập. HomePage chỉ cần import những thành phần mà nó thực sự sử dụng.
6. Tạo một component thông thường với NgModule
Để hiểu rõ sự khác biệt giữa Standalone và cách tiếp cận truyền thống, chúng ta sẽ tạo thêm một component không phải Standalone.
Trong Angular 22, muốn tạo component theo cách cũ, chúng ta phải khai báo:
standalone: false
6.1. Tạo LegacyNoticeComponent
Chạy lệnh:
ng generate component legacy/legacy-notice --standalone=false
legacy-notice.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-legacy-notice',
standalone: false,
templateUrl: './legacy-notice.html',
styleUrl: './legacy-notice.scss'
})
export class LegacyNoticeComponent {
message = 'Welcome to the Reading Dashboard!';
}
legacy-notice.html
<section>
<h2>Reading Reminder</h2>
<p>{{ message }}</p>
</section>
Khác với Standalone Component, component này không thể được import trực tiếp vào imports của một Standalone Component.
Nó phải được khai báo trong một NgModule.
6.2. Tạo LegacyUiModule
Tạo file legacy-ui.module.ts trong thư mục legacy.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LegacyNoticeComponent } from
'./legacy-notice/legacy-notice';
@NgModule({
declarations: [
LegacyNoticeComponent
],
imports: [
CommonModule
],
exports: [
LegacyNoticeComponent
]
})
export class LegacyUiModule {
}
Giải thích:
declarations: khai báo component không phải Standalone thuộc module này.imports: import các module mà component trong module cần sử dụng.exports: cho phép module khác sử dụng component được export.
6.3. Sử dụng component truyền thống trong Homepage
Điều thú vị là chúng ta vẫn có thể sử dụng component truyền thống trong một ứng dụng Standalone.
Chỉ cần import NgModule chứa component đó.
Cập nhật home-page.ts:
import { Component, signal } from '@angular/core';
import { ReadingStatsComponent } from
'../../components/reading-stats/reading-stats';
import { WeeklyGoalComponent } from
'../../components/weekly-goal/weekly-goal';
import { LegacyUiModule } from
'../../legacy/legacy-ui.module';
@Component({
selector: 'app-home-page',
imports: [
ReadingStatsComponent,
WeeklyGoalComponent,
LegacyUiModule
],
templateUrl: './home-page.html',
styleUrl: './home-page.scss'
})
export class HomePageComponent {
// Giữ nguyên phần dữ liệu books
}
Sau đó thêm component vào home-page.html:
<main>
<app-legacy-notice />
<app-reading-stats />
<app-weekly-goal />
<!-- Các nội dung còn lại -->
</main>
Như vậy, ứng dụng Angular hiện đại vẫn có thể sử dụng lại các component cũ thông qua NgModule.
Đây là một điểm rất quan trọng khi làm việc với các dự án Angular đã tồn tại nhiều năm.
7. So sánh Standalone Component và NgModule Component
Đến đây, chúng ta đã thực hành cả hai cách tiếp cận.
Bảng dưới đây tổng hợp những điểm khác biệt chính:
| Tiêu chí | Standalone | NgModule-based |
|---|---|---|
| standalone | Mặc định true từ Angular 19 | Phải khai báo false trong Angular 19+ |
| NgModule | Không bắt buộc | Bắt buộc có module khai báo component |
| Dependency | Khai báo trong imports của component | Quản lý thông qua NgModule |
| Sử dụng component | Import trực tiếp component | Import NgModule chứa component |
| Lazy Loading | Hỗ trợ loadComponent | Thường dùng loadChildren |
| Phù hợp | Ứng dụng mới | Dự án legacy hoặc module-based |
7.1. So sánh code trực tiếp
Cách 1: Standalone Component
@Component({
selector: 'app-dashboard',
imports: [
ReadingStatsComponent,
WeeklyGoalComponent
],
templateUrl: './dashboard.html'
})
export class DashboardComponent {
}
Chúng ta chỉ cần import trực tiếp các component mà Dashboard sử dụng.
Cách 2: NgModule Component
@Component({
selector: 'app-dashboard',
standalone: false,
templateUrl: './dashboard.html'
})
export class DashboardComponent {
}
Sau đó phải khai báo trong module:
@NgModule({
declarations: [
DashboardComponent
],
imports: [
CommonModule,
ReadingModule
]
})
export class DashboardModule {
}
Nhìn vào hai ví dụ, chúng ta có thể thấy Standalone giảm bớt một lớp tổ chức trung gian và làm dependency rõ ràng hơn ở cấp component.
8. Standalone có làm ứng dụng chạy nhanh hơn không?
Đây là một câu hỏi khá phổ biến.
Câu trả lời là: Standalone không tự động làm ứng dụng nhanh hơn chỉ vì chúng ta không sử dụng NgModule.
Lợi thế lớn hơn nằm ở cách tổ chức code, dependency và khả năng phân chia ứng dụng.
Ví dụ, khi kết hợp Standalone với Lazy Loading:
flowchart TD
A["Application Start"] --> B["Load Home Page"]
B --> C{"User navigates?"}
C -->|Books| D["Load Books Component"]
C -->|Reports| E["Load Reports Component"]
C -->|Settings| F["Load Settings Component"]
Angular có thể chỉ tải phần code cần thiết khi người dùng truy cập vào từng chức năng.
Điều này có thể giúp giảm lượng JavaScript cần tải ban đầu, tùy thuộc vào cách chúng ta tổ chức và build ứng dụng.
Vì vậy, nên xem Standalone là một cải tiến về kiến trúc và trải nghiệm phát triển, chứ không phải một giải pháp tăng performance tự động.
9. Khi nào nên sử dụng Standalone?
Với một project mới sử dụng Angular 22, chúng ta nên ưu tiên Standalone Components.
Một số nguyên tắc có thể áp dụng:
- Sử dụng Standalone cho các component mới.
- Chia giao diện thành các component nhỏ theo chức năng.
- Khai báo dependency trực tiếp trong imports của component.
- Sử dụng Angular Router và loadComponent khi phù hợp.
- Sử dụng NgModule khi cần tích hợp các component hoặc thư viện legacy.
- Không cần tạo NgModule chỉ để tuân theo thói quen từ các phiên bản Angular cũ.
Nếu đang làm việc với một ứng dụng Angular cũ, chúng ta cũng không cần chuyển đổi toàn bộ sang Standalone ngay lập tức. Angular hỗ trợ migration từng bước.
10. Tổng kết
Qua bài thực hành, chúng ta đã hiểu cách Standalone Components thay đổi cách tổ chức một ứng dụng Angular.
Trước đây, NgModule đóng vai trò trung tâm trong việc khai báo và quản lý component. Với Standalone, mỗi component có thể tự quản lý các dependency của mình.
Những điểm cần ghi nhớ:
- Standalone được giới thiệu từ Angular 14, stable từ Angular 15.
- Từ Angular 19, Standalone là mặc định.
- Angular 22 tiếp tục ưu tiên kiến trúc Standalone.
- Standalone Component có thể import trực tiếp các component, directive, pipe khác.
- Component truyền thống vẫn có thể được sử dụng thông qua NgModule.
- Hai cách tiếp cận có thể cùng tồn tại trong một ứng dụng.
Nếu bắt đầu một dự án Angular mới, hãy ưu tiên Standalone. Đây là cách tiếp cận đơn giản và phù hợp với hướng phát triển hiện đại của Angular.
11. Tài liệu tham khảo
- Angular – Component Basics
- Angular – Importing and Using Components
- Angular – NgModules Overview
- Angular – Standalone Migration
- Angular – Installation Guide
- Angular – Announcing Angular v22
Happy Coding!
Nhận xét