Tự học Angular - Simple Angular CRUD Example with Local Storage

Tự học Angular: Simple Angular CRUD with Local Storage

Trong bài viết này, mình sẽ học để xây dựng một ứng dụng CRUD đơn giản bằng Angular.
Vì ở mức độ cơ bản nên chúng ta sẽ sử dụng Local Storage để lưu trữ dữ liệu.
Bài này sẽ không học về Local Storage nhưng sẽ thấy được một hướng dẫn hoàn chỉnh về mặt chức năng của một ứng dụng Angular CRUD đơn giản sử dụng Local Storage trong bài viết này.



Các nội dung cần thực hiện:
  1. Tạo dự án.
  2. Cấu hình route cho dự án.
  3. Tạo các service sử dụng trong dự án.
  4. Tạo và tách module cho các components. Cấu hình route riêng cho mỗi module.
  5. Tạo guard quản lý login.
  6. Tạo model.
  7. Thực thi các chức năng CRUD.
  8. Quản lý user login, logout khỏi ứng dụng.
  9. Thêm chức năng import, export to CSV file.
Điều kiện kiến thức và môi trường:
  • Sử dụng thành thạo HTML, CSS và Javascript.
  • Hiểu biết cơ bản về TypeScript và framework Angular 15+.
  • Môi trường phát triển đã cài đặt Node 18+ cùng với npm.
  • Đã cài đặt Angular CLI, thư viện Tailwind CSS.
Dưới đây là danh sách các bài viết thực hiện step-by-step với dự án này :

 Còn bây giờ thì bắt đầu thôi nào!

Cấu trúc thư mục dự án:

Cấu trúc thư mục src/app:


Cấu trúc toàn bộ dự án mà chúng ta đang thực hiện:
Path: src/app/account/account.component.html
Component này nơi chứa layout chính và là nơi render nội dung của các component khác trong cùng chức năng.

<div class="w-screen h-screen overflow-hidden flex items-center justify-center">
    <router-outlet></router-outlet>
</div>

Account Module

Path: src/app/account/account.module.ts
Tất cả những cấu hình liên quan đến account đều ở trong file này. Chẳng hạn như khai báo component, import module, cấu hình route, ... 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { AccountComponent } from './account.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { ReactiveFormsModule } from '@angular/forms';

const routes: Routes = [
  {
    path: '',
    component: AccountComponent,
    children: [
      { path: 'login', component: LoginComponent},
      { path: 'register', component: RegisterComponent},
    ],
  },
];

@NgModule({
  declarations: [AccountComponent, LoginComponent, RegisterComponent],
  imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AccountModule {}

Login Component Template

Path: src/app/account/login/login.component.html
Chứa giao diện đăng nhập với các trường input nhập username, mật khẩu, nút nhấn đăng nhập và đường dẫn đến giao diện đăng ký. Sau khi người dùng nhấn đăng nhập thì component sẽ gọi hàm onSubmit() gửi dữ liệu đi để xử lý.
Component này có sử dụng validators để validate các trường input bên trong.Có nhiều các tạo form và trong bài này ReactiveFormsModule được sử dụng.

<div class="p-6 rounded-lg min-w-[400px] shadow-lg border border-gray-200 bg-gray-100">
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-8">
        <h1 class="text-3xl font-bold">Login</h1>
        <div class="flex flex-col gap-8">
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="username">Username</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="username" type="text" placeholder="Username" formControlName="username">
                <div class="text-base text-rose-500"
                    *ngIf="(loginForm.get('username')?.errors?.['required'] && loginForm.controls['username'].touched)">
                    <p>Username is required!!</p>
                </div>
                <div class="text-base text-rose-500"
                    *ngIf="(loginForm.get('username')?.errors?.['minlength'] && loginForm.controls['username'].dirty)">
                    <p>The username must be at least 6 characters!!</p>
                </div>
            </div>
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="password">Password</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="password" type="password" placeholder="Password" formControlName="password">
                <div class="text-base text-rose-500"
                    *ngIf="(loginForm.get('password')?.errors?.['required'] && loginForm.controls['password'].touched)">
                    <p>Password is required!!</p>
                </div>
                <div class="text-base text-rose-500"
                    *ngIf="(loginForm.get('password')?.errors?.['minlength'] && loginForm.controls['password'].dirty)">
                    <p>The password must be at least 6 characters!!</p>
                </div>
            </div>
        </div>
        <div class="flex gap-8 items-center">
            <button [disabled]="loginForm.invalid" [ngClass]="{ disabled: loginForm.invalid}"
                class="btn-submit px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white"
                type="submit">Submit</button>
            <a routerLink="/account/register" class="text-blue-500 hover:text-blue-600 underline underline-offset-2">Register</a>
        </div>
    </form>
    
</div>

Login Component Style

Path: src/app/account/login/login.component.scss
Chứa một đoạn nhỏ custom code làm đẹp giao diện.

.btn-submit.disabled {
    @apply hover:bg-blue-500 cursor-not-allowed
}

Login Component

Path: src/app/account/login/login.component.ts
Khởi tạo formModule và hàm onSubmit(). Khi người dùng đăng nhập vào ứng dụng thì hệ thống sẽ gọi đến các service liên quan để xử lý sự kiện login của người dùng.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UsersService } from 'src/app/services/users.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  loginForm!: FormGroup

  constructor(private usersService: UsersService ,private fb: FormBuilder){}

  ngOnInit(): void {
    this.loginForm = this.fb.group({
      username: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
      password: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
    })
  }

  onSubmit() {
    const value = this.loginForm.value
    this.usersService.login(value.username, value.password)
  }
}

