feat: implement Angular Day 9-14 component patterns - Day 9: Custom two-way...

feat: implement Angular Day 9-14 component patterns - Day 9: Custom two-way binding with ToggleComponent (@Input/@Output pattern)
- Day 13: Content projection with ng-content multiple selectors and ngProjectAs
- Day 14: Template rendering with ng-template, ngTemplateOutlet, ng-container
- Add interactive shopping cart demo with service sharing
- Implement responsive UI with Tailwind CSS
- Add comprehensive documentation and interactive examples
- Create reusable components: ProgressBar, AuthorList, Toggle, Card, Modal
- Setup proper TypeScript typing and RxJS patterns
parent f87a738f
This diff is collapsed.
......@@ -11,7 +11,7 @@
"test": "ng test"
},
"engines": {
"node": "20.15.1"
"node": ">=20.15.1"
},
"private": true,
"dependencies": {
......@@ -25,6 +25,7 @@
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/platform-server": "^18.2.8",
"@angular/router": "^18.0.0",
"@ctrl/tinycolor": "^4.1.0",
"@ngrx/signals": "^18.1.0",
"express": "^4.21.1",
"ng-zorro-antd": "^18.1.1",
......
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
This diff is collapsed.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NzCarouselModule } from 'ng-zorro-antd/carousel';
import { ProgressBarComponent } from '../../shared/ui/progress-bar/progress-bar.component';
import { AuthorListComponent } from '../../shared/ui/author-list/author-list.component';
import { ToggleComponent } from '../../shared/ui/toggle/toggle.component';
import { CardComponent } from '../../shared/ui/card/card.component';
import { ModalComponent } from '../../shared/ui/modal/modal.component';
import { DIComparisonComponent } from '../../shared/ui/di-comparison/di-comparison.component';
import { ProductListComponent } from '../../shared/ui/product-list/product-list.component';
import { CartViewComponent } from '../../shared/ui/cart-view/cart-view.component';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'meu-home',
standalone: true,
imports: [NzCarouselModule],
imports: [
CommonModule,
NzCarouselModule,
ProgressBarComponent,
AuthorListComponent,
ToggleComponent,
CardComponent,
ModalComponent,
DIComparisonComponent,
ProductListComponent,
CartViewComponent,
FormsModule
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {}
export class HomeComponent {
uploadProgress = 65;
videoProgress = 80;
downloadProgress = 30;
// Two-way binding properties for demo
toggleState1 = false;
toggleState2 = true;
toggleState3 = false;
userName = 'Truc Thanh';
// Content Projection demo properties
isModalOpen = false;
surveyAnswers = {
question1: false,
question2: false,
question3: false
};
// Day 14: ng-template demo properties
userAge18 = false;
itemCounter = 3;
Math = Math; // Expose Math object to template
// Action log for button demo
actionLog: Array<{time: string, message: string}> = [];
// Custom card data
customCardData = {
title: 'Advanced ng-template Demo',
description: 'Đây là ví dụ về cách sử dụng ng-template để tạo ra các component có thể customize cao.',
tags: ['angular', 'ng-template', 'reusable', 'flexible']
};
// Card actions
cardActions = [
{
label: 'Like',
icon: '👍',
className: 'tw-bg-blue-500 hover:tw-bg-blue-600 tw-text-white',
handler: () => this.addActionLog('👍 Card liked!')
},
{
label: 'Share',
icon: '📤',
className: 'tw-bg-green-500 hover:tw-bg-green-600 tw-text-white',
handler: () => this.addActionLog('📤 Card shared!')
},
{
label: 'Save',
icon: '💾',
className: 'tw-bg-purple-500 hover:tw-bg-purple-600 tw-text-white',
handler: () => this.addActionLog('💾 Card saved!')
}
];
getCurrentTime(): string {
return new Date().toLocaleTimeString();
}
// Methods for Content Projection demo
openModal(): void {
this.isModalOpen = true;
}
closeModal(): void {
this.isModalOpen = false;
}
onSave(): void {
console.log('Survey answers:', this.surveyAnswers);
alert('Survey saved! Check console for details.');
this.closeModal();
}
resetSurvey(): void {
this.surveyAnswers = {
question1: false,
question2: false,
question3: false
};
}
// Button callback methods
saveDocument = () => {
this.addActionLog('💾 Document saved successfully!');
};
deleteFile = () => {
this.addActionLog('🗑️ File deleted permanently!');
};
completeTask = () => {
this.addActionLog('✅ Task marked as completed!');
};
cancelProcess = () => {
this.addActionLog('⚠️ Process cancelled by user!');
};
addActionLog(message: string): void {
const time = new Date().toLocaleTimeString();
this.actionLog.unshift({ time, message });
// Keep only last 10 actions
if (this.actionLog.length > 10) {
this.actionLog = this.actionLog.slice(0, 10);
}
}
clearActionLog(): void {
this.actionLog = [];
}
trackByIndex(index: number): number {
return index;
}
}
import { Author } from '../interface/author.interface';
export const authors: Author[] = [
{
id: 1,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
gender: 'Male',
ipAddress: '192.168.1.1'
},
{
id: 2,
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com',
gender: 'Female',
ipAddress: '192.168.1.2'
},
{
id: 3,
firstName: 'Michael',
lastName: 'Johnson',
email: 'michael.johnson@example.com',
gender: 'Male',
ipAddress: '192.168.1.3'
},
{
id: 4,
firstName: 'Emily',
lastName: 'Davis',
email: 'emily.davis@example.com',
gender: 'Female',
ipAddress: '192.168.1.4'
},
{
id: 5,
firstName: 'David',
lastName: 'Wilson',
email: 'david.wilson@example.com',
gender: 'Male',
ipAddress: '192.168.1.5'
}
];
import { ProductModel } from '../interface/product.interface';
export const MOCK_PRODUCTS: ProductModel[] = [
{
id: 1,
sku: 'PHONE-001',
name: 'iPhone 15 Pro',
price: 999,
description: 'Latest iPhone with advanced features',
imageUrl: 'https://picsum.photos/200/200?random=1'
},
{
id: 2,
sku: 'LAPTOP-001',
name: 'MacBook Pro M3',
price: 1999,
description: 'Powerful laptop for professionals',
imageUrl: 'https://picsum.photos/200/200?random=2'
},
{
id: 3,
sku: 'TABLET-001',
name: 'iPad Air',
price: 599,
description: 'Versatile tablet for work and play',
imageUrl: 'https://picsum.photos/200/200?random=3'
},
{
id: 4,
sku: 'WATCH-001',
name: 'Apple Watch Series 9',
price: 399,
description: 'Advanced smartwatch with health features',
imageUrl: 'https://picsum.photos/200/200?random=4'
}
];
export interface Author {
id: number;
firstName: string;
lastName: string;
email: string;
gender: string;
ipAddress: string;
}
export interface ProductModel {
id: number;
sku: string;
name: string;
price: number;
description?: string;
imageUrl?: string;
}
export interface CartItem {
product: ProductModel;
quantity: number;
}
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ProductModel, CartItem } from '../interface/product.interface';
/**
* CartExtService - Alternative implementation of CartService
* Demo việc override provider trong DI
*
* Khác biệt:
* - Có thêm discount calculation
* - Có thêm external API simulation
* - Different logging style
*/
@Injectable()
export class CartExtService {
private cartItemsSubject = new BehaviorSubject<CartItem[]>([]);
private cartCountSubject = new BehaviorSubject<number>(0);
private discountPercent = 10; // 10% discount
cartItems$: Observable<CartItem[]> = this.cartItemsSubject.asObservable();
cartCount$: Observable<number> = this.cartCountSubject.asObservable();
constructor() {
console.log('🌟 CartExtService (Extended) instance được tạo');
this.loadCartFromStorage();
}
addToCart(product: ProductModel, quantity: number = 1): void {
const currentItems = this.cartItemsSubject.value;
const existingItemIndex = currentItems.findIndex(item => item.product.id === product.id);
let updatedItems: CartItem[];
if (existingItemIndex >= 0) {
updatedItems = currentItems.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + quantity }
: item
);
} else {
updatedItems = [...currentItems, { product, quantity }];
}
this.updateCart(updatedItems);
console.log(`🚀 [EXT] Added ${product.name} to cart with external processing`);
// Simulate external API call
this.syncWithExternalAPI('ADD', product.id, quantity);
}
removeFromCart(productId: number): void {
const currentItems = this.cartItemsSubject.value;
const updatedItems = currentItems.filter(item => item.product.id !== productId);
this.updateCart(updatedItems);
console.log(`🔥 [EXT] Removed product ${productId} with external sync`);
this.syncWithExternalAPI('REMOVE', productId, 0);
}
updateQuantity(productId: number, quantity: number): void {
if (quantity <= 0) {
this.removeFromCart(productId);
return;
}
const currentItems = this.cartItemsSubject.value;
const updatedItems = currentItems.map(item =>
item.product.id === productId
? { ...item, quantity }
: item
);
this.updateCart(updatedItems);
console.log(`⚡ [EXT] Updated quantity for product ${productId} = ${quantity}`);
this.syncWithExternalAPI('UPDATE', productId, quantity);
}
/**
* Calculate total with discount (Extended feature)
*/
calculateTotal(): number {
const subtotal = this.cartItemsSubject.value.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
);
// Apply discount if total > 1000
if (subtotal > 1000) {
const discount = subtotal * (this.discountPercent / 100);
return subtotal - discount;
}
return subtotal;
}
/**
* Get discount amount (Extended feature)
*/
getDiscountAmount(): number {
const subtotal = this.getSubtotal();
if (subtotal > 1000) {
return subtotal * (this.discountPercent / 100);
}
return 0;
}
/**
* Get subtotal before discount
*/
getSubtotal(): number {
return this.cartItemsSubject.value.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
);
}
getTotalItems(): number {
return this.cartItemsSubject.value.reduce(
(total, item) => total + item.quantity,
0
);
}
clearCart(): void {
this.updateCart([]);
console.log('💫 [EXT] Cleared cart with external notification');
this.syncWithExternalAPI('CLEAR', 0, 0);
}
getCurrentItems(): CartItem[] {
return this.cartItemsSubject.value;
}
/**
* Simulate external API sync (Extended feature)
*/
private syncWithExternalAPI(action: string, productId: number, quantity: number): void {
// Simulate API call
setTimeout(() => {
console.log(`🌐 [EXT] Synced with external API: ${action} - Product ${productId}, Qty: ${quantity}`);
}, Math.random() * 1000);
}
private updateCart(items: CartItem[]): void {
this.cartItemsSubject.next(items);
this.cartCountSubject.next(this.getTotalItems());
this.saveCartToStorage(items);
}
private saveCartToStorage(items: CartItem[]): void {
try {
localStorage.setItem('cart_ext', JSON.stringify(items));
} catch (error) {
console.error('❌ [EXT] Cannot save cart to localStorage:', error);
}
}
private loadCartFromStorage(): void {
try {
const savedCart = localStorage.getItem('cart_ext');
if (savedCart) {
const items: CartItem[] = JSON.parse(savedCart);
this.cartItemsSubject.next(items);
this.cartCountSubject.next(this.getTotalItems());
console.log('📥 [EXT] Loaded cart from localStorage');
}
} catch (error) {
console.error('❌ [EXT] Cannot load cart from localStorage:', error);
}
}
}
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ProductModel, CartItem } from '../interface/product.interface';
/**
* CartService - Demo Dependency Injection trong Angular
*
* @Injectable decorator:
* - Đánh dấu class này có thể được inject
* - providedIn: 'root' = singleton cho toàn app
* - Angular DI container sẽ tự động tạo và quản lý instance
*/
@Injectable({
providedIn: 'root' // Singleton pattern - chỉ có 1 instance trong toàn app
})
export class CartService {
// BehaviorSubject để theo dõi state changes
private cartItemsSubject = new BehaviorSubject<CartItem[]>([]);
private cartCountSubject = new BehaviorSubject<number>(0);
// Expose as Observable để components có thể subscribe
cartItems$: Observable<CartItem[]> = this.cartItemsSubject.asObservable();
cartCount$: Observable<number> = this.cartCountSubject.asObservable();
constructor() {
console.log('🛒 CartService instance được tạo');
// Load from localStorage if exists
this.loadCartFromStorage();
}
/**
* Thêm sản phẩm vào giỏ hàng
*/
addToCart(product: ProductModel, quantity: number = 1): void {
const currentItems = this.cartItemsSubject.value;
const existingItemIndex = currentItems.findIndex(item => item.product.id === product.id);
let updatedItems: CartItem[];
if (existingItemIndex >= 0) {
// Sản phẩm đã có trong giỏ - tăng quantity
updatedItems = currentItems.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + quantity }
: item
);
} else {
// Thêm sản phẩm mới
updatedItems = [...currentItems, { product, quantity }];
}
this.updateCart(updatedItems);
console.log(`✅ Đã thêm ${product.name} vào giỏ hàng`);
}
/**
* Xóa sản phẩm khỏi giỏ hàng
*/
removeFromCart(productId: number): void {
const currentItems = this.cartItemsSubject.value;
const updatedItems = currentItems.filter(item => item.product.id !== productId);
this.updateCart(updatedItems);
console.log(`🗑️ Đã xóa sản phẩm ID ${productId} khỏi giỏ hàng`);
}
/**
* Cập nhật số lượng sản phẩm
*/
updateQuantity(productId: number, quantity: number): void {
if (quantity <= 0) {
this.removeFromCart(productId);
return;
}
const currentItems = this.cartItemsSubject.value;
const updatedItems = currentItems.map(item =>
item.product.id === productId
? { ...item, quantity }
: item
);
this.updateCart(updatedItems);
console.log(`📝 Đã cập nhật số lượng sản phẩm ID ${productId} = ${quantity}`);
}
/**
* Tính tổng tiền
*/
calculateTotal(): number {
return this.cartItemsSubject.value.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
);
}
/**
* Lấy tổng số sản phẩm trong giỏ
*/
getTotalItems(): number {
return this.cartItemsSubject.value.reduce(
(total, item) => total + item.quantity,
0
);
}
/**
* Xóa toàn bộ giỏ hàng
*/
clearCart(): void {
this.updateCart([]);
console.log('🧹 Đã xóa toàn bộ giỏ hàng');
}
/**
* Lấy danh sách items hiện tại
*/
getCurrentItems(): CartItem[] {
return this.cartItemsSubject.value;
}
/**
* Private method để update cart và sync với localStorage
*/
private updateCart(items: CartItem[]): void {
this.cartItemsSubject.next(items);
this.cartCountSubject.next(this.getTotalItems());
this.saveCartToStorage(items);
}
/**
* Lưu giỏ hàng vào localStorage
*/
private saveCartToStorage(items: CartItem[]): void {
try {
localStorage.setItem('cart', JSON.stringify(items));
} catch (error) {
console.error('❌ Không thể lưu giỏ hàng vào localStorage:', error);
}
}
/**
* Load giỏ hàng từ localStorage
*/
private loadCartFromStorage(): void {
try {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
const items: CartItem[] = JSON.parse(savedCart);
this.cartItemsSubject.next(items);
this.cartCountSubject.next(this.getTotalItems());
console.log('📦 Đã load giỏ hàng từ localStorage');
}
} catch (error) {
console.error('❌ Không thể load giỏ hàng từ localStorage:', error);
}
}
}
import { Injectable } from '@angular/core';
import { Observable, of, delay } from 'rxjs';
import { ProductModel } from '../interface/product.interface';
import { MOCK_PRODUCTS } from '../const/products.const';
/**
* ProductService - Quản lý dữ liệu sản phẩm
* Demo về cách tạo service với @Injectable
*/
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor() {
console.log('🛍️ ProductService instance được tạo');
}
/**
* Lấy danh sách tất cả sản phẩm
* Simulate API call với delay
*/
getAllProducts(): Observable<ProductModel[]> {
console.log('📦 Loading products...');
return of(MOCK_PRODUCTS).pipe(
delay(500) // Simulate network delay
);
}
/**
* Lấy sản phẩm theo ID
*/
getProductById(id: number): Observable<ProductModel | undefined> {
const product = MOCK_PRODUCTS.find(p => p.id === id);
return of(product).pipe(delay(200));
}
/**
* Tìm kiếm sản phẩm theo tên
*/
searchProducts(query: string): Observable<ProductModel[]> {
const filteredProducts = MOCK_PRODUCTS.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase()) ||
product.description?.toLowerCase().includes(query.toLowerCase())
);
return of(filteredProducts).pipe(delay(300));
}
/**
* Lấy sản phẩm theo category (mock)
*/
getProductsByCategory(category: string): Observable<ProductModel[]> {
// Mock implementation - trong thực tế sẽ filter theo category
return of(MOCK_PRODUCTS).pipe(delay(400));
}
}
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Author } from '../../data-access/interface/author.interface';
@Component({
selector: 'app-author-detail',
standalone: true,
imports: [CommonModule],
template: `
<div *ngIf="author" class="tw-border tw-border-gray-300 tw-rounded-lg tw-p-4 tw-mb-3 tw-bg-white tw-shadow-sm hover:tw-shadow-md tw-transition-shadow">
<div class="tw-flex tw-justify-between tw-items-center">
<div class="tw-flex-1">
<div class="tw-flex tw-items-center tw-space-x-2 tw-mb-2">
<strong class="tw-text-lg tw-text-gray-800">{{ author.firstName }} {{ author.lastName }}</strong>
<span class="tw-text-xs tw-bg-gray-100 tw-px-2 tw-py-1 tw-rounded tw-font-mono">ID: {{ author.id }}</span>
</div>
<p class="tw-text-sm tw-text-gray-600 tw-mb-1">
📧 {{ author.email }}
</p>
<div class="tw-flex tw-space-x-4 tw-text-xs tw-text-gray-500">
<span>👤 {{ author.gender }}</span>
<span>🌐 {{ author.ipAddress }}</span>
</div>
</div>
<button
(click)="handleDelete()"
class="tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-font-bold tw-py-2 tw-px-3 tw-rounded tw-transition-colors tw-ml-4"
title="Delete Author"
>
🗑️
</button>
</div>
</div>
`,
styles: []
})
export class AuthorDetailComponent implements OnInit {
@Input() author!: Author;
@Output() deleteAuthor = new EventEmitter<Author>();
constructor() {}
ngOnInit(): void {
console.log('AuthorDetailComponent initialized for:', this.author?.firstName, this.author?.lastName);
}
handleDelete(): void {
console.log('Delete button clicked for author:', this.author);
console.log('Emitting deleteAuthor event...');
this.deleteAuthor.emit(this.author);
}
}
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthorDetailComponent } from '../author-detail/author-detail.component';
import { Author } from '../../data-access/interface/author.interface';
import { authors } from '../../data-access/const/authors.const';
@Component({
selector: 'app-author-list',
standalone: true,
imports: [CommonModule, AuthorDetailComponent],
template: `
<div class="tw-max-w-4xl tw-mx-auto tw-p-6">
<div class="tw-flex tw-flex-col sm:tw-flex-row tw-justify-between tw-items-start sm:tw-items-center tw-mb-6 tw-gap-4">
<h2 class="tw-text-2xl tw-font-bold tw-text-gray-800">Authors List ({{ authors.length }} authors)</h2>
<button
(click)="resetAuthors()"
class="tw-bg-white hover:tw-bg-gray-100 tw-text-black tw-border-2 tw-border-black tw-px-6 tw-py-2 tw-rounded-lg tw-transition-colors tw-font-bold tw-shadow-md hover:tw-shadow-lg"
>
🔄 Reset List
</button>
</div>
<!-- Event Log -->
<div *ngIf="eventLog.length > 0" class="tw-mb-6 tw-bg-gray-100 tw-p-4 tw-rounded-lg tw-border">
<h3 class="tw-font-semibold tw-text-gray-700 tw-mb-2">🔔 Event Log:</h3>
<div class="tw-space-y-1 tw-max-h-40 tw-overflow-y-auto">
<div *ngFor="let event of eventLog; let i = index"
class="tw-text-sm tw-text-gray-600 tw-flex tw-items-start tw-space-x-2">
<span class="tw-font-mono tw-text-xs tw-bg-gray-200 tw-px-2 tw-py-1 tw-rounded tw-flex-shrink-0">{{ i + 1 }}</span>
<span class="tw-flex-1">{{ event }}</span>
</div>
</div>
<div class="tw-flex tw-justify-between tw-items-center tw-mt-3">
<span class="tw-text-xs tw-text-gray-500">Total events: {{ eventLog.length }}</span>
<button
(click)="clearEventLog()"
class="tw-text-xs tw-bg-red-100 tw-text-red-600 hover:tw-bg-red-200 tw-px-3 tw-py-1 tw-rounded tw-transition-colors"
>
Clear Log
</button>
</div>
</div>
<div class="tw-space-y-3">
<app-author-detail
*ngFor="let author of authors; trackBy: trackByAuthorId"
[author]="author"
(deleteAuthor)="handleDelete($event)"
></app-author-detail>
</div>
<div *ngIf="authors.length === 0" class="tw-text-center tw-py-12 tw-bg-gray-50 tw-rounded-lg tw-border-2 tw-border-dashed tw-border-gray-300">
<div class="tw-text-6xl tw-mb-4">📝</div>
<p class="tw-text-gray-500 tw-text-xl tw-mb-2">No authors available</p>
<p class="tw-text-sm tw-text-gray-400 tw-mb-4">All authors have been deleted</p>
<button
(click)="resetAuthors()"
class="tw-bg-white hover:tw-bg-gray-100 tw-text-black tw-border-2 tw-border-black tw-px-6 tw-py-2 tw-rounded-lg tw-transition-colors tw-font-bold"
>
🔄 Restore Authors
</button>
</div>
</div>
`,
styles: []
})
export class AuthorListComponent implements OnInit {
authors: Author[] = [...authors]; // Tạo copy để có thể modify
eventLog: string[] = [];
constructor() {}
ngOnInit(): void {
this.logEvent('Author List component initialized');
}
handleDelete(author: Author): void {
this.logEvent(`🗑️ Delete event received for: ${author.firstName} ${author.lastName} (ID: ${author.id})`);
this.authors = this.authors.filter(item => item.id !== author.id);
this.logEvent(`✅ Author deleted successfully. Remaining: ${this.authors.length} authors`);
}
resetAuthors(): void {
this.authors = [...authors];
this.logEvent('🔄 Authors list reset to initial state');
}
clearEventLog(): void {
this.eventLog = [];
}
logEvent(message: string): void {
const timestamp = new Date().toLocaleTimeString();
this.eventLog.push(`[${timestamp}] ${message}`);
console.log('Event:', message);
}
trackByAuthorId(index: number, author: Author): number {
return author.id;
}
}
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-card',
standalone: true,
imports: [CommonModule],
template: `
<div class="tw-bg-white tw-rounded-lg tw-shadow-md tw-overflow-hidden tw-mb-4 tw-border tw-border-gray-300">
<!-- Header slot với attribute selector -->
<div class="tw-bg-blue-100 tw-px-4 tw-py-3 tw-border-b tw-border-gray-200">
<div class="tw-text-xs tw-text-blue-600 tw-mb-1">🎯 HEADER SLOT [slot=header]:</div>
<ng-content select="[slot=header]"></ng-content>
</div>
<!-- Body slot - default content -->
<div class="tw-p-4 tw-text-gray-700 tw-leading-relaxed tw-bg-yellow-50">
<div class="tw-text-xs tw-text-yellow-600 tw-mb-2">📝 BODY SLOT (default):</div>
<ng-content></ng-content>
</div>
<!-- Footer slot với class selector -->
<div class="tw-bg-green-100 tw-px-4 tw-py-3 tw-border-t tw-border-gray-200 tw-text-sm tw-text-gray-600">
<div class="tw-text-xs tw-text-green-600 tw-mb-1">🏷️ FOOTER SLOT .card-footer-content:</div>
<ng-content select=".card-footer-content"></ng-content>
</div>
<!-- Actions slot với class selector hoặc button selector -->
<div class="tw-px-4 tw-py-3 tw-border-t tw-border-gray-200 tw-flex tw-gap-2 tw-justify-end tw-bg-red-100">
<div class="tw-text-xs tw-text-red-600 tw-w-full tw-mb-2">🔘 ACTIONS SLOT (.card-actions OR button):</div>
<div class="tw-w-full tw-flex tw-gap-2 tw-justify-end tw-flex-wrap">
<ng-content select=".card-actions"></ng-content>
<ng-content select="button"></ng-content>
</div>
</div>
</div>
`,
styles: []
})
export class CardComponent {
constructor() {}
}
<div class="cart-view-container tw-p-6">
<div class="header tw-mb-6">
<h2 class="tw-text-2xl tw-font-bold tw-text-gray-800 tw-mb-2">
🛒 Giỏ Hàng Của Bạn
</h2>
<p class="tw-text-gray-600">
Demo Service Sharing - Cùng CartService instance với ProductList
</p>
</div>
<!-- Empty Cart State -->
<div *ngIf="cartItems.length === 0" class="empty-cart tw-text-center tw-py-12">
<div class="tw-text-6xl tw-mb-4">🛒</div>
<h3 class="tw-text-xl tw-font-semibold tw-text-gray-600 tw-mb-2">Giỏ hàng trống</h3>
<p class="tw-text-gray-500">Hãy thêm một số sản phẩm vào giỏ hàng của bạn</p>
</div>
<!-- Cart Items -->
<div *ngIf="cartItems.length > 0" class="cart-content">
<!-- Cart Items List -->
<div class="cart-items tw-mb-6">
<div
*ngFor="let item of cartItems; trackBy: trackByProductId"
class="cart-item tw-bg-white tw-rounded-lg tw-shadow-md tw-p-4 tw-mb-4 tw-flex tw-items-center tw-gap-4"
>
<!-- Product Image -->
<div class="product-image tw-flex-shrink-0">
<img
[src]="item.product.imageUrl"
[alt]="item.product.name"
class="tw-w-20 tw-h-20 tw-object-cover tw-rounded-md"
(error)="onImageError($event)"
>
</div>
<!-- Product Info -->
<div class="product-info tw-flex-1">
<h3 class="tw-font-semibold tw-text-lg tw-text-gray-800 tw-mb-1">
{{ item.product.name }}
</h3>
<p class="tw-text-sm tw-text-gray-500 tw-mb-1">
SKU: {{ item.product.sku }}
</p>
<p class="tw-text-sm tw-text-gray-600">
{{ item.product.description }}
</p>
</div>
<!-- Price -->
<div class="price-info tw-text-center">
<p class="tw-text-sm tw-text-gray-500">Giá</p>
<p class="tw-font-semibold tw-text-lg tw-text-blue-600">
${{ item.product.price }}
</p>
</div>
<!-- Quantity Controls -->
<div class="quantity-controls tw-flex tw-items-center tw-gap-2">
<button
(click)="decrementQuantity(item.product.id, item.quantity)"
class="quantity-btn tw-bg-gray-200 hover:tw-bg-gray-300 tw-text-gray-700 tw-w-8 tw-h-8 tw-rounded tw-flex tw-items-center tw-justify-center tw-transition-colors"
>
</button>
<span class="quantity-display tw-bg-gray-50 tw-px-3 tw-py-1 tw-rounded tw-text-center tw-min-w-[3rem]">
{{ item.quantity }}
</span>
<button
(click)="incrementQuantity(item.product.id, item.quantity)"
class="quantity-btn tw-bg-gray-200 hover:tw-bg-gray-300 tw-text-gray-700 tw-w-8 tw-h-8 tw-rounded tw-flex tw-items-center tw-justify-center tw-transition-colors"
>
+
</button>
</div>
<!-- Item Total -->
<div class="item-total tw-text-center">
<p class="tw-text-sm tw-text-gray-500">Tổng</p>
<p class="tw-font-semibold tw-text-lg tw-text-green-600">
${{ (item.product.price * item.quantity).toFixed(2) }}
</p>
</div>
<!-- Remove Button -->
<div class="remove-action">
<button
(click)="removeItem(item.product.id)"
class="remove-btn tw-bg-red-500 hover:tw-bg-red-600 tw-text-white tw-p-2 tw-rounded tw-transition-colors"
title="Xóa sản phẩm"
>
🗑️
</button>
</div>
</div>
</div>
<!-- Cart Summary -->
<div class="cart-summary tw-bg-gray-50 tw-rounded-lg tw-p-6">
<h3 class="tw-font-semibold tw-text-lg tw-mb-4">📊 Tổng Kết Đơn Hàng</h3>
<div class="summary-details tw-space-y-2 tw-mb-4">
<div class="tw-flex tw-justify-between">
<span>Tổng sản phẩm:</span>
<span class="tw-font-semibold">{{ cartCount }} items</span>
</div>
<div class="tw-flex tw-justify-between">
<span>Tạm tính:</span>
<span>${{ cartSubtotal.toFixed(2) }}</span>
</div>
<div *ngIf="cartDiscount > 0" class="tw-flex tw-justify-between tw-text-green-600">
<span>Giảm giá:</span>
<span>-${{ cartDiscount.toFixed(2) }}</span>
</div>
<hr class="tw-my-2">
<div class="tw-flex tw-justify-between tw-text-lg tw-font-bold">
<span>Tổng cộng:</span>
<span class="tw-text-blue-600">${{ cartTotal.toFixed(2) }}</span>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons tw-flex tw-gap-3">
<button
(click)="clearCart()"
class="clear-btn tw-flex-1 tw-bg-gray-500 hover:tw-bg-gray-600 tw-text-white tw-py-2 tw-px-4 tw-rounded tw-transition-colors"
>
🧹 Xóa Giỏ Hàng
</button>
<button
class="checkout-btn tw-flex-1 tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-py-2 tw-px-4 tw-rounded tw-transition-colors"
>
💳 Thanh Toán
</button>
</div>
</div>
<!-- Service Sharing Info -->
<div class="service-info tw-mt-6 tw-p-4 tw-bg-green-50 tw-border-l-4 tw-border-green-400">
<h4 class="tw-font-semibold tw-text-lg tw-mb-2">🔗 Service Sharing Demo</h4>
<ul class="tw-text-sm tw-text-gray-700 tw-space-y-1">
<li><strong>CartService</strong> được share giữa ProductList và CartView</li>
<li>✅ Thay đổi ở ProductList sẽ <strong>tự động update</strong> ở CartView</li>
<li>✅ Cùng một <strong>singleton instance</strong> được sử dụng</li>
<li>✅ Data được <strong>sync real-time</strong> thông qua RxJS Observables</li>
</ul>
</div>
</div>
</div>
.cart-item {
transition: transform 0.2s ease;
}
.cart-item:hover {
transform: translateY(-1px);
}
.quantity-btn {
font-size: 18px;
font-weight: bold;
line-height: 1;
}
.quantity-btn:hover {
transform: scale(1.1);
}
.quantity-display {
font-weight: 600;
border: 1px solid #e5e7eb;
}
.remove-btn:hover {
transform: scale(1.1);
}
.empty-cart {
opacity: 0.7;
}
.summary-details {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1rem;
background: white;
}
.action-buttons button {
font-weight: 600;
}
.service-info {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subject, takeUntil } from 'rxjs';
import { CartService } from '../../../shared/data-access/service/cart.service';
import { CartItem } from '../../../shared/data-access/interface/product.interface';
/**
* CartViewComponent - Demo Service Sharing
*
* Component này share cùng CartService instance với ProductListComponent
* Minh chứng tính singleton của service khi providedIn: 'root'
*/
@Component({
selector: 'app-cart-view',
standalone: true,
imports: [CommonModule],
templateUrl: './cart-view.component.html',
styleUrls: ['./cart-view.component.scss']
})
export class CartViewComponent implements OnInit, OnDestroy {
cartItems: CartItem[] = [];
cartCount = 0;
cartTotal = 0;
cartSubtotal = 0;
cartDiscount = 0;
private destroy$ = new Subject<void>();
/**
* Constructor - Inject cùng CartService instance
*/
constructor(private cartService: CartService) {
console.log('🛒 CartViewComponent constructor called');
console.log('🔗 CartService injected (same instance):', !!this.cartService);
}
ngOnInit(): void {
console.log('🚀 CartViewComponent ngOnInit');
this.subscribeToCartChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Subscribe to cart changes từ shared CartService
*/
private subscribeToCartChanges(): void {
// Subscribe to cart items
this.cartService.cartItems$
.pipe(takeUntil(this.destroy$))
.subscribe((items: CartItem[]) => {
this.cartItems = items;
this.updateCartTotals();
console.log('🔄 Cart items updated in CartView:', items.length);
});
// Subscribe to cart count
this.cartService.cartCount$
.pipe(takeUntil(this.destroy$))
.subscribe((count: number) => {
this.cartCount = count;
});
}
/**
* Update cart totals
*/
private updateCartTotals(): void {
this.cartSubtotal = this.cartItems.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
);
this.cartTotal = this.cartService.calculateTotal();
this.cartDiscount = this.cartSubtotal - this.cartTotal;
}
/**
* Update quantity sử dụng shared CartService
*/
updateQuantity(productId: number, quantity: number): void {
console.log(`📝 Updating quantity for product ${productId}: ${quantity}`);
this.cartService.updateQuantity(productId, quantity);
}
/**
* Remove item sử dụng shared CartService
*/
removeItem(productId: number): void {
console.log(`🗑️ Removing product ${productId} from cart`);
this.cartService.removeFromCart(productId);
}
/**
* Clear cart sử dụng shared CartService
*/
clearCart(): void {
if (confirm('Bạn có chắc chắn muốn xóa toàn bộ giỏ hàng?')) {
console.log('🧹 Clearing entire cart');
this.cartService.clearCart();
}
}
/**
* Increment quantity
*/
incrementQuantity(productId: number, currentQuantity: number): void {
this.updateQuantity(productId, currentQuantity + 1);
}
/**
* Decrement quantity
*/
decrementQuantity(productId: number, currentQuantity: number): void {
if (currentQuantity > 1) {
this.updateQuantity(productId, currentQuantity - 1);
} else {
this.removeItem(productId);
}
}
/**
* Handle image error
*/
onImageError(event: any): void {
event.target.src = 'https://picsum.photos/80/80?random=888';
}
/**
* TrackBy function for ngFor optimization
*/
trackByProductId(index: number, item: CartItem): number {
return item.product.id;
}
}
<div class="di-comparison-container p-6">
<div class="header mb-8">
<h2 class="text-3xl font-bold text-gray-800 mb-3">
🔧 Dependency Injection - So Sánh & Demo
</h2>
<p class="text-gray-600 text-lg">
Hiểu rõ sự khác biệt giữa Tight Coupling và Dependency Injection
</p>
</div>
<!-- Introduction -->
<div class="intro-section mb-8 p-6 bg-blue-50 border-l-4 border-blue-400 rounded-r-lg">
<h3 class="text-xl font-semibold mb-3">📚 Dependency Injection là gì?</h3>
<div class="space-y-2 text-gray-700">
<p><strong>Dependency Injection (DI)</strong> là một design pattern quan trọng trong lập trình:</p>
<ul class="list-disc list-inside space-y-1 ml-4">
<li><strong>Injector:</strong> Container chứa các API để tạo và quản lý instances</li>
<li><strong>Provider:</strong> "Công thức" để Injector biết cách tạo dependency</li>
<li><strong>Dependency:</strong> Object/service cần được inject vào component</li>
</ul>
</div>
</div>
<!-- Bad vs Good Comparison -->
<div class="comparison-section mb-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Bad Example -->
<div class="bad-example bg-red-50 border border-red-200 rounded-lg p-6">
<div class="header-bad mb-4">
<h3 class="text-xl font-semibold text-red-700 mb-2">
❌ Tight Coupling (Không nên làm)
</h3>
</div>
<div class="code-block bg-white border border-red-300 rounded p-4 mb-4">
<pre class="text-sm text-gray-800 whitespace-pre-wrap">{{ badExample }}</pre>
</div>
<div class="problems">
<h4 class="font-semibold text-red-600 mb-2">🚫 Vấn đề:</h4>
<ul class="text-sm text-red-700 space-y-1">
<li>• Khó thay đổi implementation</li>
<li>• Không thể test với mock objects</li>
<li>• Mỗi component có instance riêng</li>
<li>• Vi phạm SOLID principles</li>
<li>• Code cứng nhắc, khó maintain</li>
</ul>
</div>
</div>
<!-- Good Example -->
<div class="good-example bg-green-50 border border-green-200 rounded-lg p-6">
<div class="header-good mb-4">
<h3 class="text-xl font-semibold text-green-700 mb-2">
✅ Dependency Injection (Cách đúng)
</h3>
</div>
<div class="code-block bg-white border border-green-300 rounded p-4 mb-4">
<pre class="text-sm text-gray-800 whitespace-pre-wrap">{{ goodExample }}</pre>
</div>
<div class="benefits">
<h4 class="font-semibold text-green-600 mb-2">✨ Ưu điểm:</h4>
<ul class="text-sm text-green-700 space-y-1">
<li>• Loose coupling giữa các components</li>
<li>• Dễ dàng test với mock objects</li>
<li>• Share state giữa components</li>
<li>• Tuân thủ SOLID principles</li>
<li>• Code linh hoạt, dễ maintain</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Provider Override -->
<div class="override-section mb-8 p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
<h3 class="text-xl font-semibold text-yellow-700 mb-4">
🔄 Provider Override - Thay Đổi Implementation
</h3>
<div class="code-block bg-white border border-yellow-300 rounded p-4 mb-4">
<pre class="text-sm text-gray-800 whitespace-pre-wrap">{{ overrideExample }}</pre>
</div>
<div class="explanation text-yellow-700">
<p class="mb-2">
<strong>Provider Override</strong> cho phép thay đổi implementation mà không cần sửa component code:
</p>
<ul class="list-disc list-inside space-y-1 ml-4">
<li>Development environment: MockCartService</li>
<li>Production environment: ApiCartService</li>
<li>Testing environment: TestCartService</li>
</ul>
</div>
</div>
<!-- Interactive Demo -->
<div class="demo-section mb-8">
<h3 class="text-2xl font-semibold text-gray-800 mb-6">🎮 Interactive Demo</h3>
<div class="demo-buttons grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<button
(click)="demoManualInstantiation()"
class="demo-btn bg-red-500 hover:bg-red-600 text-white py-3 px-4 rounded-lg transition-colors font-semibold"
>
❌ Manual Instance
</button>
<button
(click)="demoServiceMethods()"
class="demo-btn bg-green-500 hover:bg-green-600 text-white py-3 px-4 rounded-lg transition-colors font-semibold"
>
✅ Injected Service
</button>
<button
(click)="demoProviderOverride()"
class="demo-btn bg-blue-500 hover:bg-blue-600 text-white py-3 px-4 rounded-lg transition-colors font-semibold"
>
🔄 Provider Override
</button>
<button
(click)="demoSingleton()"
class="demo-btn bg-purple-500 hover:bg-purple-600 text-white py-3 px-4 rounded-lg transition-colors font-semibold"
>
🔗 Singleton Pattern
</button>
</div>
<div class="demo-instruction bg-gray-50 border border-gray-200 rounded-lg p-4">
<p class="text-gray-700">
<strong>📝 Hướng dẫn:</strong> Nhấn các nút trên và kiểm tra <strong>Browser Console</strong>
(F12 → Console) để xem chi tiết cách DI hoạt động.
</p>
</div>
</div>
<!-- DI Benefits -->
<div class="benefits-section mb-8">
<h3 class="text-2xl font-semibold text-gray-800 mb-6">🌟 Lợi Ích Của Dependency Injection</h3>
<div class="benefits-grid grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="benefit-card bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
<div class="benefit-icon text-3xl mb-3">🧪</div>
<h4 class="font-semibold text-lg mb-2">Testability</h4>
<p class="text-gray-600">
Dễ dàng mock dependencies trong unit tests, tăng code coverage và reliability.
</p>
</div>
<div class="benefit-card bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
<div class="benefit-icon text-3xl mb-3">🔧</div>
<h4 class="font-semibold text-lg mb-2">Maintainability</h4>
<p class="text-gray-600">
Code loose coupling, dễ modify và extend mà không ảnh hưởng các component khác.
</p>
</div>
<div class="benefit-card bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
<div class="benefit-icon text-3xl mb-3">🔄</div>
<h4 class="font-semibold text-lg mb-2">Flexibility</h4>
<p class="text-gray-600">
Dễ dàng swap implementations cho different environments (dev, test, prod).
</p>
</div>
<div class="benefit-card bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
<div class="benefit-icon text-3xl mb-3"></div>
<h4 class="font-semibold text-lg mb-2">Performance</h4>
<p class="text-gray-600">
Singleton pattern giúp share instances, giảm memory usage và tăng performance.
</p>
</div>
</div>
</div>
<!-- Angular DI Features -->
<div class="angular-features-section">
<h3 class="text-2xl font-semibold text-gray-800 mb-6">🅰️ Angular DI Features</h3>
<div class="features-list space-y-4">
<div class="feature-item bg-white border border-gray-200 rounded-lg p-4 flex items-start gap-4">
<div class="feature-icon text-2xl">🎯</div>
<div>
<h4 class="font-semibold text-lg mb-1">Hierarchical Injection</h4>
<p class="text-gray-600">
DI system có hierarchy từ root → module → component, cho phép override ở mỗi level.
</p>
</div>
</div>
<div class="feature-item bg-white border border-gray-200 rounded-lg p-4 flex items-start gap-4">
<div class="feature-icon text-2xl">🏭</div>
<div>
<h4 class="font-semibold text-lg mb-1">Multiple Provider Types</h4>
<p class="text-gray-600">
Class, Factory, Value, và Existing providers cho flexibility tối đa.
</p>
</div>
</div>
<div class="feature-item bg-white border border-gray-200 rounded-lg p-4 flex items-start gap-4">
<div class="feature-icon text-2xl">🔍</div>
<div>
<h4 class="font-semibold text-lg mb-1">Tree-shakable Services</h4>
<p class="text-gray-600">
providedIn: 'root' cho phép tree-shaking, chỉ bundle services thực sự được sử dụng.
</p>
</div>
</div>
</div>
</div>
</div>
.comparison-section {
.bad-example, .good-example {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.bad-example:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
}
.good-example:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.15);
}
}
.code-block {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
}
.demo-btn {
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
}
}
.benefit-card, .feature-item {
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.benefit-icon, .feature-icon {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.intro-section {
animation: slideIn 0.6s ease-out;
}
.demo-instruction {
animation: fadeIn 0.8s ease-out 0.3s both;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.demo-section {
.demo-buttons {
.demo-btn {
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s;
}
&:hover::before {
left: 100%;
}
}
}
}
// Responsive improvements
@media (max-width: 768px) {
.comparison-section {
.grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
.benefits-grid {
grid-template-columns: 1fr;
}
.demo-buttons {
grid-template-columns: 1fr;
gap: 0.75rem;
}
}
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartService } from '../../../shared/data-access/service/cart.service';
import { CartExtService } from '../../../shared/data-access/service/cart-ext.service';
/**
* BadProductComponent - Demo cách KHÔNG NÊN làm (Tight Coupling)
* Khởi tạo trực tiếp dependencies, khó test và maintain
*/
class BadProductComponent {
private cartService: CartService;
constructor() {
// ❌ BAD: Tight coupling - tạo dependency trực tiếp
this.cartService = new CartService();
console.log('❌ BadProductComponent: Tạo CartService trực tiếp');
}
addToCart(productId: number): void {
console.log('❌ BadProductComponent: Adding product với tight coupling');
// Logic thêm sản phẩm...
}
// ❌ Vấn đề:
// - Không thể dễ dàng thay đổi implementation
// - Khó test (không mock được)
// - Vi phạm Dependency Inversion Principle
// - Mỗi component sẽ có instance riêng (không share state)
}
/**
* GoodProductComponent - Demo cách ĐÚNG (Dependency Injection)
* Inject dependencies thông qua constructor
*/
class GoodProductComponent {
constructor(private cartService: CartService) {
console.log('✅ GoodProductComponent: CartService được inject');
}
addToCart(productId: number): void {
console.log('✅ GoodProductComponent: Adding product với DI');
// Logic thêm sản phẩm...
}
// ✅ Ưu điểm:
// - Loose coupling
// - Dễ test (có thể mock service)
// - Có thể dễ dàng thay đổi implementation
// - Share state với các component khác
}
/**
* DIComparisonComponent - Demo so sánh Tight Coupling vs Dependency Injection
*/
@Component({
selector: 'app-di-comparison',
standalone: true,
imports: [CommonModule],
templateUrl: './di-comparison.component.html',
styleUrls: ['./di-comparison.component.scss']
})
export class DIComparisonComponent implements OnInit {
// Demo examples
badExample: string = '';
goodExample: string = '';
overrideExample: string = '';
// Inject CartService để demo
constructor(private cartService: CartService) {
console.log('🎯 DIComparisonComponent: CartService injected');
}
ngOnInit(): void {
this.setupExamples();
this.demonstrateDI();
}
private setupExamples(): void {
// Bad example code
this.badExample = `
// ❌ CÁCH KHÔNG NÊN LÀM (Tight Coupling)
class ProductComponent {
cartService: CartService;
constructor() {
// Tạo dependency trực tiếp
this.cartService = new CartService();
}
addToCart(product: Product) {
this.cartService.addToCart(product);
}
}
// Vấn đề:
// - Không thể thay đổi implementation
// - Khó test (không mock được)
// - Mỗi component có instance riêng
// - Vi phạm SOLID principles
`;
// Good example code
this.goodExample = `
// ✅ CÁCH ĐÚNG (Dependency Injection)
@Component({
selector: 'app-product',
// ...
})
export class ProductComponent {
constructor(private cartService: CartService) {
// Angular DI container tự động inject
}
addToCart(product: Product) {
this.cartService.addToCart(product);
}
}
// Ưu điểm:
// - Loose coupling
// - Dễ test và mock
// - Share state giữa components
// - Dễ thay đổi implementation
`;
// Override example
this.overrideExample = `
// 🔄 OVERRIDE PROVIDER
@NgModule({
providers: [
{
provide: CartService,
useClass: CartExtService // Thay thế implementation
}
]
})
export class AppModule {}
// Hoặc trong @Injectable
@Injectable({
providedIn: 'root',
useClass: CartExtService
})
export class CartService {}
`;
}
private demonstrateDI(): void {
console.log('🔍 Demonstrating DI concepts...');
// Demo 1: Service instance
console.log('📦 Current CartService instance:', this.cartService);
console.log('🔢 Cart total:', this.cartService.calculateTotal());
// Demo 2: Service methods
this.cartService.getCurrentItems().forEach(item => {
console.log(`🛒 Item in cart: ${item.product.name} x ${item.quantity}`);
});
}
/**
* Demo manual instantiation (BAD practice)
*/
demoManualInstantiation(): void {
console.log('❌ Demo: Manual instantiation (BAD)');
// Tạo instance mới - KHÔNG được share state
// const manualCartService = new CartService(); // TypeScript error: constructor is private
console.log('❌ Không thể tạo CartService manually - Constructor injection required');
}
/**
* Demo service methods thông qua injected service
*/
demoServiceMethods(): void {
console.log('✅ Demo: Using injected service');
// Sử dụng injected service
const total = this.cartService.calculateTotal();
const itemCount = this.cartService.getTotalItems();
console.log('💰 Total from injected service:', total);
console.log('📦 Item count from injected service:', itemCount);
}
/**
* Demo provider override concept
*/
demoProviderOverride(): void {
console.log('🔄 Demo: Provider Override Concept');
console.log('📝 Check console for detailed explanation');
// Giải thích trong console
console.log(`
🔧 Provider Override trong Angular DI:
1. Default Provider:
@Injectable({ providedIn: 'root' })
export class CartService { ... }
2. Override trong NgModule:
@NgModule({
providers: [
{ provide: CartService, useClass: CartExtService }
]
})
3. Override trong Component:
@Component({
providers: [
{ provide: CartService, useClass: CartExtService }
]
})
4. Factory Provider:
{
provide: CartService,
useFactory: (http: HttpClient) => new CartApiService(http),
deps: [HttpClient]
}
5. Value Provider:
{ provide: API_URL, useValue: 'https://api.example.com' }
`);
}
/**
* Demo singleton pattern
*/
demoSingleton(): void {
console.log('🔗 Demo: Singleton Pattern in DI');
// Injected service là singleton
const service1 = this.cartService;
// Nếu inject ở component khác cũng sẽ là cùng instance
console.log('🎯 CartService instance:', service1);
console.log('🔄 Same instance across components due to providedIn: "root"');
}
}
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-modal',
standalone: true,
imports: [CommonModule],
template: `
<div
class="tw-fixed tw-inset-0 tw-bg-black tw-bg-opacity-50 tw-flex tw-items-center tw-justify-center tw-z-50"
*ngIf="isOpen"
(click)="closeModal()">
<div
class="tw-bg-white tw-rounded-lg tw-max-w-lg tw-w-full tw-mx-4 tw-max-h-screen tw-overflow-hidden tw-shadow-xl"
(click)="$event.stopPropagation()">
<!-- Modal Header -->
<div class="tw-px-6 tw-py-4 tw-border-b tw-border-gray-200 tw-flex tw-justify-between tw-items-center tw-bg-gray-50">
<ng-content select="modal-header"></ng-content>
<button
class="tw-bg-transparent tw-border-none tw-text-2xl tw-cursor-pointer tw-text-gray-500 hover:tw-text-gray-700 tw-w-8 tw-h-8 tw-flex tw-items-center tw-justify-center tw-rounded hover:tw-bg-gray-200 tw-transition-colors"
(click)="closeModal()"
type="button">
×
</button>
</div>
<!-- Modal Body -->
<div class="tw-p-6 tw-max-h-96 tw-overflow-y-auto">
<ng-content select="modal-body"></ng-content>
</div>
<!-- Modal Footer -->
<div class="tw-px-6 tw-py-4 tw-border-t tw-border-gray-200 tw-flex tw-justify-end tw-gap-2 tw-bg-gray-50">
<ng-content select="modal-footer"></ng-content>
</div>
</div>
</div>
`,
styles: []
})
export class ModalComponent {
@Input() isOpen = false;
@Output() closeEvent = new EventEmitter<void>();
closeModal(): void {
this.closeEvent.emit();
}
}
<div class="product-list-container tw-p-6">
<div class="header tw-mb-6">
<h2 class="tw-text-2xl tw-font-bold tw-text-gray-800 tw-mb-2">
🛍️ Danh Sách Sản Phẩm
</h2>
<p class="tw-text-gray-600">
Demo Dependency Injection - Constructor Injection
</p>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading tw-text-center tw-py-8">
<div class="tw-inline-block tw-animate-spin tw-rounded-full tw-h-8 tw-w-8 tw-border-b-2 tw-border-blue-600"></div>
<p class="tw-mt-2 tw-text-gray-600">Đang tải sản phẩm...</p>
</div>
<!-- Products Grid -->
<div *ngIf="!loading" class="products-grid tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 xl:tw-grid-cols-4 tw-gap-6">
<div
*ngFor="let product of products"
class="product-card tw-bg-white tw-rounded-lg tw-shadow-md hover:tw-shadow-lg tw-transition-shadow tw-duration-300 tw-overflow-hidden"
>
<!-- Product Image -->
<div class="product-image tw-h-48 tw-bg-gray-100 tw-flex tw-items-center tw-justify-center">
<img
[src]="product.imageUrl"
[alt]="product.name"
class="tw-w-full tw-h-full tw-object-cover"
(error)="onImageError($event)"
>
</div>
<!-- Product Info -->
<div class="product-info tw-p-4">
<h3 class="tw-font-semibold tw-text-lg tw-text-gray-800 tw-mb-1 line-clamp-1">
{{ product.name }}
</h3>
<p class="tw-text-sm tw-text-gray-500 tw-mb-2">
SKU: {{ product.sku }}
</p>
<p class="tw-text-sm tw-text-gray-600 tw-mb-3 line-clamp-2">
{{ product.description }}
</p>
<!-- Price -->
<div class="price-section tw-mb-4">
<span class="tw-text-xl tw-font-bold tw-text-blue-600">
${{ product.price }}
</span>
</div>
<!-- Add to Cart Button -->
<button
(click)="addToCart(product)"
class="add-to-cart-btn tw-w-full tw-bg-blue-600 tw-text-white tw-py-2 tw-px-4 tw-rounded-md hover:tw-bg-blue-700 tw-transition-colors tw-duration-200 tw-flex tw-items-center tw-justify-center tw-gap-2"
[disabled]="addingToCart[product.id]"
>
<span *ngIf="!addingToCart[product.id]">
🛒 Thêm vào giỏ
</span>
<span *ngIf="addingToCart[product.id]" class="tw-flex tw-items-center tw-gap-2">
<div class="tw-animate-spin tw-rounded-full tw-h-4 tw-w-4 tw-border-b-2 tw-border-white"></div>
Đang thêm...
</span>
</button>
</div>
</div>
</div>
<!-- Cart Summary -->
<div class="cart-summary tw-mt-8 tw-p-4 tw-bg-blue-50 tw-rounded-lg">
<h3 class="tw-font-semibold tw-text-lg tw-mb-2">📊 Tình Trạng Giỏ Hàng</h3>
<div class="tw-flex tw-justify-between tw-items-center">
<span>Tổng số sản phẩm: <strong>{{ cartCount }}</strong></span>
<span>Tổng tiền: <strong class="tw-text-blue-600">${{ cartTotal.toFixed(2) }}</strong></span>
</div>
</div>
<!-- DI Info -->
<div class="di-info tw-mt-6 tw-p-4 tw-bg-yellow-50 tw-border-l-4 tw-border-yellow-400">
<h4 class="tw-font-semibold tw-text-lg tw-mb-2">🔧 Dependency Injection Info</h4>
<ul class="tw-text-sm tw-text-gray-700 tw-space-y-1">
<li><strong>ProductService</strong> được inject để load dữ liệu</li>
<li><strong>CartService</strong> được inject để quản lý giỏ hàng</li>
<li>✅ Cả hai services đều là <strong>singleton</strong> (providedIn: 'root')</li>
<li>✅ Angular tự động resolve dependencies thông qua constructor</li>
</ul>
</div>
</div>
.line-clamp-1 {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.add-to-cart-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.product-card:hover {
transform: translateY(-2px);
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subject, takeUntil } from 'rxjs';
import { ProductService } from '../../../shared/data-access/service/product.service';
import { CartService } from '../../../shared/data-access/service/cart.service';
import { ProductModel } from '../../../shared/data-access/interface/product.interface';
/**
* ProductListComponent - Demo Constructor Injection
*
* Inject dependencies:
* - ProductService: để load danh sách sản phẩm
* - CartService: để thêm sản phẩm vào giỏ hàng
*
* Lưu ý về DI:
* - Angular tự động inject instances thông qua constructor
* - Không cần khởi tạo manually với 'new'
* - Services được share giữa các components (singleton)
*/
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit, OnDestroy {
products: ProductModel[] = [];
loading = true;
cartCount = 0;
cartTotal = 0;
addingToCart: { [productId: number]: boolean } = {};
private destroy$ = new Subject<void>();
/**
* Constructor Injection
* Angular DI container tự động inject các dependencies
*
* @param productService - Service để quản lý sản phẩm
* @param cartService - Service để quản lý giỏ hàng
*/
constructor(
private productService: ProductService,
private cartService: CartService
) {
console.log('🎯 ProductListComponent constructor called');
console.log('📦 ProductService injected:', !!this.productService);
console.log('🛒 CartService injected:', !!this.cartService);
}
ngOnInit(): void {
console.log('🚀 ProductListComponent ngOnInit');
// Load products sử dụng injected ProductService
this.loadProducts();
// Subscribe to cart changes sử dụng injected CartService
this.subscribeToCartChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Load danh sách sản phẩm từ ProductService
*/
private loadProducts(): void {
console.log('📦 Loading products using injected ProductService...');
this.productService.getAllProducts()
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (products: ProductModel[]) => {
this.products = products;
this.loading = false;
console.log('✅ Products loaded:', products.length);
},
error: (error: any) => {
console.error('❌ Error loading products:', error);
this.loading = false;
}
});
}
/**
* Subscribe to cart changes từ CartService
*/
private subscribeToCartChanges(): void {
// Subscribe to cart count
this.cartService.cartCount$
.pipe(takeUntil(this.destroy$))
.subscribe((count: number) => {
this.cartCount = count;
console.log('🔄 Cart count updated:', count);
});
// Subscribe to cart items để tính total
this.cartService.cartItems$
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.cartTotal = this.cartService.calculateTotal();
console.log('💰 Cart total updated:', this.cartTotal);
});
}
/**
* Thêm sản phẩm vào giỏ hàng sử dụng injected CartService
*/
addToCart(product: ProductModel): void {
console.log('🛒 Adding product to cart:', product.name);
// Set loading state
this.addingToCart[product.id] = true;
// Simulate some processing time
setTimeout(() => {
// Sử dụng injected CartService
this.cartService.addToCart(product, 1);
// Reset loading state
this.addingToCart[product.id] = false;
console.log('✅ Product added to cart successfully');
}, 500);
}
/**
* Handle image error
*/
onImageError(event: any): void {
event.target.src = 'https://picsum.photos/200/200?random=999';
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProgressBarComponent } from './progress-bar.component';
describe('ProgressBarComponent', () => {
let component: ProgressBarComponent;
let fixture: ComponentFixture<ProgressBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProgressBarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProgressBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-progress-bar',
standalone: true,
imports: [CommonModule],
template: `
<div
class="progress-bar-container"
[style.backgroundColor]="backgroundColor"
>
<div
class="progress"
[style]="{
backgroundColor: progressColor,
width: progress + '%'
}"
></div>
</div>
`,
styles: [
`
.progress-bar-container,
.progress {
height: 20px;
}
.progress-bar-container {
width: 100%;
border-radius: 10px;
overflow: hidden;
}
.progress {
transition: width 0.3s ease;
border-radius: 10px;
}
`,
],
})
export class ProgressBarComponent implements OnInit, OnChanges {
@Input() backgroundColor: string = '#e0e0e0';
@Input() progressColor: string = '#4caf50';
@Input() progress = 0;
constructor() {}
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {
if ('progress' in changes) {
if (typeof changes['progress'].currentValue !== 'number') {
const progress = Number(changes['progress'].currentValue);
if (Number.isNaN(progress)) {
this.progress = 0;
} else {
this.progress = Math.min(Math.max(progress, 0), 100); // Giới hạn từ 0-100
}
}
}
}
}
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-toggle',
standalone: true,
imports: [CommonModule],
template: `
<div
class="toggle-wrapper"
[class.checked]="checked"
tabindex="0"
(click)="toggle()"
(keydown.enter)="toggle()"
(keydown.space)="toggle()"
>
<div class="toggle"></div>
</div>
<div class="toggle-label">
<ng-content></ng-content>
</div>
`,
styles: [`
.toggle-wrapper {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #fe4551;
box-shadow: 0 20px 20px 0 rgba(254, 69, 81, 0.3);
outline: none;
}
.toggle-wrapper:active {
width: 95px;
height: 95px;
box-shadow: 0 15px 15px 0 rgba(254, 69, 81, 0.5);
}
.toggle-wrapper:focus {
outline: 2px solid #007acc;
outline-offset: 2px;
}
.toggle {
transition: all 0.2s ease-in-out;
height: 20px;
width: 20px;
background-color: transparent;
border: 10px solid #fff;
border-radius: 50%;
cursor: pointer;
}
.toggle-wrapper.checked {
background-color: #48e98a;
box-shadow: 0 20px 20px 0 rgba(72, 233, 138, 0.3);
}
.toggle-wrapper.checked:active {
box-shadow: 0 15px 15px 0 rgba(72, 233, 138, 0.5);
}
.toggle-wrapper.checked .toggle {
width: 0;
height: 17px;
background-color: #fff;
border-color: transparent;
border-radius: 30px;
}
`]
})
export class ToggleComponent implements OnInit {
// Input property - nhận giá trị từ parent
@Input() checked = false;
// Output property với suffix "Change" - gửi event về parent
@Output() checkedChange = new EventEmitter<boolean>();
constructor() {}
ngOnInit(): void {
console.log('Toggle component initialized with checked:', this.checked);
}
toggle(): void {
console.log('Toggle clicked! Current state:', this.checked);
// Thay đổi state
this.checked = !this.checked;
// Emit event về parent component (quan trọng cho two-way binding!)
this.checkedChange.emit(this.checked);
console.log('New state:', this.checked, 'Event emitted!');
}
}
/* Test Tailwind CSS */
.test-tailwind {
@apply tw-bg-blue-500 tw-text-white tw-p-4;
}
.test-button {
@apply tw-bg-red-600 hover:tw-bg-red-700 tw-text-white tw-px-4 tw-py-2 tw-rounded;
}
......@@ -28,6 +28,105 @@ module.exports = {
colors: {
white: "#ffffff",
black: "#000000",
// Thêm các màu cơ bản cho Tailwind CSS
transparent: 'transparent',
current: 'currentColor',
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
},
red: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
blue: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
green: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
yellow: {
50: '#fefce8',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
purple: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7c3aed',
800: '#6b21a8',
900: '#581c87',
},
pink: {
50: '#fdf2f8',
100: '#fce7f3',
200: '#fbcfe8',
300: '#f9a8d4',
400: '#f472b6',
500: '#ec4899',
600: '#db2777',
700: '#be185d',
800: '#9d174d',
900: '#831843',
},
indigo: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
},
},
fontFamily: {
"roboto-black": ["Roboto-Black", "sans-serif"],
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment