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  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.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.tstodo.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

    No comments:

    Powered by Blogger.