Register Component Template

Path: src/app/account/register/register.component.html
Chứa giao diện đăng ký với các trường input nhập name, username, password, .... Sau khi người dùng nhấn đăng ký thì component sẽ gọi hàm onSubmit() gửi dữ liệu đi để xử lý.
Component này có sử dụng validators để validate các trường input bên trong.Có nhiều các tạo form và trong bài này ReactiveFormsModule được sử dụng.

<div class="p-6 rounded-lg min-w-[400px] shadow-lg border border-gray-200 bg-gray-100">
    <form [formGroup]="registerForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-8">
        <h1 class="text-3xl font-bold">Register</h1>
        <div class="flex flex-col gap-6">
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="firstName">First Name</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="firstName" type="text" placeholder="First Name" formControlName="firstName">
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('firstName')?.errors?.['required'] && registerForm.controls['firstName'].touched)">
                    <p>First Name is required!!</p>
                </div>
            </div>
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="lastName">Last Name</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="lastName" type="text" placeholder="Last Name" formControlName="lastName">
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('lastName')?.errors?.['required'] && registerForm.controls['lastName'].touched)">
                    <p>Last Name is required!!</p>
                </div>
            </div>
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="username">Username</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="username" type="text" placeholder="Username" formControlName="username">
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('username')?.errors?.['required'] && registerForm.controls['username'].touched)">
                    <p>Username is required!!</p>
                </div>
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('username')?.errors?.['minlength'] && registerForm.controls['username'].dirty)">
                    <p>The username must be at least 6 characters!!</p>
                </div>
            </div>
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="password">Password</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="password" type="password" placeholder="Password" formControlName="password">
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('password')?.errors?.['required'] && registerForm.controls['password'].touched)">
                    <p>Password is required!!</p>
                </div>
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('password')?.errors?.['minlength'] && registerForm.controls['password'].dirty)">
                    <p>The password must be at least 6 characters!!</p>
                </div>
            </div>
            <div class="flex flex-col gap-2">
                <label class="text-base font-medium" for="confirmPassword">Confirm Password</label>
                <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                    id="confirmPassword" type="password" placeholder="Confirm Password"
                    formControlName="confirmPassword">
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('confirmPassword')?.errors?.['required'] && registerForm.controls['confirmPassword'].touched)">
                    <p>Confirm password is required!!</p>
                </div>
                <div class="text-base text-rose-500"
                    *ngIf="(registerForm.get('confirmPassword')?.errors?.['minlength'] && registerForm.controls['confirmPassword'].dirty)">
                    <p>The password must be at least 6 characters!!</p>
                </div>
            </div>
        </div>
        <div class="flex gap-8 items-center">
            <button [disabled]="registerForm.invalid" [ngClass]="{ disabled: registerForm.invalid}"
                class="btn-submit px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white"
                type="submit">Submit</button>
            <a routerLink="/account/login" class="text-blue-500 hover:text-blue-600 underline underline-offset-2">Already have a account?</a>
        </div>
    </form>
