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 source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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: {},
},
};
<div>
<p class="tw-text-white tw-bg-black tw-p-5">Welcome to the home page!</p>
<div class="tw-p-8 tw-space-y-12 tw-bg-gray-50 tw-min-h-screen">
<!-- Progress Bar Demo Section -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">📊 Progress Bar Demo (Day 7)</h1>
<div class="tw-space-y-4">
<div>
<h3 class="tw-text-lg tw-font-semibold tw-text-gray-700 tw-mb-2">Upload Progress ({{ uploadProgress }}%)</h3>
<app-progress-bar
[progress]="uploadProgress"
[backgroundColor]="'#e0e0e0'"
[progressColor]="'#2196f3'"
></app-progress-bar>
</div>
<div>
<h3 class="tw-text-lg tw-font-semibold tw-text-gray-700 tw-mb-2">Video Progress ({{ videoProgress }}%)</h3>
<app-progress-bar
[progress]="videoProgress"
[backgroundColor]="'#f5f5f5'"
[progressColor]="'#ff9800'"
></app-progress-bar>
</div>
<div>
<h3 class="tw-text-lg tw-font-semibold tw-text-gray-700 tw-mb-2">Download Progress ({{ downloadProgress }}%)</h3>
<app-progress-bar
[progress]="downloadProgress"
[backgroundColor]="'#eeeeee'"
[progressColor]="'#4caf50'"
></app-progress-bar>
</div>
</div>
<div class="tw-mt-6">
<h3 class="tw-text-lg tw-font-semibold tw-text-gray-700 tw-mb-2">Test voi cac gia tri khac nhau:</h3>
<div class="tw-space-y-2">
<app-progress-bar [progress]="15" [progressColor]="'#e91e63'"></app-progress-bar>
<app-progress-bar [progress]="45" [progressColor]="'#9c27b0'"></app-progress-bar>
<app-progress-bar [progress]="90" [progressColor]="'#00bcd4'"></app-progress-bar>
<app-progress-bar [progress]="100" [progressColor]="'#4caf50'"></app-progress-bar>
</div>
</div>
</section>
<!-- Author List Demo Section -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">👥 Component Interaction Demo (Day 8)</h1>
<p class="tw-text-gray-600 tw-mb-6">Demo Output va EventEmitter - Child component gui event len Parent component khi click nut Delete</p>
<app-author-list></app-author-list>
</section>
<!-- Two-way Binding Demo Section -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">🔄 Custom Two-way Binding Demo (Day 9)</h1>
<p class="tw-text-gray-600 tw-mb-6">Demo Two-way binding với Toggle component và ngModel</p>
<!-- ngModel Demo -->
<div class="tw-mb-8 tw-p-4 tw-bg-blue-50 tw-rounded-lg tw-border tw-border-blue-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-blue-800 tw-mb-4">📝 ngModel Two-way Binding</h3>
<div class="tw-space-y-4">
<div>
<label class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">Your name:</label>
<input
type="text"
[(ngModel)]="userName"
class="tw-w-full tw-px-3 tw-py-2 tw-border tw-border-gray-300 tw-rounded-md tw-focus:outline-none tw-focus:ring-2 tw-focus:ring-blue-500"
placeholder="Enter your name"
/>
</div>
<div class="tw-p-3 tw-bg-white tw-rounded tw-border-l-4 tw-border-blue-400">
<strong class="tw-text-blue-800">Hello, {{ userName }}!</strong>
</div>
</div>
</div>
<!-- Custom Toggle Two-way Binding Demo -->
<div class="tw-mb-8 tw-p-4 tw-bg-green-50 tw-rounded-lg tw-border tw-border-green-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-green-800 tw-mb-4">🎯 Custom Toggle Two-way Binding</h3>
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-3 tw-gap-6">
<!-- Toggle 1 -->
<div class="tw-text-center tw-p-4 tw-bg-white tw-rounded-lg tw-shadow">
<h4 class="tw-font-medium tw-text-gray-700 tw-mb-3">Toggle 1</h4>
<app-toggle [(checked)]="toggleState1"></app-toggle>
<p class="tw-mt-3 tw-text-sm tw-font-mono tw-bg-gray-100 tw-px-2 tw-py-1 tw-rounded">
State: {{ toggleState1 ? 'ON' : 'OFF' }}
</p>
<button
(click)="toggleState1 = !toggleState1"
class="tw-mt-2 tw-text-xs tw-bg-gray-200 hover:tw-bg-gray-300 tw-px-3 tw-py-1 tw-rounded tw-transition-colors"
>
Toggle from Parent
</button>
</div>
<!-- Toggle 2 -->
<div class="tw-text-center tw-p-4 tw-bg-white tw-rounded-lg tw-shadow">
<h4 class="tw-font-medium tw-text-gray-700 tw-mb-3">Toggle 2</h4>
<app-toggle [(checked)]="toggleState2"></app-toggle>
<p class="tw-mt-3 tw-text-sm tw-font-mono tw-bg-gray-100 tw-px-2 tw-py-1 tw-rounded">
State: {{ toggleState2 ? 'ON' : 'OFF' }}
</p>
<button
(click)="toggleState2 = !toggleState2"
class="tw-mt-2 tw-text-xs tw-bg-gray-200 hover:tw-bg-gray-300 tw-px-3 tw-py-1 tw-rounded tw-transition-colors"
>
Toggle from Parent
</button>
</div>
<!-- Toggle 3 -->
<div class="tw-text-center tw-p-4 tw-bg-white tw-rounded-lg tw-shadow">
<h4 class="tw-font-medium tw-text-gray-700 tw-mb-3">Toggle 3</h4>
<app-toggle [(checked)]="toggleState3"></app-toggle>
<p class="tw-mt-3 tw-text-sm tw-font-mono tw-bg-gray-100 tw-px-2 tw-py-1 tw-rounded">
State: {{ toggleState3 ? 'ON' : 'OFF' }}
</p>
<button
(click)="toggleState3 = !toggleState3"
class="tw-mt-2 tw-text-xs tw-bg-gray-200 hover:tw-bg-gray-300 tw-px-3 tw-py-1 tw-rounded tw-transition-colors"
>
Toggle from Parent
</button>
</div>
</div>
</div>
<!-- Explanation Section -->
<div class="tw-p-4 tw-bg-yellow-50 tw-rounded-lg tw-border tw-border-yellow-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-yellow-800 tw-mb-3">💡 Cách hoạt động:</h3>
<div class="tw-space-y-2 tw-text-sm tw-text-yellow-700">
<p><strong>1. Two-way binding syntax:</strong> <code class="tw-bg-yellow-200 tw-px-1 tw-rounded">[(checked)]="toggleState1"</code></p>
<p><strong>2. Tương đương với:</strong> <code class="tw-bg-yellow-200 tw-px-1 tw-rounded">[checked]="toggleState1" (checkedChange)="toggleState1 = $event"</code></p>
<p><strong>3. Component can:</strong> Input() checked va Output() checkedChange</p>
<p><strong>4. Test:</strong> Click toggle hoặc button "Toggle from Parent" để xem sync</p>
</div>
</div>
</section>
<!-- Content Projection Demo Section -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">🎯 Content Projection Demo (Day 13)</h1>
<p class="tw-text-gray-600 tw-mb-6">Demo ng-content với các loại selector: attribute, class, tag, và ngProjectAs</p>
<!-- Basic ng-content demo với Toggle -->
<div class="tw-mb-8 tw-p-4 tw-bg-blue-50 tw-rounded-lg tw-border tw-border-blue-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-blue-800 tw-mb-4">1. Basic ng-content (Single Slot)</h3>
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-3 tw-gap-4">
<div class="tw-text-center">
<app-toggle [(checked)]="surveyAnswers.question1">
<span class="tw-text-sm tw-font-medium">Do you like Angular?</span>
</app-toggle>
</div>
<div class="tw-text-center">
<app-toggle [(checked)]="surveyAnswers.question2">
<strong class="tw-text-purple-600">Do you use TypeScript?</strong>
</app-toggle>
</div>
<div class="tw-text-center">
<app-toggle [(checked)]="surveyAnswers.question3">
<em class="tw-text-green-600">Do you want to learn more?</em>
</app-toggle>
</div>
</div>
<div class="tw-mt-4 tw-p-3 tw-bg-white tw-rounded tw-border-l-4 tw-border-blue-400">
<p class="tw-text-sm tw-text-gray-600">
<strong>Code:</strong> <code>&lt;ng-content&gt;&lt;/ng-content&gt;</code> - Nhận tất cả content không có selector
</p>
</div>
</div>
<!-- Multi-slot ng-content demo dengan Card -->
<div class="tw-mb-8 tw-p-4 tw-bg-green-50 tw-rounded-lg tw-border tw-border-green-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-green-800 tw-mb-4">2. Multi-slot ng-content (Multiple Selectors)</h3>
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-4">
<!-- Card 1: Attribute selector -->
<app-card>
<h3 slot="header">📝 Survey Card</h3>
<p>Please answer the following questions about your Angular experience.</p>
<p>Your responses help us improve our content.</p>
<div class="card-footer-content">
<small>Last updated: Today</small>
</div>
<div class="card-actions">
<button
class="tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors tw-mr-2 tw-text-sm"
(click)="openModal()">
View Details
</button>
<button
class="tw-bg-gray-500 hover:tw-bg-gray-600 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors tw-text-sm"
(click)="resetSurvey()">
Reset
</button>
</div>
</app-card>
<!-- Card 2: CSS Class selector -->
<app-card>
<div slot="header">⭐ Results Card</div>
<div>
<p><strong>Question 1:</strong> {{ surveyAnswers.question1 ? '✅ Yes' : '❌ No' }}</p>
<p><strong>Question 2:</strong> {{ surveyAnswers.question2 ? '✅ Yes' : '❌ No' }}</p>
<p><strong>Question 3:</strong> {{ surveyAnswers.question3 ? '✅ Yes' : '❌ No' }}</p>
</div>
<div class="card-footer-content">
<small>Total Yes: {{ (surveyAnswers.question1 ? 1 : 0) + (surveyAnswers.question2 ? 1 : 0) + (surveyAnswers.question3 ? 1 : 0) }}/3</small>
</div>
<div class="card-actions">
<button
class="tw-bg-green-600 hover:tw-bg-green-700 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors tw-text-sm"
(click)="onSave()">
Save Results
</button>
</div>
</app-card>
</div>
<div class="tw-mt-4 tw-p-3 tw-bg-white tw-rounded tw-border-l-4 tw-border-green-400">
<p class="tw-text-sm tw-text-gray-600">
<strong>Selectors:</strong><br>
<code>select="[slot=header]"</code> - Attribute selector<br>
<code>select=".card-footer-content"</code> - CSS Class selector<br>
<code>select=".card-actions"</code> - Class selector cho actions<br>
<code>select="button"</code> - Tag selector cho buttons<br>
<code>&lt;ng-content&gt;</code> - Default slot (không có selector)
</p>
</div>
<!-- DEBUG CARD - Simple test -->
<div class="tw-mt-4">
<h4 class="tw-text-md tw-font-semibold tw-text-gray-700 tw-mb-2">🐛 Debug Card - Simple Test:</h4>
<app-card>
<h3 slot="header">Debug Header</h3>
<p>This is body content</p>
<div class="card-footer-content">Footer content here</div>
<button class="tw-bg-red-500 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">Direct Button</button>
<div class="card-actions">
<button class="tw-bg-blue-500 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">Button in Div</button>
</div>
</app-card>
</div>
</div>
<!-- ngProjectAs demo dengan Modal -->
<div class="tw-mb-8 tw-p-4 tw-bg-purple-50 tw-rounded-lg tw-border tw-border-purple-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-purple-800 tw-mb-4">3. ngProjectAs Demo</h3>
<p class="tw-text-sm tw-text-gray-600 tw-mb-4">
Sử dụng <code>ngProjectAs</code> để project content vào đúng slot ngay cả khi element không match selector trực tiếp.
</p>
<button
(click)="openModal()"
class="tw-bg-purple-600 hover:tw-bg-purple-700 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors"
>
Open Survey Modal
</button>
</div>
<!-- Modal với ngProjectAs -->
<app-modal [isOpen]="isModalOpen" (closeEvent)="closeModal()">
<!-- Sử dụng ngProjectAs để project vào modal-header slot -->
<div ngProjectAs="modal-header">
<h2 class="tw-text-xl tw-font-bold tw-text-gray-800">📊 Survey Details</h2>
</div>
<!-- Sử dụng ngProjectAs để project vào modal-body slot -->
<div ngProjectAs="modal-body">
<p class="tw-mb-4">Here are your current survey responses:</p>
<div class="tw-space-y-2">
<div class="tw-flex tw-justify-between tw-p-2 tw-bg-gray-50 tw-rounded">
<span>Do you like Angular?</span>
<span class="tw-font-semibold">{{ surveyAnswers.question1 ? '✅ Yes' : '❌ No' }}</span>
</div>
<div class="tw-flex tw-justify-between tw-p-2 tw-bg-gray-50 tw-rounded">
<span>Do you use TypeScript?</span>
<span class="tw-font-semibold">{{ surveyAnswers.question2 ? '✅ Yes' : '❌ No' }}</span>
</div>
<div class="tw-flex tw-justify-between tw-p-2 tw-bg-gray-50 tw-rounded">
<span>Do you want to learn more?</span>
<span class="tw-font-semibold">{{ surveyAnswers.question3 ? '✅ Yes' : '❌ No' }}</span>
</div>
</div>
<p class="tw-mt-4 tw-text-sm tw-text-gray-600">
<strong>Note:</strong> Elements trên sử dụng <code>ngProjectAs="modal-header"</code>
<code>ngProjectAs="modal-body"</code> để project vào đúng slots.
</p>
</div>
<!-- Sử dụng ngProjectAs để project vào modal-footer slot -->
<div ngProjectAs="modal-footer">
<button
class="tw-bg-gray-500 hover:tw-bg-gray-600 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors tw-mr-2"
(click)="closeModal()">
Close
</button>
<button
class="tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors"
(click)="onSave()">
Save & Close
</button>
</div>
</app-modal>
<!-- Explanation Section -->
<div class="tw-p-4 tw-bg-yellow-50 tw-rounded-lg tw-border tw-border-yellow-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-yellow-800 tw-mb-3">💡 Content Projection Summary:</h3>
<div class="tw-space-y-2 tw-text-sm tw-text-yellow-700">
<p><strong>1. Single Slot:</strong> <code>&lt;ng-content&gt;&lt;/ng-content&gt;</code> - Nhận tất cả content</p>
<p><strong>2. Attribute Selector:</strong> <code>&lt;ng-content select="[slot=header]"&gt;</code></p>
<p><strong>3. Class Selector:</strong> <code>&lt;ng-content select=".card-footer-content"&gt;</code></p>
<p><strong>4. Tag Selector:</strong> <code>&lt;ng-content select="button"&gt;</code></p>
<p><strong>5. ngProjectAs:</strong> <code>&lt;div ngProjectAs="modal-header"&gt;</code> - Project vào slot cụ thể</p>
<p><strong>6. Multiple ng-content:</strong> Mỗi selector sẽ nhận content phù hợp</p>
</div>
</div>
</section>
<!-- DAY 14: ng-template, ngTemplateOutlet và ng-container Demo -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">🎨 Day 14: ng-template, ngTemplateOutlet & ng-container</h1>
<p class="tw-text-gray-600 tw-mb-6">Demo các khái niệm về template rendering và reusable templates</p>
<!-- 1. ng-template với *ngIf else -->
<div class="tw-mb-8 tw-p-4 tw-bg-blue-50 tw-rounded-lg tw-border tw-border-blue-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-blue-800 tw-mb-4">1. ng-template với *ngIf else (Review)</h3>
<div class="tw-mb-4">
<label class="tw-flex tw-items-center tw-space-x-2">
<input
type="checkbox"
[(ngModel)]="userAge18"
class="tw-form-checkbox tw-h-4 tw-w-4 tw-text-blue-600">
<span>User is 18+ years old</span>
</label>
</div>
<div class="tw-p-4 tw-bg-white tw-rounded tw-border">
<div *ngIf="userAge18; else underage">
✅ Bạn có thể xem nội dung dành cho người lớn
</div>
<ng-template #underage>
❌ Bạn chưa đủ tuổi để xem nội dung này
</ng-template>
</div>
<div class="tw-mt-2 tw-text-sm tw-text-gray-600">
<code>ng-template</code> chỉ render khi được gọi (ở đây là khi *ngIf else)
</div>
</div>
<!-- 2. Reusable Template với ngTemplateOutlet -->
<div class="tw-mb-8 tw-p-4 tw-bg-green-50 tw-rounded-lg tw-border tw-border-green-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-green-800 tw-mb-4">2. Reusable Template với ngTemplateOutlet</h3>
<!-- Demo counter template reuse -->
<div class="tw-space-y-4">
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<div class="tw-mb-2 tw-font-medium">Header Section:</div>
You have selected <ng-container [ngTemplateOutlet]="counterTemplate"></ng-container> items.
</div>
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<div class="tw-mb-2 tw-font-medium">Body Section:</div>
There are <ng-container [ngTemplateOutlet]="counterTemplate"></ng-container> items in your cart.
</div>
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<div class="tw-mb-2 tw-font-medium">Footer Section:</div>
Total selected: <ng-container [ngTemplateOutlet]="counterTemplate"></ng-container> items.
</div>
<!-- Controls -->
<div class="tw-flex tw-space-x-2">
<button
(click)="itemCounter = itemCounter + 1"
class="tw-bg-green-600 hover:tw-bg-green-700 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">
Add Item (+)
</button>
<button
(click)="itemCounter = Math.max(0, itemCounter - 1)"
class="tw-bg-red-600 hover:tw-bg-red-700 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">
Remove Item (-)
</button>
<button
(click)="itemCounter = 0"
class="tw-bg-gray-600 hover:tw-bg-gray-700 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">
Reset
</button>
</div>
</div>
<!-- Template definition -->
<ng-template #counterTemplate>
<span class="tw-inline-flex tw-items-center tw-px-2.5 tw-py-0.5 tw-rounded-full tw-text-xs tw-font-medium tw-bg-blue-100 tw-text-blue-800">
{{ itemCounter }}
<span class="tw-ml-1">📦</span>
</span>
</ng-template>
<div class="tw-mt-4 tw-text-sm tw-text-gray-600">
✨ Template <code>#counterTemplate</code> được tái sử dụng 3 lần thay vì copy-paste code
</div>
</div>
<!-- 3. ngTemplateOutlet với Context (truyền data) -->
<div class="tw-mb-8 tw-p-4 tw-bg-purple-50 tw-rounded-lg tw-border tw-border-purple-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-purple-800 tw-mb-4">3. ngTemplateOutlet với Context (truyền data)</h3>
<div class="tw-space-y-4">
<!-- Reusable button template với data -->
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<h4 class="tw-font-medium tw-mb-3">Reusable Button Template:</h4>
<div class="tw-space-x-2 tw-space-y-2">
<!-- Button 1: Primary với icon -->
<ng-container
[ngTemplateOutlet]="buttonTemplate"
[ngTemplateOutletContext]="{
label: 'Save Document',
className: 'tw-bg-blue-600 hover:tw-bg-blue-700',
icon: '💾',
onClick: saveDocument
}">
</ng-container>
<!-- Button 2: Danger với icon -->
<ng-container
[ngTemplateOutlet]="buttonTemplate"
[ngTemplateOutletContext]="{
label: 'Delete File',
className: 'tw-bg-red-600 hover:tw-bg-red-700',
icon: '🗑️',
onClick: deleteFile
}">
</ng-container>
<!-- Button 3: Success không icon -->
<ng-container
[ngTemplateOutlet]="buttonTemplate"
[ngTemplateOutletContext]="{
label: 'Complete Task',
className: 'tw-bg-green-600 hover:tw-bg-green-700',
icon: null,
onClick: completeTask
}">
</ng-container>
<!-- Button 4: Warning với implicit data -->
<ng-container
[ngTemplateOutlet]="buttonTemplate"
[ngTemplateOutletContext]="{
$implicit: 'Cancel Process',
className: 'tw-bg-yellow-600 hover:tw-bg-yellow-700',
icon: '⚠️',
onClick: cancelProcess
}">
</ng-container>
</div>
</div>
<!-- Action log -->
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<h4 class="tw-font-medium tw-mb-2">Action Log:</h4>
<div class="tw-max-h-32 tw-overflow-y-auto tw-bg-gray-50 tw-p-2 tw-rounded tw-text-sm">
<div *ngFor="let action of actionLog; trackBy: trackByIndex" class="tw-mb-1">
<span class="tw-text-gray-500">{{ action.time }}</span> - {{ action.message }}
</div>
<div *ngIf="actionLog.length === 0" class="tw-text-gray-400 tw-italic">
No actions yet. Click a button above!
</div>
</div>
<button
(click)="clearActionLog()"
class="tw-mt-2 tw-bg-gray-400 hover:tw-bg-gray-500 tw-text-white tw-px-2 tw-py-1 tw-rounded tw-text-xs">
Clear Log
</button>
</div>
</div>
<!-- Button template definition với context -->
<ng-template
#buttonTemplate
let-label="label"
let-className="className"
let-icon="icon"
let-onClick="onClick"
let-implicitLabel>
<button
[class]="'tw-text-white tw-px-4 tw-py-2 tw-rounded tw-transition-colors tw-text-sm tw-inline-flex tw-items-center tw-space-x-2 ' + className"
(click)="onClick && onClick()">
<span *ngIf="icon">{{ icon }}</span>
<span>{{ label || implicitLabel }}</span>
</button>
</ng-template>
<div class="tw-mt-4 tw-text-sm tw-text-gray-600">
✨ Template nhận data qua <code>ngTemplateOutletContext</code>:<br>
<code>let-label="label"</code> - bind context.label vào variable label<br>
<code>let-implicitLabel</code> - bind context.$implicit vào variable implicitLabel<br>
<code>let-onClick="onClick"</code> - truyền function callback
</div>
</div>
<!-- 4. ng-container vs div (không tạo extra DOM) -->
<div class="tw-mb-8 tw-p-4 tw-bg-orange-50 tw-rounded-lg tw-border tw-border-orange-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-orange-800 tw-mb-4">4. ng-container vs div (Demo DOM structure)</h3>
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-4">
<!-- Với div (tạo extra DOM) -->
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<h4 class="tw-font-medium tw-mb-2 tw-text-red-600">❌ Với div (tạo extra DOM):</h4>
<div class="tw-bg-gray-100 tw-p-2 tw-rounded tw-text-sm tw-font-mono">
<div class="tw-border tw-border-red-200 tw-bg-red-50 tw-p-2">
<div [ngTemplateOutlet]="statusTemplate"
[ngTemplateOutletContext]="{ status: 'success', message: 'Operation completed' }"></div>
</div>
</div>
<div class="tw-mt-2 tw-text-xs tw-text-gray-600">
Inspect element để thấy extra &lt;div&gt; bao quanh
</div>
</div>
<!-- Với ng-container (không tạo extra DOM) -->
<div class="tw-bg-white tw-rounded tw-p-4 tw-border">
<h4 class="tw-font-medium tw-mb-2 tw-text-green-600">✅ Với ng-container (clean DOM):</h4>
<div class="tw-bg-gray-100 tw-p-2 tw-rounded tw-text-sm tw-font-mono">
<div class="tw-border tw-border-green-200 tw-bg-green-50 tw-p-2">
<ng-container [ngTemplateOutlet]="statusTemplate"
[ngTemplateOutletContext]="{ status: 'warning', message: 'Please check input' }"></ng-container>
</div>
</div>
<div class="tw-mt-2 tw-text-xs tw-text-gray-600">
Inspect element - không có extra wrapper element
</div>
</div>
</div>
<!-- Status template -->
<ng-template #statusTemplate let-status="status" let-message="message">
<div class="tw-flex tw-items-center tw-space-x-2">
<span [ngSwitch]="status">
<span *ngSwitchCase="'success'"></span>
<span *ngSwitchCase="'warning'">⚠️</span>
<span *ngSwitchCase="'error'"></span>
<span *ngSwitchDefault>ℹ️</span>
</span>
<span class="tw-font-medium">{{ message }}</span>
</div>
</ng-template>
<div class="tw-mt-4 tw-text-sm tw-text-gray-600">
<code>ng-container</code> không tạo DOM element, giúp HTML clean hơn và tránh ảnh hưởng CSS
</div>
</div>
<!-- 5. Template as Component Input (Advanced) -->
<div class="tw-mb-8 tw-p-4 tw-bg-pink-50 tw-rounded-lg tw-border tw-border-pink-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-pink-800 tw-mb-4">5. Template as Component Input (Advanced)</h3>
<p class="tw-text-sm tw-text-gray-600 tw-mb-4">
Truyền template từ parent component vào child component để customize giao diện
</p>
<!-- Sử dụng Card với custom template -->
<div class="tw-space-y-4">
<!-- Card với default template -->
<app-card>
<h3 slot="header">🎨 Default Card</h3>
<p>This card uses the default styling and layout defined in the Card component.</p>
<div class="card-footer-content">
<small>Default footer styling</small>
</div>
<div class="card-actions">
<button class="tw-bg-blue-600 hover:tw-bg-blue-700 tw-text-white tw-px-3 tw-py-1 tw-rounded tw-text-sm">Default Button</button>
</div>
</app-card>
<!-- Card với custom template trong content -->
<app-card>
<div slot="header">
<ng-container [ngTemplateOutlet]="customHeaderTemplate"></ng-container>
</div>
<ng-container [ngTemplateOutlet]="customBodyTemplate"
[ngTemplateOutletContext]="{ data: customCardData }"></ng-container>
<div class="card-footer-content">
<ng-container [ngTemplateOutlet]="customFooterTemplate"></ng-container>
</div>
<div class="card-actions">
<ng-container [ngTemplateOutlet]="customActionsTemplate"
[ngTemplateOutletContext]="{ actions: cardActions }"></ng-container>
</div>
</app-card>
</div>
<!-- Custom templates definition -->
<ng-template #customHeaderTemplate>
<div class="tw-flex tw-items-center tw-space-x-2">
<span class="tw-text-2xl">🚀</span>
<div>
<h3 class="tw-font-bold tw-text-purple-700">Custom Styled Card</h3>
<p class="tw-text-xs tw-text-purple-500">với template được customize</p>
</div>
</div>
</ng-template>
<ng-template #customBodyTemplate let-data="data">
<div class="tw-bg-gradient-to-r tw-from-purple-100 tw-to-pink-100 tw-p-4 tw-rounded">
<h4 class="tw-font-semibold tw-text-purple-800 tw-mb-2">{{ data.title }}</h4>
<p class="tw-text-purple-700 tw-mb-3">{{ data.description }}</p>
<div class="tw-flex tw-flex-wrap tw-gap-2">
<span *ngFor="let tag of data.tags"
class="tw-px-2 tw-py-1 tw-bg-purple-200 tw-text-purple-800 tw-rounded-full tw-text-xs">
#{{ tag }}
</span>
</div>
</div>
</ng-template>
<ng-template #customFooterTemplate>
<div class="tw-flex tw-items-center tw-justify-between tw-text-xs">
<span class="tw-text-purple-600">⚡ Powered by ng-template</span>
<span class="tw-text-gray-500">{{ getCurrentTime() }}</span>
</div>
</ng-template>
<ng-template #customActionsTemplate let-actions="actions">
<div class="tw-flex tw-space-x-2">
<button *ngFor="let action of actions"
[class]="'tw-px-3 tw-py-1 tw-rounded tw-text-sm tw-transition-colors ' + action.className"
(click)="action.handler()">
{{ action.icon }} {{ action.label }}
</button>
</div>
</ng-template>
<div class="tw-mt-4 tw-text-sm tw-text-gray-600">
✨ Templates có thể nhận data và được tái sử dụng với context khác nhau
</div>
</div>
<!-- Summary và Best Practices -->
<div class="tw-p-4 tw-bg-yellow-50 tw-rounded-lg tw-border tw-border-yellow-200">
<h3 class="tw-text-lg tw-font-semibold tw-text-yellow-800 tw-mb-3">💡 Day 14 Summary</h3>
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-3 tw-gap-4 tw-text-sm">
<div>
<h4 class="tw-font-semibold tw-text-yellow-700 tw-mb-2">ng-template</h4>
<ul class="tw-space-y-1 tw-text-yellow-600">
<li>• Định nghĩa template không render trực tiếp</li>
<li>• Sử dụng với structural directives</li>
<li>• Tái sử dụng code HTML</li>
<li>• Có thể nhận data qua context</li>
<li>• Reference bằng #templateName</li>
</ul>
</div>
<div>
<h4 class="tw-font-semibold tw-text-yellow-700 tw-mb-2">ngTemplateOutlet</h4>
<ul class="tw-space-y-1 tw-text-yellow-600">
<li>• Render ng-template đã định nghĩa</li>
<li>• Cú pháp: [ngTemplateOutlet]="ref"</li>
<li>• Truyền data qua ngTemplateOutletContext</li>
<li>• Hỗ trợ $implicit cho default value</li>
<li>• Cho phép template reuse</li>
</ul>
</div>
<div>
<h4 class="tw-font-semibold tw-text-yellow-700 tw-mb-2">ng-container</h4>
<ul class="tw-space-y-1 tw-text-yellow-600">
<li>• Container không tạo DOM element</li>
<li>• Tránh wrapper element thừa</li>
<li>• Không ảnh hưởng CSS styling</li>
<li>• Sử dụng với structural directives</li>
<li>• Clean HTML output</li>
</ul>
</div>
</div>
<div class="tw-mt-4 tw-p-3 tw-bg-yellow-100 tw-rounded">
<h4 class="tw-font-semibold tw-text-yellow-800 tw-mb-2">🎯 Best Practices:</h4>
<ul class="tw-text-sm tw-text-yellow-700 tw-space-y-1">
<li><strong>1. Reusability:</strong> Dùng ng-template cho code HTML lặp lại trong component</li>
<li><strong>2. Clean DOM:</strong> Dùng ng-container thay vì div khi không cần wrapper element</li>
<li><strong>3. Type Safety:</strong> Cẩn thận với context data vì không có type checking</li>
<li><strong>4. Performance:</strong> Templates chỉ render khi được gọi, giúp optimize performance</li>
<li><strong>5. Flexibility:</strong> Cho phép parent component customize child component template</li>
</ul>
</div>
</div>
</section>
<!-- Dependency Injection Demo Section -->
<section class="tw-bg-white tw-rounded-lg tw-shadow-lg tw-p-6">
<h1 class="tw-text-3xl tw-font-bold tw-text-gray-800 tw-mb-6">🔧 Dependency Injection Demo (Day 15)</h1>
<p class="tw-text-gray-600 tw-mb-6">
Demo Constructor Injection, Service Sharing, Provider Override và DI Concepts
</p>
<!-- DI Comparison Component -->
<div class="tw-mb-8">
<app-di-comparison></app-di-comparison>
</div>
<!-- Shopping Cart Demo -->
<div class="tw-grid tw-grid-cols-1 lg:tw-grid-cols-2 tw-gap-8">
<!-- Product List -->
<div class="tw-bg-gray-50 tw-rounded-lg tw-p-4">
<h3 class="tw-text-xl tw-font-semibold tw-mb-4">🛍️ Product List (với DI)</h3>
<app-product-list></app-product-list>
</div>
<!-- Cart View -->
<div class="tw-bg-gray-50 tw-rounded-lg tw-p-4">
<h3 class="tw-text-xl tw-font-semibold tw-mb-4">🛒 Cart View (Service Sharing)</h3>
<app-cart-view></app-cart-view>
</div>
</div>
<!-- DI Benefits Summary -->
<div class="tw-mt-8 tw-p-4 tw-bg-blue-50 tw-border-l-4 tw-border-blue-400 tw-rounded-r-lg">
<h4 class="tw-font-semibold tw-text-lg tw-mb-2">🎯 DI Demo Summary</h4>
<div class="tw-text-sm tw-text-gray-700 tw-space-y-1">
<p><strong>Constructor Injection:</strong> ProductService và CartService được inject vào components</p>
<p><strong>Service Sharing:</strong> Cùng CartService instance được share giữa ProductList và CartView</p>
<p><strong>Loose Coupling:</strong> Components không phụ thuộc vào concrete implementations</p>
<p><strong>Testability:</strong> Dễ dàng mock services cho unit testing</p>
<p><strong>Maintainability:</strong> Có thể thay đổi implementation mà không ảnh hưởng components</p>
</div>
</div>
</section>
</div>
\ No newline at end of file
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 source diff could not be displayed because it is too large. You can view the blob instead.
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