hoc-lap-trinh-9

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

By hoadv