</div>

Register Component Style

Path: src/app/account/register/register.component.scss
Chứa một đoạn nhỏ custom code làm đẹp giao diện.

.btn-submit.disabled {
    @apply hover:bg-blue-500 cursor-not-allowed
}

Register Component

Path: src/app/account/register/register.component.ts
Khởi tạo formModule và hàm onSubmit(). Khi người dùng đăng ký thì hệ thống sẽ gọi đến các service liên quan để xử lý sự kiện register của người dùng.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from 'src/app/models/user';
import { UsersService } from 'src/app/services/users.service';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss'],
})
export class RegisterComponent implements OnInit {
  registerForm!: FormGroup;

  constructor(private usersService: UsersService, private fb: FormBuilder) {}

  ngOnInit(): void {
    this.registerForm = this.fb.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      username: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
      password: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
      confirmPassword: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
    });
  }

  onSubmit() {
    const value = this.registerForm.value;

    if (value.password !== value.confirmPassword) {
      alert('The password is not the same!');
      return;
    }

    const newUser: User = {
      firstName: value.firstName,
      lastName: value.lastName,
      username: value.username,
      password: value.password,
    };

    if(this.usersService.addUser(newUser)) {
      this.registerForm.reset()
    }
  }
}

Login Guard

Path: src/app/guard/login.guard.ts
Login guard là một angular guard dùng để ngăn chặn người dùng truy cập vào ứng dụng mà chưa được xác minh và hạn chế một số quyền đối với việc truy cập đó. Bằng cách sử dụng canActivate quyết định xem rằng việc truy cập đó có được tiếp tục. Nếu không thỏa mãn điều kiện thì ứng dụng tự động redirect đến trang login.
Login guard được gắn vào những nơi cần thỏa mãn điều kiện truy cập. Cụ thể trong ứng dụng này login guard được gắn ở app-routing.module.tsusers.module.ts để ngăn chặn người dùng truy cập vào Home và Users mà không đăng nhập trước.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UsersComponent } from './users.component';
import { RouterModule, Routes } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms'

import { UserListComponent } from './user-list/user-list.component';
import { AddUserComponent } from './add-user/add-user.component';
import { EditUserComponent } from './edit-user/edit-user.component';
import { LoginGuard } from '../guards/login.guard';

const routes: Routes = [
  {
    path: '',
    component: UsersComponent,
    canActivate: [LoginGuard],
    children: [
      { path: '', component: UserListComponent },
      { path: 'add-user', component: AddUserComponent },
      { path: 'edit-user/:id', component: EditUserComponent },
    ],
  },
];

@NgModule({
  declarations: [UsersComponent, UserListComponent, AddUserComponent, EditUserComponent],
  imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class UsersModule {}

Home Component Template

Path: src/app/home/home.component.html
Đơn giản là trang chủ của ứng dụng. Ở bài viết này chỉ tập trung về chức năng vì vậy không có giao diện hoàn chỉnh cho trang chủ.

<div class="container mx-auto pt-8 mt-[60px]">
    <p>home works!</p>
</div>

User Model

Path: src/app/model/user.ts
Một interface đơn giản để định nghĩa các thuộc tính của user.

export interface User {
    id?: number,
    firstName: string,
    lastName: string,
    username: string,
    password: string
}

Local Storage Service

Path: src/app/services/local-storage.service.ts
Local Storage service chứa những phương thức thao tác liên quan đến Local Storage như get, set và remove item trên storage.

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class LocalStorageService {
  storage!: Storage;
  constructor() {
    this.storage = window.localStorage
  }

  setObject(key: string, value: any): void {
    if (!value) {
      return;
    }
    this.storage[key] = JSON.stringify(value);
  }

  getValue<T>(key: string): T {
    const obj = JSON.parse(this.storage[key] || null);
    return obj || null;
  }

  removeItem(key: string) {
    this.storage.removeItem(key)
  }
}

Users Service

Path: src/app/services/users.service.ts
Users service chứa những phương thức thao tác dữ liệu danh sach users được lưu trên Local Storage. Ngoài ra còn có phương thức quản lý hành động login, logout khi Account component gọi đến service này.
Service này cũng cung cấp các phương thức để lưu dữ liệu lên Local Storage hay load dữ liệu từ Local Storage và cập nhật dữ liệu. 
Bạn có thể xem bài viết 7 để hiểu hơn về cách hoạt động của service này nhé.

import { Injectable } from '@angular/core';
import { User } from '../models/user';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { LocalStorageService } from './local-storage.service';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root',
})
export class UsersService {
  private static readonly UsersStorageKey = 'users';

