Xây Dựng Ứng Dụng Todos MVC Đơn Giản Với Angular: Hướng Dẫn Từ A Đến Z
Xây dựng một ứng dụng Todos MVC đơn giản với Angular là một
cách tuyệt vời để rèn kỹ năng phát triển ứng dụng web và hiểu sâu về
mô hình MVC và Angular. Hướng dẫn từ A đến Z này
sẽ chỉ bạn cách cài đặt môi trường phát triển, tạo cấu trúc dự án và thiết
kế giao diện người dùng thân thiện. Qua hướng dẫn này, bạn sẽ học cách quản
lý danh sách công việc một cách dễ dàng. Tận dụng sức mạnh của
Angular và mô hình MVC, bạn sẽ xây dựng một
ứng dụng Todos linh hoạt và dễ mở rộng.
Hãy bắt đầu hành trình này và tạo ra một ứng dụng Todos MVC chất
lượng với Angular!
Các tính năng trong ứng dụng Angular Todos:
- Thêm, sửa, xóa task.
- Lọc task: Tất cả, đang thực hiện, hoàn thành.
Thay đổi trạng thái của task: Đang thực hiện => hoàn thành và ngược
lại.
Cài đặt dự án.
Trước khi chúng ta bắt đầu code thì chúng ta phải setup môi trường
Angular
và import các gói thư viện cần thiết.
Để khởi tạo một dự án Angular thì chúng ta sẽ sử dụng lệnh sau trên
terminal:
ng new angular_todos
Ở ứng dụng này chúng ta sẽ sử dụng stylesheet là SCSS và không sử
dụng routing. Các bạn nên tạo dự án sao cho phù hợp với bài viết nha.
Cài đặt các gói thư viện:
- Bootstrap: Web design front-end framework.
npm i bootstrap
- Eva icons: Open-source icons.
npm i eva-icons
Sau khi cài đặt xong các thư viện trên, đã đến lúc chúng ta sử dụng chúng
trong dự án bằng cách mở file angular.json ở thư mục gốc.
Đầu tiên, bạn cần thêm đường dẫn dưới đây vào vị trí "scripts" sau:
Đường dẫn: "node_modules/eva-icons/eva.min.js"
/* You can add global styles to this file, and also import other style files */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import "../node_modules/bootstrap/scss/bootstrap";
@import "../node_modules/bootstrap/scss/functions";
@import "../node_modules/bootstrap/scss/variables";
@import "../node_modules/bootstrap/scss/mixins";
@import "../node_modules/bootstrap/scss/utilities";
@import "../node_modules/bootstrap/scss/grid";
@import "../node_modules/eva-icons/style/eva-icons.css";
html, body {
background-color: #f5f5f5;
height: 100%;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
*:not(i) {
font-family: "Roboto", sans-serif;
}
p {
margin-bottom: 0;
}
Chúng ta cần thêm các đường dẫn để sử dụng Bootstrap cũng như
Eva-icons
nên những việc làm trên đều rất cần thiết.
Cùng với đó là chúng ta sẽ nhúng font Roboto để sử dụng trong dự
án.
Và cuối cùng là style lại một số thứ trong ứng dụng như đoạn code trên.
Xóa bỏ code mẫu
Angular sẽ tự động thêm đoạn code mẫu khi mình tạo dự án mới để
giúp chúng ta thấy được một cái gì đótrên màn hình khi chúng ta chạy dự
án. Đơn giản chỉ cần mở file src/app/app.component.html và
xòa hết nội dung bên trong. Sau này chúng ta sẽ đưa code của mình vào
trong file này. Đừng xóa file này đi nhé.
Import Forms Module
Chúng ta sẽ sử dụng FromsModule để làm việc với trường input bên trong các
component quản lý việc thêm hoặc sửa task mà chúng ta sẽ tạo sau. Bây giờ
hãy làm theo hướng dẫn bên dưới và thêm nó vào file
src/app/app.module.ts
Code cũ:
imports: [BrowserModule]
Code mới:
imports: [BrowserModule, FormsModule],
Sau cùng thì code trong file src/app/app.module.ts sẽ như thế
này:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
],
imports: [BrowserModule, FormsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Tạo model
Chúng ta cần tạo một cấu trúc dữ liệu cho task sửu dụng trong todos app.
Đó là lúc tốt nhất để tạo một model.
Hãy tạo một folder bên trong thư mục src/app có tên là
models. Bây giờ hãy tạo một file Typescript có tên là
todo.model.ts
Và đường dẫn mới đến file đó là src/app/models/todo.model.ts
Hãy mở file đó lên và nhập đoạn code bên dưới:
export class Todo {
id!: number;
content!: string;
isCompleted!: boolean;
constructor(id: number, content: string, isCompleted?: boolean) {
this.id = id;
this.content = content;
this.isCompleted = false;
}
// Another way
/*
constructor(
public id: number;
public content: string;
public isCompleted: boolean = false
) {}
*/
}
Giải thích:
Trong class Todo, chúng ta sẽ sử dụng phương thức constructor() để
định nghĩa: id, content, isCompleted.
- id: định nghĩa duy nhất cho mỗi task (todo-item).
content: Tên của task mà chúng ta sẽ nhập thông qua một trường
input.
isCompleted: Có giá trị là một boolean thông báo rằng task đó
đã hoàn thành hay chưa.
Tiếp theo đó là cần tạo một model FilteButton và một enum Filter để biểu
diễn trạng thái của các bút nhấn trong thanh filter trong ứng dụng.
Cùng trong thư mục models, hãy tạo một file Typescript có tên là
filtering.model,ts có nội dung như bên dưới:
export interface FilterButton {
type: Filter;
label: string;
isActive: boolean;
}
export enum Filter {
All,
Active,
Completed
}
Giải thích:
Trong interface FilterButton có các thuộc tính:
type, label, isActive.
type: Kiểu nút nhấn được sử dụng, ở đây sử dụng enum là Filter.
- label: Là nội dung bên trong nút nhấn.
isActive: Có giá trị là một boolean thông báo trạng thái của
nút nhấn.
Tạo services
Phục vụ trong việc quản lý dữ liệu và todos sẽ có các
services được tạo trong dự án. Chúng ta sẽ sử dụng
Local Storage để lưu trữ dữ liệu của ứng dụng. Đồng thời sẽ có một
service quản lý todos.
Bạn có thể tạo các services thủ công hoặc bằng lệnh của
CLI. Mình sẽ trình bày cả 2 cách để hiểu rõ hơn nhé.
Tạo bằng CLI:
Mở terminal tại dự án và nhập lệnh sau:
ng g s <tên file> --skip-tests=true
- ng: là từ khóa thuộc cấu trúc lệnh của CLI.
g: là generate, có thể ghi đầy đủ là generate thay vì
g.
s: là service, có thể ghi đầy đủ
là service thay vì s.
- <tên file>: tên bạn muốn đặt cho file đang tạo.
--skip-tests-true: bỏ qua file testing (bạn có thể tự tìm hiểu
phần này nha)
Sau khi chạy lệnh trên thì CLI tự động tạo cho bạn file service trong thư
mục src/app.
Lý thuyết là vậy, bây giờ hãy tạo một service quản lý dữ liệu có tên là
local-storage
Mở terminal tại dự án và nhập lệnh sau:
ng g s services/local-storage --skip-tests=true
Lệnh trên sẽ tạo một file tên là local-storage.service.ts bên trong
thư mục src/app/services
Hãy nhập nội dung bên dưới vào file local-storage.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class LocalStorageService {
storage!: Storage;
constructor() {
this.storage = window.localStorage;
}
set(key: string, value: string): void {
this.storage[key] = value;
}
get(key: string): string {
return this.storage[key] || false;
}
setObject(key: string, value: any): void {
if (!value) {
return;
}
this.storage[key] = JSON.stringify(value);
}
getObject(key: string): any {
return JSON.parse(this.storage[key] || '{}');
}
getValue<T>(key: string): T {
const obj = JSON.parse(this.storage[key] || null);
return obj || null;
}
remove(key: string): any {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
get length(): number {
return this.storage.length;
}
get isStorageEmpty(): boolean {
return this.length === 0;
}
}
Giải thích:
Đoạn mã trên định nghĩa một service trong Angular được gọi là
LocalStorageService, được sử dụng để tương tác với
localStorage trong trình duyệt. Dưới đây là giải thích từng
phương thức và thuộc tính trong service này:
storage: Storage: Đây là một thuộc tính của lớp, kiểu dữ liệu
là Storage, dùng để lưu trữ đối tượng localStorage.
constructor(): Phương thức khởi tạo của lớp
LocalStorageService. Trong phương thức này, this.storage được gán
bằng window.localStorage để truy cập và tương tác với localStorage
trong trình duyệt.
set(key: string, value: string): void: Phương thức này nhận
vào một khóa (key) và giá trị (value) dưới dạng chuỗi. Nhiệm vụ của
phương thức là lưu trữ giá trị vào localStorage bằng cách gán
this.storage[key] = value.
get(key: string): string: Phương thức này nhận vào một khóa
(key) dưới dạng chuỗi và trả về giá trị tương ứng từ localStorage.
Nếu không tìm thấy giá trị, phương thức trả về false.
setObject(key: string, value: any): void: Phương thức này
nhận vào một khóa (key) dưới dạng chuỗi và một đối tượng (value).
Nhiệm vụ của phương thức là lưu trữ đối tượng vào localStorage bằng
cách chuyển đổi đối tượng thành chuỗi JSON và gán this.storage[key]
= JSON.stringify(value).
getObject(key: string): any: Phương thức này nhận vào một
khóa (key) dưới dạng chuỗi và trả về đối tượng tương ứng từ
localStorage. Nếu không tìm thấy đối tượng, phương thức trả về một
đối tượng rỗng ({}).
getValue<T>(key: string): T: Phương thức này nhận vào
một khóa (key) dưới dạng chuỗi và trả về giá trị tương ứng từ
localStorage. Phương thức sử dụng JSON.parse để chuyển đổi chuỗi
JSON thành đối tượng. Nếu không tìm thấy giá trị, phương thức trả về
null.
remove(key: string): any: Phương thức này nhận vào một khóa
(key) dưới dạng chuỗi và xóa giá trị tương ứng từ localStorage bằng
cách sử dụng phương thức removeItem của localStorage.
clear(): Phương thức này xóa tất cả các mục trong
localStorage bằng cách sử dụng phương thức clear() của localStorage.
length: number: Đây là một thuộc tính chỉ đọc (getter) trả về
số lượng mục trong localStorage thông qua thuộc tính length của
localStorage.
isStorageEmpty: boolean: Đây là một thuộc tính chỉ đọc
(getter) trả về giá trị boolean (true hoặc false) xác định xem
localStorage có trống không. Thuộc tính này được xác định bằng cách
so sánh length của localStorage với 0.
Service LocalStorageService này cung cấp các phương thức để thao tác với
localStorage như lưu trữ và truy xuất giá trị bằng cách sử dụng khóa,
lưu trữ và truy xuất đối tượng bằng cách chuyển đổi thành chuỗi JSON,
xóa mục và xóa tất cả các mục trong localStorage.
Tiếp theo, hãy tạo một service quản lý todos có tên là todo
Mở terminal tại dự án và nhập lệnh sau:
ng g s services/todo --skip-tests=true
Lệnh trên sẽ tạo một file tên là todo.service.ts bên
trong thư mục src/app/services
Hãy nhập nội dung bên dưới vào file todo.service.ts:
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { Todo } from '../models/todo.model';
import { Filter } from '../models/filtering.model';
import { LocalStorageService } from './local-storage.service';
@Injectable({
providedIn: 'root',
})
export class TodoService {
private static readonly TodoStorageKey = 'todos';
private todos: Todo[] = [];
private filteredTodos: Todo[] = [];
private displayTodosSubject: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
private lengthSubject: BehaviorSubject<number> = new BehaviorSubject<number>(0);
private currentFilter: Filter = Filter.All;
todos$: Observable<Todo[]> = this.displayTodosSubject.asObservable();
length$: Observable<number> = this.lengthSubject.asObservable();
constructor(private storageService: LocalStorageService) {}
fetchFromLocalStorage() {
this.todos = this.storageService.getValue<Todo[]>(TodoService.TodoStorageKey) || [];
this.filteredTodos = [...this.todos];
this.updateTodosData();
}
updateToLocalStorage() {
this.storageService.setObject(TodoService.TodoStorageKey, this.todos);
this.filterTodos(this.currentFilter, false);
this.updateTodosData();
}
filterTodos(filter: Filter, isFiltering: boolean = true) {
this.currentFilter = filter;
switch (filter) {
case Filter.Active:
this.filteredTodos = this.todos.filter((todo) => !todo.isCompleted);
break;
case Filter.Completed:
this.filteredTodos = this.todos.filter((todo) => todo.isCompleted);
break;
case Filter.All:
this.filteredTodos = [...this.todos];
break;
}
if (isFiltering) {
this.updateTodosData();
}
}
addTodo(content: string) {
const date = new Date(Date.now()).getTime();
const newTodo = new Todo(date, content);
this.todos.unshift(newTodo);
this.updateToLocalStorage();
}
changeTodoStatus(id: number, isCompleted: boolean) {
const index = this.todos.findIndex((todo) => todo.id === id);
const todo = this.todos[index];
todo.isCompleted = isCompleted;
this.todos.splice(index, 1, todo);
this.updateToLocalStorage();
}
editTodo(id: number, content: string) {
const index = this.todos.findIndex((todo) => todo.id === id);
const todo = this.todos[index];
todo.content = content;
this.todos.splice(index, 1, todo);
this.updateToLocalStorage();
}
deleteTodo(id: number) {
const index = this.todos.findIndex((todo) => todo.id === id);
this.todos.splice(index, 1);
this.updateToLocalStorage();
}
toggleAllStatus() {
this.todos = this.todos.map((todo) => ({
...todo,
isCompleted: !this.todos.every((todo) => todo.isCompleted),
}));
this.updateToLocalStorage();
}
clearCompleted() {
this.todos = this.todos.filter((todo) => !todo.isCompleted);
this.updateToLocalStorage();
}
private updateTodosData() {
this.displayTodosSubject.next(this.filteredTodos);
this.lengthSubject.next(this.todos.length);
}
}
Giải thích:
Đoạn code trên là một service trong Angular được sử dụng để quản lý các
công việc (todos).
TodoStorageKey: Một hằng số được sử dụng làm khóa để lưu trữ
todos vào local storage.
todos, filteredTodos: Mảng todos và filteredTodos chứa danh
sách các công việc và danh sách công việc đã được lọc.
displayTodosSubject, lengthSubject: BehaviorSubject được sử
dụng để giữ và phát các giá trị liên quan đến danh sách công việc và
số lượng công việc.
currentFilter: Biến để lưu trữ bộ lọc hiện tại cho danh sách
công việc.
todos$, length$: Các thuộc tính Observable để theo dõi các
thay đổi của displayTodosSubject và lengthSubject.
constructor(): Phương thức khởi tạo của service, được gọi khi
một instance của service được tạo. Trong phương thức này,
LocalStorageService được inject vào service.
fetchFromLocalStorage(): Phương thức để lấy danh sách công
việc từ local storage và cập nhật các biến liên quan.
updateToLocalStorage(): Phương thức để cập nhật danh sách
công việc vào local storage và cập nhật các biến liên quan.
filterTodos(filter, isFiltering): Phương thức để lọc danh
sách công việc dựa trên bộ lọc được chọn. isFiltering là một cờ để
xác định xem có cần cập nhật danh sách công việc hiển thị hay không.
addTodo(content): Phương thức để thêm một công việc mới vào
danh sách công việc.
changeTodoStatus(id, isCompleted): Phương thức để thay đổi
trạng thái của một công việc.
editTodo(id, content): Phương thức để chỉnh sửa nội dung của
một công việc.
- deleteTodo(id): Phương thức để xóa một công việc.
toggleAllStatus(): Phương thức để thay đổi trạng thái của tất
cả công việc.
clearCompleted(): Phương thức để xóa tất cả các công việc đã
hoàn thành.
updateTodosData(): Phương thức để cập nhật các giá trị trong
displayTodosSubject và lengthSubject.
Service này sử dụng LocalStorageService để lưu trữ và truy xuất danh
sách công việc từ local storage. Các thành phần khác trong ứng dụng có
thể sử dụng TodoService để thực hiện các thao tác CRUD (Create, Read,
Update, Delete) trên danh sách công việc.
Tạo thủ công:
Đầu tiên, các bạn tạo một folder có tên là services bên
trong thư mục src/app của ứng dụng. Lần lượt tạo 2 file Typescript
tên là local-storage.service.ts và todo.service.ts
Cấu trúc file service chuẩn thường được sử dụng sẽ như ví dụ sau:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class LocalStorageService {
}
Chúng ta sẽ viết các phương thức bên trong class và sau đó sẽ được inject
vào các component để sử dụng.
Nội dung bên trong file local-storage.service.ts và
todo.service.ts cũng giống như tạo bằng CLI ở trên.
Vậy là cơ bản chúng ta đã thêm và tạo được những thành phần cần thiết để
sử dụng trong ứng dụng. Bây giờ hãy tiến hành tạo các component quản lý
các các phần của ứng dụng todos.
Tạo Components
Trong Angular, một component là một khối xây dựng cơ bản để xây dựng
giao diện người dùng. Nó định nghĩa một phần tử giao diện độc lập, có
thể được sử dụng và tái sử dụng trong ứng dụng.
Một component trong Angular bao gồm ba phần chính:
Template (Mẫu): Template xác định giao diện người dùng của
component bằng cách sử dụng các thẻ HTML, các directive, và các
hướng dẫn để hiển thị dữ liệu và xử lý sự kiện. Template được viết
bằng ngôn ngữ HTML với các mở rộng của Angular như directive và
binding.
Class: Class đại diện cho logic của component. Nó chứa các
thuộc tính và phương thức để xử lý dữ liệu, xử lý sự kiện và tương
tác với các service và các thành phần khác trong ứng dụng. Class
được viết bằng TypeScript.
Metadata: Metadata cung cấp các thông tin bổ sung về
component như tên, selector (tên gọi của component khi sử dụng),
đường dẫn đến template, style, và các dependencies khác. Metadata
được khai báo bằng cách sử dụng decorator @Component từ Angular core
library.
Một component có thể có các thành phần con (child components) và các
directive nhưng chỉ có thể có một thành phần cha (parent component). Các
component có thể truyền dữ liệu và tương tác với nhau thông qua các
Input và Output properties.
Để sử dụng một component trong ứng dụng Angular, bạn có thể tạo và sử
dụng nó như một thẻ HTML trong template của một component khác, hoặc sử
dụng nó như một route để hiển thị trang riêng biệt.
Component trong Angular giúp tách biệt logic và giao diện, tạo ra sự tái
sử dụng, dễ bảo trì và phát triển ứng dụng. Nó là một khái niệm cốt lõi
trong cấu trúc của Angular framework.
Để tạo component trong Angular cũng sẽ có 2 cách là thủ công và bằng lệnh
CLI. Mình nghĩ các bạn nên tạo bằng lệnh CLI sẽ tiết kiệm thời gian trong
quá trình code hơn.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/<tên component> --skip-tests=true
Lệnh trên sẽ tạo một file tên là todo.service.ts bên
trong thư mục src/app/services
Trong ứng dụng này ta sẽ tạo các component sau:
- Header: là component quản lý header.
- Footer: là component quản lý footer.
Todo-input: là component quản lý sự kiện nhập vào trường input.
- Todo-item: là component quản lý một task.
- Todo-list: là component quản lý danh sách các task.
Todo-input Component
Todo-input (Component quản lý sự kiện nhập vào trường input):
Component Todo-input là thành phần quản lý phần nhập liệu của công
việc trong ứng dụng.
Chứa một trường input để người dùng nhập nội dung công việc.
Component này sẽ xử lý sự kiện khi người dùng nhập liệu và thêm
công việc mới vào danh sách công việc.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/todo-input --skip-tests=true
Lệnh trên sẽ tạo một folder tên là todo-input bên
trong thư mục src/app/components.
Todo-input Component Template
Path: src/app/components/todo-input.component.html
<input
type="text"
(keyup.enter)="onSubmit()"
[(ngModel)]="todoContent"
class="input-todos w-100 h-100"
placeholder="What needs to be done?"
required
/>
Todo-input Component Style
Path: src/app/components/todo-input.component.scss
.input-todos {
outline: none;
border: none;
margin-left: 10px;
font-size: 35px;
}
::placeholder {
color: rgba(0, 0, 0, 0.5);
opacity: 0.5;
}
Todo-input Component
Path: src/app/components/todo-input.component.ts
import { Component } from '@angular/core';
import { TodoService } from 'src/app/services/todo.service';
@Component({
selector: 'app-todo-input',
templateUrl: './todo-input.component.html',
styleUrls: ['./todo-input.component.scss'],
})
export class TodoInputComponent {
todoContent: string = '';
constructor(private todosService: TodoService) {}
onSubmit() {
if (this.todoContent.trim() === '') {
this.todoContent = '';
return false;
}
this.todosService.addTodo(this.todoContent);
this.todoContent = '';
return true;
}
}
todoContent: string = '';: Một biến todoContent kiểu string
được khởi tạo với giá trị rỗng. Biến này sẽ chứa nội dung của công
việc được nhập vào.
constructor(private todosService: TodoService) {}: Phương
thức khởi tạo của component, trong đó TodoService được inject vào
component thông qua dependency injection. Điều này cho phép
component truy cập và sử dụng các phương thức của TodoService để
thao tác với danh sách công việc.
onSubmit(): Phương thức được gọi khi người dùng nhấn nút
submit hoặc nhấn phím Enter để thêm công việc. Trong phương thức
này, điều kiện if (this.todoContent.trim() === '') kiểm tra xem
nội dung công việc có bị rỗng hay không. Nếu rỗng, nội dung công
việc sẽ được xóa và phương thức sẽ trả về false. Nếu nội dung công
việc không rỗng, phương thức addTodo của todosService sẽ được gọi
để thêm công việc vào danh sách công việc thông qua
this.todosService.addTodo(this.todoContent). Sau đó, nội dung công
việc sẽ được xóa và phương thức trả về true để cho phép các xử lý
khác (nếu có) xảy ra.
Header Component
Header Component là thành phần quản lý phần đầu trang của ứng dụng.
Nó có thể chứa tiêu đề, logo, các nút điều hướng hoặc bất kỳ thành
phần nào liên quan đến phần đầu trang.
Component này có thể được sử dụng để thể hiện một header cố định
hoặc có thể thay đổi theo ngữ cảnh.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/header --skip-tests=true
Lệnh trên sẽ tạo một folder tên là header bên trong thư
mục src/app/components.
Header Component Template
Path: src/app/components/header.component.html
<div class="d-flex align-items-center h-100">
<span class="icon-wrapper h-100 text-center" (click)="toggleAllStatus()">
<i class="eva eva-chevron-down"></i>
</span>
<app-todo-input></app-todo-input>
</div>
Header Component Style
Path: src/app/components/header.component.scss
.icon-wrapper {
width: 40px;
line-height: 45px;
font-size: 40px;
color: grey;
background: white;
transition: 250ms all ease-in-out;
cursor: pointer;
&:hover {
color: black;
}
}
app-todo-input {
width: 100%;
}
Header Component
Path: src/app/components/header.component.ts
import { Component } from '@angular/core';
import { TodoService } from 'src/app/services/todo.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
constructor(private todoService: TodoService) {}
toggleAllStatus() {
this.todoService.toggleAllStatus();
}
}
constructor(private todoService: TodoService) {}: Phương thức
khởi tạo của component, trong đó TodoService được inject vào
component thông qua dependency injection. Điều này cho phép
component truy cập và sử dụng các phương thức của TodoService để
thao tác với danh sách công việc.
toggleAllStatus(): Phương thức được gọi khi người dùng thực
hiện một hành động để chuyển đổi trạng thái của tất cả công việc
trong danh sách. Trong phương thức này, phương thức
toggleAllStatus() của todoService được gọi để thực hiện chức năng
chuyển đổi trạng thái của tất cả công việc trong danh sách.
Todo-item Component
Todo-item (Component quản lý một task):
Component Todo-item là thành phần quản lý một công việc trong danh
sách công việc.
Nó sẽ hiển thị thông tin về công việc như nội dung, trạng thái
hoàn thành và các tùy chọn chỉnh sửa, xóa công việc.
Component này sẽ xử lý các sự kiện khi người dùng thay đổi trạng
thái hoàn thành công việc, chỉnh sửa nội dung công việc hoặc xóa
công việc khỏi danh sách.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/todo-item --skip-tests=true
Lệnh trên sẽ tạo một folder tên là todo-item bên
trong thư mục src/app/components.
Todo-item Component Template
Path: src/app/components/todo-item.component.html
<div
class="todo-item d-flex justify-content-between align-items-center"
(mouseover)="isHovered = true"
(mouseout)="isHovered = false"
>
<div class="todo">
<input
type="checkbox"
[id]="todo.id"
[ngClass]="{ checked: todo.isCompleted }"
[checked]="todo.isCompleted"
class="toggle text-center"
(change)="changeTodoStatus()"
/>
<label [@fadeStrikeThrough]="todo.isCompleted ? 'completed' : 'active'" [for]="todo.id">{{
todo.content
}}</label>
</div>
<div class="d-flex align-items-center">
<span
class="icon-wrapper text-center edit"
[hidden]="todo.isCompleted"
[ngClass]="{ active: isHovered }"
>
<i class="eva eva-edit-outline" (click)="isEditing = true"></i>
</span>
<span
class="icon-wrapper text-center"
[ngClass]="{ active: isHovered }"
(click)="handleDelete()"
>
<i class="eva eva-close"></i>
</span>
</div>
<form class="edit-form" (keyup)="submitEdit($event)" *ngIf="isEditing">
<input type="text" name="editTodo" [(ngModel)]="todo.content" />
</form>
</div>
Todo-item Component Style
Path: src/app/components/todo-item.component.scss
.todo-item {
min-height: 50px;
padding: 0 5px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
& .todo {
position: relative;
cursor: pointer;
font-size: 18px;
user-select: none;
& .toggle {
width: 40px;
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none;
outline: none;
appearance: none;
}
& .toggle + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
& .toggle.checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
& label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
}
}
& .icon-wrapper {
height: 25px;
width: 25px;
font-size: 25px;
color: white;
background: white;
transition: 250ms all ease-in-out;
&:hover {
transform: scale(1.2, 1.2);
color: rgb(247, 78, 48);
}
&.active {
color: tomato;
cursor: pointer;
}
&.edit {
&:hover {
transform: scale(1.2, 1.2);
color: rgb(0, 162, 255);
}
&.active {
color: deepskyblue;
cursor: pointer;
}
}
}
& .edit-form {
position: absolute;
width: 98.5%;
height: 100%;
background: white;
& input {
height: 92%;
width: 92%;
margin-left: 35px;
font-size: 18px;
padding-left: 10px;
}
}
}
Todo-item Component
Path: src/app/components/todo-item.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { Todo } from 'src/app/models/todo.model';
const fadeStrikeThroughAnimation = trigger('fadeStrikeThrough', [
state(
'active',
style({
fontSize: '18px',
color: 'black',
})
),
state(
'completed',
style({
fontSize: '17px',
color: 'lightgrey',
textDecoration: 'line-through',
})
),
transition('active <=> completed', [animate(250)]),
]);
@Component({
selector: 'app-todo-item',
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.scss'],
animations: [fadeStrikeThroughAnimation],
})
export class TodoItemComponent implements OnInit {
@Input() todo!: Todo;
@Output() changeStatus: EventEmitter<Todo> = new EventEmitter<Todo>();
@Output() editTdo: EventEmitter<Todo> = new EventEmitter<Todo>();
@Output() removeTodo: EventEmitter<Todo> = new EventEmitter<Todo>();
isHovered = false;
isEditing = false;
ngOnInit(): void {}
changeTodoStatus() {
this.changeStatus.emit({ ...this.todo, isCompleted: !this.todo.isCompleted });
}
submitEdit(event: KeyboardEvent) {
const { keyCode } = event;
if (keyCode === 13) {
console.log('submitted');
this.editTdo.emit(this.todo);
this.isEditing = false;
}
}
handleDelete() {
this.removeTodo.emit(this.todo);
}
}
animations: [fadeStrikeThroughAnimation]: Đây là mảng các
animations được sử dụng trong component. Trong trường hợp này,
component sử dụng animation fadeStrikeThroughAnimation để thực hiện
hiệu ứng khi chuyển đổi trạng thái của công việc.
@Input() todo!: Todo;: Đây là một decorator @Input được sử
dụng để nhận dữ liệu đầu vào từ component cha. Trong trường hợp này,
component cha sẽ truyền một đối tượng Todo vào thuộc tính todo của
component TodoItemComponent.
@Output() changeStatus: EventEmitter<Todo> = new
EventEmitter<Todo>();: Đây là một decorator @Output và EventEmitter được sử dụng để phát
sự kiện khi trạng thái công việc thay đổi. EventEmitter<Todo>
sẽ phát ra một sự kiện và truyền một đối tượng Todo.
@Output() editTdo: EventEmitter<Todo> = new
EventEmitter<Todo>();: Đây là một decorator @Output và EventEmitter được sử dụng để phát
sự kiện khi công việc được chỉnh sửa. EventEmitter<Todo> sẽ
phát ra một sự kiện và truyền một đối tượng Todo.
@Output() removeTodo: EventEmitter<Todo> = new
EventEmitter<Todo>();: Đây là một decorator @Output và EventEmitter được sử dụng để phát
sự kiện khi công việc bị xóa. EventEmitter<Todo> sẽ phát ra
một sự kiện và truyền một đối tượng Todo.
isHovered = false;: Một biến boolean để theo dõi trạng thái
hover của công việc trong danh sách.
isEditing = false;: Một biến boolean để theo dõi trạng thái
chỉnh sửa nội dung của công việc.
ngOnInit(): void {}: Phương thức ngOnInit được triển khai từ
interface OnInit và được gọi khi component được khởi tạo. Trong
trường hợp này, phương thức này không có hành động gì.
changeTodoStatus(): Phương thức được gọi khi người dùng thực
hiện một hành động để chuyển đổi trạng thái của công việc. Phương
thức này gửi một sự kiện changeStatus thông qua EventEmitter và
truyền một đối tượng Todo mới có trạng thái isCompleted đảo ngược.
submitEdit(event: KeyboardEvent): Phương thức được gọi khi
người dùng nhấn phím Enter để hoàn thành chỉnh sửa nội dung công
việc. Phương thức này kiểm tra keyCode của sự kiện và nếu keyCode là
13 (mã phím Enter), nó gửi một sự kiện editTdo thông qua
EventEmitter và truyền đối tượng Todo hiện tại.
handleDelete(): Phương thức được gọi khi người dùng thực hiện
hành động xóa công việc. Phương thức này gửi một sự kiện removeTodo
thông qua EventEmitter và truyền đối tượng Todo hiện tại.
Todo-list Component
Todo-list (Component quản lý danh sách các task):
Component Todo-list là thành phần quản lý toàn bộ danh sách các
công việc.
Nó sẽ hiển thị các công việc trong danh sách và sử dụng Todo-item
component để hiển thị mỗi công việc.
Component này sẽ quản lý các hoạt động liên quan đến danh sách
công việc như thêm, xóa, chỉnh sửa, và cập nhật trạng thái công
việc.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/todo-list --skip-tests=true
Lệnh trên sẽ tạo một folder tên là todo-list bên trong
thư mục src/app/components.
Todo-list Component Template
Path: src/app/components/todo-list.component.html
<app-todo-item
*ngFor="let todo of todos$ | async"
[todo]="todo"
(changeStatus)="onChangeTodoStatus($event)"
(editTdo)="onEditTodo($event)"
(removeTodo)="onDeleteTodo($event)"
></app-todo-item>
Todo-list Component
Path: src/app/components/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Todo } from 'src/app/models/todo.model';
import { TodoService } from 'src/app/services/todo.service';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.scss'],
})
export class TodoListComponent implements OnInit {
todos$!: Observable<Todo[]>;
constructor(private todoService: TodoService) {}
ngOnInit(): void {
this.todos$ = this.todoService.todos$;
}
onChangeTodoStatus(todo: Todo) {
this.todoService.changeTodoStatus(todo.id, todo.isCompleted);
}
onEditTodo(todo: Todo) {
this.todoService.editTodo(todo.id, todo.content);
}
onDeleteTodo(todo: Todo) {
this.todoService.deleteTodo(todo.id);
}
}
todos$!: Observable<Todo[]>;: Một biến Observable để
theo dõi danh sách các công việc. Biến này sẽ được gán giá trị thông
qua subscription của todos$ từ TodoService.
constructor(private todoService: TodoService) {}: Phương thức
khởi tạo của component, trong đó TodoService được inject vào
component thông qua dependency injection. Điều này cho phép
component truy cập và sử dụng các phương thức của TodoService để
thao tác với danh sách công việc.
ngOnInit(): void {}: Phương thức ngOnInit được triển khai từ
interface OnInit và được gọi khi component được khởi tạo. Trong
phương thức này, biến todos$ sẽ được gán giá trị thông qua
subscription của todoService.todos$.
onChangeTodoStatus(todo: Todo): Phương thức được gọi khi
người dùng thay đổi trạng thái của một công việc. Phương thức này
gọi phương thức changeTodoStatus() của todoService để cập nhật trạng
thái của công việc.
onEditTodo(todo: Todo): Phương thức được gọi khi người dùng
chỉnh sửa một công việc. Phương thức này gọi phương thức editTodo()
của todoService để cập nhật nội dung của công việc.
onDeleteTodo(todo: Todo): Phương thức được gọi khi người dùng
xóa một công việc. Phương thức này gọi phương thức deleteTodo() của
todoService để xóa công việc khỏi danh sách.
Footer Component
Footer (Component quản lý footer):
Component Footer là thành phần quản lý phần cuối trang của ứng
dụng.
Chứa thông tin bản quyền, liên hệ, các liên kết quan trọng hoặc
bất kỳ nội dung nào liên quan đến phần cuối trang.
Tương tự như Header, Footer có thể được sử dụng để hiển thị một
footer cố định hoặc có thể thay đổi theo ngữ cảnh.
Mở terminal tại dự án và nhập lệnh sau:
ng g c components/footer --skip-tests=true
Lệnh trên sẽ tạo một folder tên là footer bên trong thư
mục src/app/components.
Footer Component Template
Path: src/app/components/footer.component.html
<div class="footer w-100">
<div
class="h-100 position-absolute d-flex justify-content-between align-items-center"
style="top: 0; bottom: 0; left: 0; right: 0"
>
<span class="items-count"> {{ length }} item{{ length > 1 ? "s" : "" }} </span>
<div>
<button
type="button"
class="filter-btn"
*ngFor="let btn of filterButtons"
[ngClass]="{ active: btn.isActive }"
(click)="filter(btn.type)"
>
{{ btn.label }}
</button>
</div>
<button
class="filter-btn clear-completed-btn"
[ngClass]="{ visible: hasComplete$ | async }"
(click)="clearCompleted()"
>
Clear Completed
</button>
</div>
</div>
Footer Component Style
Path: src/app/components/footer.component.scss
.footer {
position: relative;
height: 40px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
&:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
& .filter-btn {
padding: 5px;
border-radius: 0.25rem;
transition: 250ms all ease-in-out;
outline: none;
cursor: pointer;
margin-right: 5px;
background: white;
border: 1px solid white;
&:hover,
&.active {
border-color: burlywood;
}
}
& .clear-completed-btn {
// TODO: Change to hidden when implement completed todos
visibility: hidden;
&.visible {
visibility: visible;
}
}
& .items-count {
padding-left: 10px;
}
}
Footer Component
Path: src/app/components/footer.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subject, map, takeUntil } from 'rxjs';
import { Filter, FilterButton } from 'src/app/models/filtering.model';
import { TodoService } from 'src/app/services/todo.service';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent implements OnInit, OnDestroy {
filterButtons: FilterButton[] = [
{ type: Filter.All, label: 'All', isActive: true },
{ type: Filter.Active, label: 'Active', isActive: false },
{ type: Filter.Completed, label: 'Completed', isActive: false },
];
constructor(private todoService: TodoService) {}
hasComplete$!: Observable<boolean>;
destroy$: Subject<any> = new Subject<any>();
length = 0;
ngOnInit(): void {
this.hasComplete$ = this.todoService.todos$.pipe(
map((todos) => todos.some((todo) => todo.isCompleted)),
takeUntil(this.destroy$)
);
this.todoService.length$.pipe(takeUntil(this.destroy$)).subscribe((length) => {
this.length = length;
});
}
filter(type: Filter) {
this.filterButtons.forEach((btn) => {
btn.isActive = btn.type === type;
});
this.todoService.filterTodos(type);
}
clearCompleted() {
this.todoService.clearCompleted();
}
ngOnDestroy(): void {
this.destroy$.next('No Todo!');
this.destroy$.complete();
}
}
filterButtons: FilterButton[] = [...]: Một mảng các
FilterButton, đại diện cho các nút lọc công việc trong chân trang.
Mỗi FilterButton có các thuộc tính type (loại), label (nhãn) và
isActive (đang được chọn hay không).
constructor(private todoService: TodoService) {}: Phương thức
khởi tạo của component, trong đó TodoService được inject vào
component thông qua dependency injection. Điều này cho phép
component truy cập và sử dụng các phương thức của TodoService để
thao tác với danh sách công việc.
hasComplete$!: Observable<boolean>;: Một biến
Observable để theo dõi trạng thái hoàn thành của các công việc. Biến
này sẽ được gán giá trị thông qua pipe và các toán tử RxJS.
destroy$: Subject<any> = new Subject<any>();: Một
đối tượng Subject để quản lý việc huỷ đăng ký và giải phóng tài
nguyên khi component bị huỷ.
ngOnInit(): void {}: Phương thức ngOnInit được triển khai từ
interface OnInit và được gọi khi component được khởi tạo. Trong
phương thức này, biến hasComplete$ sẽ được gán giá trị thông qua
pipe và takeUntil để giải phóng tài nguyên khi component bị huỷ.
Biến length sẽ được cập nhật thông qua subscription của
todoService.length$.
filter(type: Filter): Phương thức được gọi khi người dùng
chọn một loại bộ lọc. Phương thức này sẽ cập nhật trạng thái
isActive của các nút bộ lọc và gọi phương thức filterTodos() của
todoService để áp dụng bộ lọc cho danh sách công việc.
clearCompleted(): Phương thức được gọi khi người dùng nhấn
nút để xóa các công việc đã hoàn thành. Phương thức này gọi phương
thức clearCompleted() của todoService để xóa các công việc đã hoàn
thành khỏi danh sách công việc.
ngOnDestroy(): void {}: Phương thức ngOnDestroy được triển
khai từ interface OnDestroy và được gọi khi component bị huỷ. Trong
phương thức này, đối tượng destroy$ gửi một giá trị để kết thúc
subscription và giải phóng tài nguyên.
Vậy là chúng ta đã tạo xong tất cả các component sẽ được sử dụng bên trong
ứng dụng todos nay. Bây giờ hãy tiến hành sử dụng nó.
App Component
Chúng ta sẽ sử dụng các component đã tạo trước đó để xây dựng giao
diện.
Hãy mở file app.component.html ở thư mục src/app và thêm
đoạn code bên dưới:
<div class="wrapper d-flex flex-column align-items-center w-100">
<h1 class="title">todos</h1>
<div class="row justify-content-center w-100">
<div class="todo-wrapper p-0 d-flex flex-column col-md-6 col-sm-8">
<app-header></app-header>
<app-todo-list></app-todo-list>
<app-footer *ngIf="hasTodos$ | async"></app-footer>
</div>
</div>
<small class="instruction text-center">
<em>Press Enter to add new todo. Press Arrow icon to toggle todos.</em>
<br /><br />
Copyright TodosMVC Project
</small>
</div>
<app-header></app-header>: Đây là một thẻ custom
element được sử dụng để gọi và hiển thị component app-header.
Component app-header sẽ được render ở đây.
<app-todo-list></app-todo-list>: Đây là một thẻ
custom element được sử dụng để gọi và hiển thị component
app-todo-list. Component app-todo-list sẽ được render ở đây.
<app-footer *ngIf="hasTodos$ |
async"></app-footer>: Đây là một thẻ custom element được sử dụng để gọi và hiển thị
component app-footer. Tuy nhiên, nó chỉ hiển thị khi điều kiện
hasTodos$ là true. *ngIf là một directive trong Angular được sử dụng
để điều khiển việc hiển thị của phần tử dựa trên một điều kiện.
hasTodos$ | async sử dụng pipe async để xử lý một Observable
hasTodos$ và hiển thị phần tử khi có dữ liệu.
Tiếp theo, hãy mở file app.component.scss ở thư
mục src/app và thêm đoạn code bên dưới:
.wrapper {
& .title {
color: rgba(175, 47, 47, 0.15);
font-size: 120px;
padding: 30px 0;
font-weight: 100;
}
& .instruction {
color: #bfbfbf;
padding: 30px 0;
margin-top: auto;
}
& .todo-wrapper {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
background: white;
& app-header {
height: 60px;
padding: 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.1);
}
}
}
Tiếp theo, hãy mở file app.component.ts ở thư
mục src/app và thêm đoạn code bên dưới:
import { Component, OnInit } from '@angular/core';
import { TodoService } from './services/todo.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(private todosService: TodoService) {}
hasTodos$!: Observable<boolean>;
ngOnInit(): void {
this.todosService.fetchFromLocalStorage();
this.hasTodos$ = this.todosService.length$.pipe(map((length) => length > 0));
this.todosService.todos$.subscribe(console.log)
}
}
constructor(private todosService: TodoService) {}: Phương
thức khởi tạo của component, trong đó TodoService được inject vào
component thông qua dependency injection. Điều này cho phép
component truy cập và sử dụng các phương thức của TodoService để
thao tác với danh sách công việc.
hasTodos$!: Observable<boolean>;: Một biến Observable
để theo dõi trạng thái có công việc hay không. Biến này sẽ được gán
giá trị thông qua pipe và toán tử map để chuyển đổi giá trị độ dài
danh sách công việc thành một giá trị boolean.
ngOnInit(): void {}: Phương thức ngOnInit được triển khai từ
interface OnInit và được gọi khi component được khởi tạo. Trong
phương thức này, phương thức fetchFromLocalStorage() của todoService
được gọi để tải danh sách công việc từ localStorage. Biến hasTodos$
sẽ được gán giá trị thông qua subscription của todoService.length$
và sử dụng toán tử map để kiểm tra xem danh sách công việc có độ dài
lớn hơn 0 hay không. Cuối cùng, phương thức subscribe() được gọi
trên todosService.todos$ để hiển thị danh sách công việc trong
console.
Cuối cùng, hãy mở file app.module.ts ở thư
mục src/app và thêm đoạn code bên dưới: Trong quá trình
code, tạo các components bằng lệnh CLI thì sẽ được tự động import
các đường dẫn khai báo component trong file này. Hãy đảm bảo rằng bạn đã
khai báo đầy đủ các thành phần cần thiết trong ứng dụng.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { TodoListComponent } from './components/todo-list/todo-list.component';
import { TodoItemComponent } from './components/todo-item/todo-item.component';
import { TodoInputComponent } from './components/todo-input/todo-input.component';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
TodoListComponent,
TodoItemComponent,
TodoInputComponent,
HeaderComponent,
FooterComponent,
],
imports: [BrowserModule, FormsModule, BrowserAnimationsModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Chạy dự án
Đến bước này là gần như bạn đã hoàn thành được ứng dụng todos này rồi. Hãy
tiến hành chạy dự án bằng các mở terminal tại thư mục dự án và chạy lệnh
sau:
ng serve
Lệnh trên sẽ build ứng dụng của chúng ta và sẽ chạy trên http://localhost:4200
Nếu đã có ứng dụng chạy trên PORT 4200 thì bạn có thể chạy dự án bằng lệnh
sau:
ng serve --port <number>
Bạn chỉ cần thay number bằng PORT mà bạn muốn chạy dự án.
Kết quả
Kết luận
Trong bài viết "Xây dựng ứng dụng Todos MVC đơn giản với Angular: Hướng
dẫn từ A đến Z", chúng ta đã tìm hiểu về cách xây dựng một ứng dụng quản
lý công việc (todos) sử dụng kiến trúc MVC (Model-View-Controller) và
Angular framework. Chúng ta đã đi qua quá trình xây dựng từ đầu đến
cuối, từ việc tạo các component cho header, footer, nhập liệu, danh sách
và các task, cho đến việc xử lý các hoạt động như thêm, chỉnh sửa, xóa
công việc.
Trong quá trình hướng dẫn, chúng ta đã tìm hiểu về các khái niệm cơ bản
trong Angular như component, template, class, và metadata. Chúng ta đã
thấy cách tách biệt logic và giao diện bằng cách sử dụng component và
cách chúng tương tác thông qua các Input và Output properties.
Bằng việc tạo một ứng dụng Todos MVC đơn giản, chúng ta đã nhận ra lợi
ích của việc sử dụng Angular trong việc phát triển ứng dụng web. Angular
giúp chúng ta xây dựng ứng dụng có cấu trúc rõ ràng, dễ bảo trì và mở
rộng. Nó cung cấp các tính năng như dependency injection, routing,
reactive programming và quản lý trạng thái ứng dụng thông qua RxJS.
Từ bài viết này, bạn đã có kiến thức và kỹ năng cần thiết để bắt đầu xây
dựng các ứng dụng Angular phức tạp hơn. Bạn đã tìm hiểu về cấu trúc và
quy trình làm việc với Angular và có thể áp dụng những kiến thức này vào
các dự án thực tế của mình.
Qua bài viết này, hy vọng bạn đã có một cái nhìn tổng quan về quy trình
và các bước cơ bản để xây dựng một ứng dụng Todos MVC với Angular. Hãy
tiếp tục nghiên cứu và phát triển kỹ năng của mình để tạo ra các ứng
dụng web tuyệt vời khác.
Chúc các bạn thành công !