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 Ứng Dụng Todos MVC Đơn Giản Với Angular: Hướng Dẫn Từ A Đến Z




Angular Todo


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 MVCAngular. 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"













Tiếp theo, bạn mở file styles.scss ở thư mục gốc và thêm các dòng
dưới đây:





/* 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 
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.tstodo.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
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ả













    Ứng dụng Angular Todo

    Ứng dụng Angular Todo





    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 !

    Source Code



    Tài liệu tham khảo




    Hiju Blog

    I'm HiJu

    Post a Comment

    Previous Post Next Post