  private users: User[] = [];
  private usersSubject: BehaviorSubject<User[]> = new BehaviorSubject<User[]>([]);
  private currentUser!: User | null;
  private currentUserSubject: BehaviorSubject<User | null> = new BehaviorSubject<User | null>(null);
  private length!: number;
  private lengthSubject: BehaviorSubject<number> = new BehaviorSubject<number>(0);

  users$: Observable<User[]> = this.usersSubject.asObservable();
  currentUser$: Observable<User | null> = this.currentUserSubject.asObservable();
  length$: Observable<number> = this.lengthSubject.asObservable();

  constructor(private localStorageService: LocalStorageService, private router: Router) {}

  fetchDataFromLocalStorage() {
    this.users = this.localStorageService.getValue<User[]>(UsersService.UsersStorageKey) || [];
    this.currentUser = this.localStorageService.getValue<User | null>('currentUser') || null;
    this.updateData();
  }

  updateToLocalStorage() {
    this.localStorageService.setObject(UsersService.UsersStorageKey, this.users);
    this.localStorageService.setObject('currentUser', this.currentUser);
    this.updateData();
  }

  importDataFromFile(users: User[]) {
    this.users = users;
    this.updateToLocalStorage();
  }

  addUser(user: User): boolean {
    const isHasUser = this.users.find((u) => u.username === user.username);

    if (isHasUser) {
      alert('Username is exiting!');
      return false;
    }

    let id = new Date(Date.now()).getTime();
    let newUser = { id, ...user };
    this.users.unshift(newUser);
    this.updateToLocalStorage();
    alert('Add new user successful!!');
    return true;
  }

  deleteUser(id: number | undefined) {
    const idx = this.users.findIndex((u) => u.id === id);
    if (this.users[idx].username === this.currentUser?.username) {
      alert("You can't delete current user!!!");
      return;
    }
    this.users.splice(idx, 1);
    this.updateToLocalStorage();
  }

  updateUser(user: User) {
    const idx = this.users.findIndex((u) => u.id === user.id);
    this.users.splice(idx, 1, user);
    this.updateToLocalStorage();
  }

  login(username: string, password: string) {
    const user = this.users.find((u) => u.username === username);
    if (!user) {
      alert('Username is not exiting!!');
      return;
    }

    if (user.password !== password) {
      console.log(typeof password, typeof user.password);
      alert('Password is not correct!');
      return;
    }

    this.currentUser = user;
    this.updateToLocalStorage();
    this.router.navigate(['/home']);
  }

  logout() {
    this.localStorageService.removeItem('currentUser');
    this.currentUserSubject.next(null);
    this.router.navigate(['/account/login']);
  }

  getUserById(id: number): Observable<User | undefined> {
    return of(this.users.find((u) => u.id === id));
  }

  private updateData() {
    this.usersSubject.next(this.users);
    this.currentUserSubject.next(this.currentUser);
    this.lengthSubject.next(this.users.length);
  }
}

Users Component Template

Path: src/app/users/users.component.html
Component này nơi chứa layout chính và là nơi render nội dung của các component khác trong cùng chức năng.

<div class="container mx-auto pt-8 mt-[60px]">
    <router-outlet></router-outlet>
</div>

Users Module

Path: src/app/users/users.module.ts
Tất cả những cấu hình liên quan đến account đều ở trong file này. Chẳng hạn như khai báo component, import module, cấu hình route, ... 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UsersComponent } from './users.component';
import { RouterModule, Routes } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms'

import { UserListComponent } from './user-list/user-list.component';
import { AddUserComponent } from './add-user/add-user.component';
import { EditUserComponent } from './edit-user/edit-user.component';
import { LoginGuard } from '../guards/login.guard';

