Code base đi thi : https://github.com/vanhoa690/web208-angular-base
Link github: https://github.com/vanhoa690/angular-su24
Video CRUD Youtube: https://www.youtube.com/watch?v=KruN4sEbML0
Video Login/Register Youtube: https://www.youtube.com/watch?v=tu21Q5-YPrM
Deploy https://angular-su24.vercel.app/admin/products/list
ng g s services/auth
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { RegisterForm } from '../../types/Auth';
@Injectable({
providedIn: 'root',
})
export class AuthService {
apiUrl = 'http://localhost:3000';
http = inject(HttpClient);
register(data: RegisterForm) {
return this.http.post(`${this.apiUrl}/register`, data);
}
}
pages/register.ts
import { Component, inject } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './register.component.html',
styleUrl: './register.component.css',
})
export class RegisterComponent {
authService = inject(AuthService);
registerForm: FormGroup = new FormGroup({
email: new FormControl('', [Validators.email, Validators.required]),
password: new FormControl('', [
Validators.minLength(6),
Validators.required,
]),
});
handleSubmit() {
console.log(this.registerForm.value);
this.authService.register(this.registerForm.value).subscribe({
next: () => {
console.log('thong bao + chuyen trang');
},
error: (error) => {
// show error
console.error(error.message);
},
});
}
}
pages/register.html
<form [formGroup]="registerForm" (ngSubmit)="handleSubmit()">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input
type="email"
class="form-control"
id="exampleInputEmail1"
aria-describedby="emailHelp"
placeholder="Enter email"
formControlName="email"
/>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input
type="password"
class="form-control"
id="exampleInputPassword1"
placeholder="Password"
formControlName="password"
/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
ng g c pages/admin/products/add
ng g c pages/admin/products/update
pages/admin/products/update.component.ts
import { Component, inject } from '@angular/core';
import { ProductService } from '../../../../services/product.service';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-update',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './update.component.html',
styleUrl: './update.component.css',
})
export class ProductUpdateComponent {
productService = inject(ProductService);
route = inject(ActivatedRoute);
productId!: string | undefined;
addProductForm: FormGroup = new FormGroup({
// FormControl : gia tri ban dau, Validator
title: new FormControl('', [Validators.required, Validators.minLength(6)]),
image: new FormControl('', []),
category: new FormControl('', [Validators.required]),
});
ngOnInit() {
this.route.params.subscribe((param) => {
this.productId = param['id'];
this.productService.getProductDetail(param['id']).subscribe({
next: (data) => {
// update data vao addProductForm
this.addProductForm.patchValue(data);
},
error: (error) => {
// show thong bao error
console.error(error);
},
});
});
}
handleSubmit() {
console.log(this.addProductForm);
if (!this.productId) return;
this.productService
.updateProduct(this.productId, this.addProductForm.value)
.subscribe({
next: () => {
console.log('thong bao + chuyen trang');
},
error: (error) => {
// show error
console.error(error.message);
},
});
}
}
services/product.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import {
AddProductForm,
CreateProductForm,
Product,
} from '../../types/Product';
@Injectable({
providedIn: 'root',
})
export class ProductService {
apiUrl = 'http://localhost:3000/products';
http = inject(HttpClient);
getAllProducts() {
return this.http.get<Product[]>(this.apiUrl);
}
addProduct(data: AddProductForm) {
return this.http.post(this.apiUrl, data);
}
updateProduct(id: string, data: AddProductForm) {
return this.http.put(`${this.apiUrl}/${id}`, data);
}
deleteProduct(id: string) {
return this.http.delete(`${this.apiUrl}/${id}`);
}
getProductDetail(id: string) {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
}
pages/admin/products/add.component.ts
import { Component, inject } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { ProductService } from '../../../../services/product.service';
@Component({
selector: 'app-add',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './add.component.html',
styleUrl: './add.component.css',
})
export class ProductAddComponent {
productService = inject(ProductService);
addProductForm: FormGroup = new FormGroup({
// FormControl : gia tri ban dau, Validator
title: new FormControl('', [Validators.required, Validators.minLength(6)]),
image: new FormControl('', []),
category: new FormControl('', [Validators.required]),
});
handleSubmit() {
console.log(this.addProductForm);
this.productService.addProduct(this.addProductForm.value).subscribe({
next: () => {
console.log('thong bao + chuyen trang');
},
error: (error) => {
// show error
console.error(error.message);
},
});
}
}
pages/admin/products/add.component.html
<div class="container">
<h2 class="text-center">Product Add</h2>
<form [formGroup]="addProductForm" (ngSubmit)="handleSubmit()">
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
class="form-control"
id="title"
placeholder="Title"
formControlName="title"
/>
@if(addProductForm.controls['title'].errors?.['required'] &&
addProductForm.controls['title'].touched ) {
<small class="text-danger text-small">Title is Required</small>
} @if(addProductForm.controls['title'].errors?.['minlength'] &&
addProductForm.controls['title'].touched ) {
<small class="text-danger text-small">Min Length is min 6 ky tu</small>
}
</div>
<div class="form-group">
<label for="image">Image</label>
<input
type="text"
class="form-control"
id="Image"
placeholder="Image"
formControlName="image"
/>
</div>
<div class="form-group">
<label for="category">Category</label>
<select class="form-control" id="category" formControlName="category">
<option value="">Select Category</option>
<option value="1">Laptop</option>
<option value="2">PC</option>
</select>
@if(addProductForm.controls['category'].errors?.['required'] &&
addProductForm.controls['category'].touched ) {
<small class="text-danger text-small">Category is Required</small>
}
</div>
<button
type="submit"
class="btn btn-primary"
[disabled]="addProductForm.invalid"
>
Submit
</button>
</form>
</div>
Cài đặt Angular CLI
npm install -g @angular/cli
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Tạo project
ng new projectName
cd projectName
Run project
ng serve --open
"scripts": {
"ng": "ng",
"start": "ng serve",
"dev": "concurrently \"npx json-server db.json\" \"ng serve --o\"",
...
},
Cài đặt tailwind css: https://tailwindcss.com/docs/guides/angular
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
Update file taildwind.config.js:
content: [ "./src/**/*.{html,ts}", ],
Update file style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Cách 2: Sử dụng bootstrap CDN và update file index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>MyAppSu</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
</head>
<body>
<app-root> </app-root>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
</body>
</html>
Create Interface Product: types/Product.ts
export interface Product {
id: number,
title: string,
price: number,
description: string,
category: string,
image: string,
rating: {
rate: number,
count: number
},
}
Config Route Angular
app.compnent.html
<router-outlet></router-outlet>
- Layout Admin
ng g c layouts/admin-layout
admin-layout.component.html
<div>
<app-sidebar></app-sidebar>
<router-outlet></router-outlet>
</div>
2.Page admin product list
ng g c pages/admin/products/list
(Đổi tên component cho đỡ nhầm lẫn: ListComponent -> ProducuctListComponent)
Config app.routes.ts
import { Routes } from '@angular/router';
import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component';
import { ProductListComponent } from './pages/admin/products/list/list.component';
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent,
children: [
{
path: 'products/list',
component: ProductListComponent,
},
],
},
];
Call API
Chạy server: json-server (hoặc Nodejs + Mongodb)
npx json-server db.json
JSON server: File db.json : https://fakestoreapi.com/products
db.json
{
"products": [
{
"id": "5",
"title": "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet",
"price": 695,
"description": "From our Legends Collection, the Naga was inspired by the mythical water dragon that protects the ocean's pearl. Wear facing inward to be bestowed with love and abundance, or outward for protection.",
"category": "jewelery",
"image": "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg",
"rating": {
"rate": 4.6,
"count": 400
}
},
]
}
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient()],
};
ng g s services/product
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from '../../types/Product';
@Injectable({
providedIn: 'root',
})
export class ProductService {
http = inject(HttpClient);
apiUrl = 'http://localhost:3000/products';
constructor() {}
getAllProducts() {
return this.http.get<Product[]>(this.apiUrl);
}
getProductDetail(id: number) {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
deleteProduct(id: number) {
return this.http.delete(`${this.apiUrl}/${id}`);
}
}
import { Component, inject } from '@angular/core';
import { Product } from '../../../../../types/Product';
import { RouterLink } from '@angular/router';
import { ProductService } from '../../../../services/product.service';
@Component({
selector: 'app-list',
standalone: true,
imports: [RouterLink],
templateUrl: './list.component.html',
styleUrl: './list.component.css',
})
export class ProductListComponent {
products: Product[] = [];
productService = inject(ProductService);
ngOnInit() {
this.productService.getAllProducts().subscribe({
next: (products) => {
this.products = products;
},
error: (error) => {
// show error
console.error(error.message);
},
});
}
handleDeleteProduct(id: number) {
if (window.confirm('Xoa that nhe')) {
this.productService.deleteProduct(id).subscribe({
next: () => {
this.products = this.products.filter((product) => product.id !== id);
},
error: (error) => {
console.error(error.message);
},
});
}
}
}
pages/admin/products/list
<p>Product List</p>
<!-- Copy Code Lab 2: Table Product List -->
<div class="container">
<div class="card">
<div class="card-header">Product Info</div>
<table>
<tr>
<th>Title</th>
<th>Image</th>
<th>Actions</th>
</tr>
@for (product of products; track product.id) {
<tr>
<td>{{ product.title }}</td>
<td><img [src]="product.image" width="100px" /></td>
<td>
<a class="btn btn-warning" [routerLink]="['/products', product.id]"
>View</a
>
<a
class="btn btn-info"
[routerLink]="['/admin/products/edit', product.id]"
>Edit</a
>
<button
class="btn btn-danger"
(click)="handleDeleteProduct(product.id)"
>
Delete
</button>
</td>
</tr>
}
</table>
</div>
</div>
Tạo Client Layout và product Detail cho khách hàng xem được
ng g c layouts/client-layout
ng g c pages/products/detail
Đổi tên DetailComponent thành ProductDetailComponent (trong detail.component.ts để tránh nhầm lẫn)
app.routes.ts : Thêm route products/:id vào routes[]
import { Routes } from '@angular/router';
import { AdminLayoutComponent } from './layouts/admin-layout/admin-layout.component';
import { ProductListComponent } from './pages/admin/products/list/list.component';
import { ClientLayoutComponent } from './layouts/client-layout/client-layout.component';
import { ProductDetailComponent } from './pages/products/detail/detail.component';
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent,
children: [
{
path: 'products/list',
component: ProductListComponent,
},
],
},
{
path: '',
component: ClientLayoutComponent,
children: [
{
path: 'products/:id',
component: ProductDetailComponent,
},
],
},
];
detail.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductService } from '../../../services/product.service';
import { Product } from '../../../../types/Product';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-detail',
standalone: true,
imports: [NgIf],
templateUrl: './detail.component.html',
styleUrl: './detail.component.css',
})
export class ProductDetailComponent {
route = inject(ActivatedRoute);
productService = inject(ProductService);
product!: Product | undefined;
ngOnInit() {
this.route.params.subscribe((param) => {
this.productService.getProductDetail(param['id']).subscribe({
next: (data) => {
this.product = data;
},
error: (error) => {
console.error(error);
},
});
});
}
}
detail.component.html
<h2>Product Detail</h2>
@if (product) {
<img [src]="product.image" width="100px" />
}
<h1 *ngIf="product">{{ product.title }}</h1>