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.ts
users.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! ✊✊

Hiju Blog

I'm HiJu

Post a Comment

Previous Post Next Post