const routes: Routes = [
  {
    path: '',
    component: UsersComponent,
    canActivate: [LoginGuard],
    children: [
      { path: '', component: UserListComponent },
      { path: 'add-user', component: AddUserComponent },
      { path: 'edit-user/:id', component: EditUserComponent },
    ],
  },
];

@NgModule({
  declarations: [UsersComponent, UserListComponent, AddUserComponent, EditUserComponent],
  imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class UsersModule {}

Add User Component Template

Path: src/app/users/add-users/add-users.component.html
Chứa giao diện là một forrm với các trường input nhập name, username, password, .... Sau khi người dùng nhấn adđ thì component sẽ gọi hàm onSubmit() gửi dữ liệu đi để xử lý.
Component này có sử dụng validators để validate các trường input bên trong.Có nhiều các tạo form và trong bài này ReactiveFormsModule được sử dụng.

<div class="flex flex-col gap-8">
    <h1 class="text-4xl font-bold">Add new user</h1>
    <div>
        <form [formGroup]="addForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-8">
            <div class="flex gap-8">
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="firstName">First Name</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="firstName" type="text" placeholder="First Name" formControlName="firstName">
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('firstName')?.errors?.['required'] && addForm.controls['firstName'].touched)">
                        <p>First Name is required!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="lastName">Last Name</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="lastName" type="text" placeholder="Last Name" formControlName="lastName">
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('lastName')?.errors?.['required'] && addForm.controls['lastName'].touched)">
                        <p>Last Name is required!!</p>
                    </div>
                </div>
            </div>
            <div class="flex gap-8">
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="username">Username</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="username" type="text" placeholder="Username" formControlName="username">
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('username')?.errors?.['required'] && addForm.controls['username'].touched)">
                        <p>Username is required!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="password">Password</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="password" type="password" placeholder="Password" formControlName="password">
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('password')?.errors?.['required'] && addForm.controls['password'].touched)">
                        <p>Password is required!!</p>
                    </div>
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('password')?.errors?.['minlength'] && addForm.controls['password'].dirty)">
                        <p>The password must be at least 6 characters!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="confirmPassword">Confirm Password</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="confirmPassword" type="password" placeholder="Confirm Password"
                        formControlName="confirmPassword">
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('confirmPassword')?.errors?.['required'] && addForm.controls['confirmPassword'].touched)">
                        <p>Confirm password is required!!</p>
                    </div>
                    <div class="text-base text-rose-500"
                        *ngIf="(addForm.get('confirmPassword')?.errors?.['minlength'] && addForm.controls['confirmPassword'].dirty)">
                        <p>The password must be at least 6 characters!!</p>
                    </div>
                </div>
            </div>
            <div class="flex gap-8 items-center">
                <button [disabled]="addForm.invalid" [ngClass]="{ disabled: addForm.invalid}"
                    class="btn-submit px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white"
                    type="submit">Submit</button>
                <a routerLink="/users" class="text-blue-500 hover:text-blue-600 underline underline-offset-2">Cancel</a>
            </div>
        </form>
    </div>
</div>

Add User Component Style

Path: src/app/account/register/register.component.scss
Chứa một đoạn nhỏ custom code làm đẹp giao diện.

.btn-submit.disabled {
    @apply hover:bg-blue-500 cursor-not-allowed
}

Add User Component

Path: src/app/users/add-user/add-user.component.ts
Khởi tạo formModule và hàm onSubmit(). Khi người dùng đăng ký thì hệ thống sẽ gọi đến các service liên quan để xử lý sự kiện thêm mới người dùng.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from 'src/app/models/user';
import { UsersService } from 'src/app/services/users.service';

@Component({
  selector: 'app-add-user',
  templateUrl: './add-user.component.html',
  styleUrls: ['./add-user.component.scss'],
})
export class AddUserComponent implements OnInit {
  addForm!: FormGroup;

  constructor(private fb: FormBuilder, private usersService: UsersService) {}

  ngOnInit(): void {
    this.addForm = this.fb.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      username: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
      password: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
      confirmPassword: [
        '',
        Validators.compose([Validators.required, Validators.minLength(6)]),
      ],
    });
  }

  onSubmit() {
    const value = this.addForm.value;

    if (value.password !== value.confirmPassword) {
      alert('The password is not the same!');
      return;
    }

    const newUser: User = {
      firstName: value.firstName,
      lastName: value.lastName,
      username: value.username,
      password: value.password,
    };

    if(this.usersService.addUser(newUser)) {
      this.addForm.reset()
    }
  }
}

Edit User Component Template

Path: src/app/users/edit-users/edit-users.component.html
Chứa giao diện là một forrm với các trường input nhập name, username, password, .... Sau khi người dùng nhấn edit thì component sẽ gọi hàm onSubmit() gửi dữ liệu đi để xử lý.
Component này có sử dụng validators để validate các trường input bên trong.Có nhiều các tạo form và trong bài này ReactiveFormsModule được sử dụng.

<div class="flex flex-col gap-8">
    <h1 class="text-4xl font-bold">Edit user</h1>
    <div>
        <form [formGroup]="editForm" (ngSubmit)="onSubmit()" class="flex flex-col gap-8">
            <div class="flex gap-8">
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="firstName">First Name</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="firstName" type="text" placeholder="First Name" formControlName="firstName">
                    <div class="text-base text-rose-500"
                        *ngIf="(editForm.get('firstName')?.errors?.['required'] && editForm.controls['firstName'].touched)">
                        <p>First Name is required!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="lastName">Last Name</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="lastName" type="text" placeholder="Last Name" formControlName="lastName">
                    <div class="text-base text-rose-500"
                        *ngIf="(editForm.get('lastName')?.errors?.['required'] && editForm.controls['lastName'].touched)">
                        <p>Last Name is required!!</p>
                    </div>
                </div>
            </div>
            <div class="flex gap-8">
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="username">Username</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="username" type="text" placeholder="Username" formControlName="username">
                    <div class="text-base text-rose-500"
                        *ngIf="(editForm.get('username')?.errors?.['required'] && editForm.controls['username'].touched)">
                        <p>Username is required!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="password">Password</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="password" type="password" placeholder="Password" formControlName="password">
                    <div class="text-base text-rose-500"
                        *ngIf="(editForm.get('password')?.errors?.['minlength'] && editForm.controls['password'].dirty)">
                        <p>The password must be at least 6 characters!!</p>
                    </div>
                </div>
                <div class="flex flex-col gap-2">
                    <label class="text-base font-medium" for="confirmPassword">Confirm Password</label>
                    <input class="outline-none border border-slate-300 rounded-md px-4 py-2 focus:border-x-slate-500"
                        id="confirmPassword" type="password" placeholder="Confirm Password"
                        formControlName="confirmPassword">
                    <div class="text-base text-rose-500"
                        *ngIf="(editForm.get('confirmPassword')?.errors?.['minlength'] && editForm.controls['confirmPassword'].dirty)">
                        <p>The password must be at least 6 characters!!</p>
                    </div>
                </div>
            </div>
            <div class="flex gap-8 items-center">
                <button [disabled]="editForm.invalid" [ngClass]="{ disabled: editForm.invalid}"
                    class="btn-submit px-4 py-2 rounded-md bg-blue-500 hover:bg-blue-600 text-white"
                    type="submit">Submit</button>
                <a routerLink="/users" class="text-blue-500 hover:text-blue-600 underline underline-offset-2">Cancel</a>
            </div>
        </form>
    </div>
</div>

Edit User Component

Path: src/app/users/edit-user/edit-user.component.ts
Khởi tạo formModule và hàm onSubmit(). Khi người dùng edit xong thì hệ thống sẽ gọi đến các service liên quan để xử lý sự kiện edit thong tin user.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { User } from 'src/app/models/user';
import { UsersService } from 'src/app/services/users.service';

@Component({
  selector: 'app-edit-user',
  templateUrl: './edit-user.component.html',
  styleUrls: ['./edit-user.component.scss'],
})
export class EditUserComponent implements OnInit {
  editForm!: FormGroup;
  user!: User | undefined;

  constructor(
    private usersService: UsersService,
    private activatedRoute: ActivatedRoute,
    private fb: FormBuilder
  ) {}

  ngOnInit(): void {
    const id = parseInt(this.activatedRoute.snapshot.params?.['id']);
    this.usersService.getUserById(id).subscribe((user) => {
      this.user = user;
      this.editForm = this.fb.group({
        firstName: [user?.firstName, Validators.required],
        lastName: [user?.lastName, Validators.required],
        username: [
          user?.username,
          Validators.compose([Validators.required, Validators.minLength(6)]),
        ],
        password: ['', Validators.compose([Validators.minLength(6)])],
        confirmPassword: ['', Validators.compose([Validators.minLength(6)])],
      });
    });
  }

  onSubmit() {
    const isUpdate = confirm('Do you want to update this user??');
    if (isUpdate) {
      const value = this.editForm.value;

      if (value.password === value.confirmPassword) {
        const newUser: User = {
          id: this.user?.id,
          firstName: value.firstName,
          lastName: value.lastName,
          username: value.username,
          password:
            value.password === '' ? this.user?.password : value.password,
        };
        this.usersService.updateUser(newUser);
        alert('Update user successful!!');
      } else {
        alert('The password is not the same!');
      }
    }
  }
}

User List Component Template

Path: src/app/users/user-list/user-list.component.html
Component này nơi render của danh sách user. Mục đích là nhận dữ liệu từ service và render. Bao gồm các nút chức năng như thêm, import, export to csv, bảng danh sách user, ... 

<div class="flex flex-col gap-8">
    <h1 class="text-4xl font-bold">Users</h1>
    <div class="flex justify-between">
        <a routerLink="add-user"><button class="btn-add">Add new user</button></a>
        <div class="flex gap-4">
            <input class="hidden" type="file" name="importCSV" id="importCSV" accept=".csv"
                (change)="importCSV($event)" />
            <label
                class="btn-import px-2 py-1 cursor-pointer rounded-md text-white transition-all"
                for="importCSV">Import</label>
            <button (click)="exportToCSV()" class="btn-export">Export</button>
        </div>
    </div>
    <table style="width: 100%;">
        <thead>
            <tr>
                <th>ID</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Username</th>
                <th style="width: 20%;">Actions</th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let user of users">
                <td>{{user.id}}</td>
                <td>{{user.firstName}}</td>
                <td>{{user.lastName}}</td>
                <td>{{user.username}}</td>
                <td class="flex gap-4 justify-center">
                    <a [routerLink]="'edit-user/' + user.id"><button class="btn-edit">Edit</button></a>
                    <button (click)="deleteUser(user.id)" class="btn-delete">Delete</button>
                </td>
            </tr>
            <tr>
                <td class="font-bold" colspan="6">
                    {{ length === 0 ? "No users" : length > 1 ? length + " users." : length + " user."}}
                </td>
            </tr>
        </tbody>
    </table>
</div>

User List Component Style

Path: src/app/account/user-list/user-list.component.scss
Chứa một đoạn nhỏ custom code làm đẹp giao diện.

th, td {
    @apply text-center py-2 border border-gray-200
}

button {
    @apply px-2 py-1 rounded-md text-white transition-all
}

.btn-edit {
    @apply bg-blue-500 hover:bg-blue-600
}

.btn-delete {
    @apply bg-rose-500 hover:bg-rose-600
}

.btn-add, .btn-import, .btn-export {
    @apply bg-emerald-800 hover:bg-emerald-900 px-4 py-2
}

User List Component

Path: src/app/users/user-list/user-list.component.ts
Gọi service và lấy dữ liệu, chứa các phương thức như delete user, import, export to csv file, ... 

import { Component, OnInit } from '@angular/core';
import { read, utils, writeFile } from 'xlsx';

import { User } from 'src/app/models/user';
import { UsersService } from 'src/app/services/users.service';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
  users: User[] = []
  length: number = 0

  constructor(private usersService: UsersService){}

  ngOnInit(): void {
    this.usersService.users$.subscribe(users => this.users = users)
    this.usersService.length$.subscribe(len => this.length = len)
  }

  deleteUser(id: number | undefined) {
    const isDelete = confirm("Do you want to delete this user??")
    if(isDelete) {
      this.usersService.deleteUser(id);
    }
  }

  importCSV(event: any) {
    const file = event.target.files[0];
    const reader: FileReader = new FileReader();
    reader.onload = (e: any) => {
      const data = e.target.result;
      const workbook = read(data, { type: 'array' });

      // Get the first worksheet
      const worksheet = workbook.Sheets[workbook.SheetNames[0]];

      // Convert the worksheet to JSON
      const jsonData = utils.sheet_to_json(worksheet, { header: 1 });
      console.log(jsonData)
      // Process the JSON data
      const list: User[] = [];
      jsonData.forEach((user: any, index) => {
        if (index !== 0) {
          let temp: User = {
            id: parseInt(user[0]),
            firstName: user[1].toString(),
            lastName: user[2].toString(),
            username: user[3].toString(),
            password: user[4].toString(),
          }
          list.push(temp);
        }
      });
      this.usersService.importDataFromFile(list);
    };
    reader.readAsArrayBuffer(file);
  }

  exportToCSV() {
    if(confirm('Do you want export to CSV?')) {
      if (this.users.length === 0) {
        return alert('Empty list!');
      } else {
        // convert ID to string
        const newUsers = this.users.map((user) => {
          return {
            ...user,
            id: user.id?.toString(),
          };
        });
        const heading = [['ID', 'First Name', 'Last Name', 'Username', 'Password']];
        const wb = utils.book_new();
        const ws = utils.json_to_sheet([]);
        utils.sheet_add_aoa(ws, heading);
        utils.sheet_add_json(ws, newUsers, {
          origin: 'A2',
          skipHeader: true,
        });
        utils.book_append_sheet(wb, ws, 'Users');
        writeFile(wb, 'data.csv');
      }
    }
  }
}

App Routing Module

Path: src/app/app-routing.module.ts
Routing chính của một ứng dụng Angular được cấu hình tại file này. Những đường dẫn được khai báo là một mảng các đường dẫn và được bảo vệ bởi guard.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginGuard } from './guards/login.guard';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent, canActivate: [LoginGuard] },
  {
    path: 'users',
    loadChildren: () =>
      import('./users/users.module').then((m) => m.UsersModule),
  },
  {
    path: 'account',
    loadChildren: () =>
      import('./account/account.module').then((m) => m.AccountModule),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

App Component Template

Path: src/app/app.component.html
App component là component gốc của ứng dụng, nó chưa thanh điều hướng chính và hiển thị linh hoạt giao diện đối với người dùng khi login hay chưa login. Hiển thị nội dung của từng tuyến đường mà app định tuyến đến.

<ng-container *ngIf="currentUser$ | async">
    <nav class="fixed top-0 w-screen h-[60px] bg-slate-800 text-white">
        <div class="flex items-center container mx-auto h-full justify-between">
            <div class="flex h-full items-center gap-8">
                <a routerLink="/home">Home</a>
                <a routerLink="/users">Users</a>
            </div>
            <div>
                <button (click)="logout()">Logout</button>
            </div>
        </div>
    </nav>
</ng-container>
<div class="w-screen h-screen overflow-hidden flex flex-col">
    <div class="flex-1">
        <router-outlet></router-outlet>
    </div>
    <ng-container *ngIf="currentUser$ | async">
        <footer class="py-8">
            <h3 class="text-3xl font-medium text-center">Angular CRUD</h3>
        </footer>
    </ng-container>
</div>

App Component

Path: src/app/app.component.ts
Khi khởi tạo ứng dụng, app component được render và lấy dữ liệu từ Local Storage về ứng dụng. 

import { Component, OnInit } from '@angular/core';
import { UsersService } from './services/users.service';
import { Observable } from 'rxjs';
import { User } from './models/user';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  title = 'angular_crud';
  currentUser$!: Observable<User | null>;
  constructor(
    private usersService: UsersService,
  ) {}

  ngOnInit(): void {
    this.usersService.fetchDataFromLocalStorage();
    this.currentUser$ = this.usersService.currentUser$;
  }

  logout() {
    this.usersService.logout()
  }
}

App Module

Path: src/app/app.module.ts
Khi khởi tạo ứng dụng, app component được render và lấy dữ liệu từ Local Storage về ứng dụng. 

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';


@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

App Style

Path: src/styles.scss
File SCSS import thư viện Tailwind CSS sử dụng trong ứng dụng.

/* You can add global styles to this file, and also import other style files */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

Kết luận

Nếu bạn có điều gì thắc mắc thì đừng ngần ngại mà comment phía bên dưới bài post để chúng ta cùng nhau thảo luận nha.
Hãy chia sẽ nếu thấy bài viết bỏ ích nha.
Chúc các bạn thành công! ✊✊

No comments:

Powered by Blogger.