# 25 คำถามสัมภาษณ์ Angular ยอดนิยม: คู่มือฉบับสมบูรณ์สู่ความสำเร็จ > 25 คำถามสัมภาษณ์ Angular ที่ถูกถามมากที่สุดในปี 2026 พร้อมคำตอบโดยละเอียด ตัวอย่างโค้ด และเคล็ดลับเพื่อคว้าตำแหน่งนักพัฒนา Angular - Published: 2026-02-04 - Updated: 2026-04-27 - Author: SharpSkill - Tags: angular interview, frontend interview, angular questions, typescript, technical interview - Reading time: 16 min --- การสัมภาษณ์ทางเทคนิคของ Angular จะประเมินความเข้าใจสถาปัตยกรรมของเฟรมเวิร์ก ความเชี่ยวชาญใน TypeScript และแนวปฏิบัติที่ดีในการพัฒนา frontend คู่มือนี้นำเสนอ 25 คำถามที่ถูกถามมากที่สุดพร้อมคำตอบโดยละเอียดและตัวอย่างโค้ดเพื่อการเตรียมตัวที่ดีที่สุด > **เคล็ดลับการเตรียมตัว** > > คำถามเหล่านี้ครอบคลุม Angular เวอร์ชันใหม่ (16+) รวมถึง Signals คอมโพเนนต์ standalone และ control flow รูปแบบใหม่ การเข้าใจแนวคิดสมัยใหม่เหล่านี้แสดงให้เห็นถึงการติดตามเทคโนโลยีอย่างต่อเนื่อง ## พื้นฐานของ Angular ### 1. Angular กับ AngularJS แตกต่างกันอย่างไร? Angular (เวอร์ชัน 2 ขึ้นไป) คือการเขียนใหม่ทั้งหมดของ AngularJS ความแตกต่างหลักอยู่ที่สถาปัตยกรรม ภาษา และประสิทธิภาพ AngularJS ใช้ JavaScript และรูปแบบ MVC ร่วมกับระบบ two-way binding ซึ่งอาจทำให้เกิดปัญหาด้านประสิทธิภาพ Angular ใช้ TypeScript สถาปัตยกรรมที่ยึดตามคอมโพเนนต์ และระบบ change detection ที่ได้รับการปรับแต่งแล้ว ```typescript // AngularJS (1.x) - Controller-based // angular.module('app').controller('UserController', function($scope) { // $scope.user = { name: 'Alice' }; // }); // Angular (2+) - Component-based // user.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-user', standalone: true, template: `

{{ user.name }}

{{ user.email }}

` }) export class UserComponent { // Strong typing with TypeScript user = { name: 'Alice', email: 'alice@example.com' }; } ``` Angular ยังรองรับโมดูล ES6 อย่างเป็นมาตรฐาน มาพร้อม tooling ที่ดีกว่าผ่าน Angular CLI และสถาปัตยกรรมที่เหมาะกับแอปพลิเคชันระดับองค์กรมากขึ้น ### 2. คอมโพเนนต์ของ Angular คืออะไร และสร้างอย่างไร? คอมโพเนนต์ของ Angular คือคลาส TypeScript ที่ตกแต่งด้วย `@Component` มันห่อหุ้มลอจิก เทมเพลต และสไตล์ของอินเทอร์เฟซผู้ใช้ส่วนหนึ่ง ```typescript // product-card.component.ts import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; // Interface for strong typing interface Product { id: number; name: string; price: number; inStock: boolean; } @Component({ // CSS selector to use the component selector: 'app-product-card', // Standalone = no NgModule needed standalone: true, // Dependency imports imports: [CommonModule], // Inline or external template (templateUrl) template: `

{{ product.name }}

{{ product.price | currency:'USD' }}

@if (product.inStock) { } @else { Out of Stock }
`, // Encapsulated styles by default styles: [` .product-card { padding: 1rem; border: 1px solid #e0e0e0; } .price { font-weight: bold; color: #2563eb; } .out-of-stock { color: #dc2626; } `] }) export class ProductCardComponent { // Input: data from parent @Input({ required: true }) product!: Product; // Output: events to parent @Output() addToCart = new EventEmitter(); onAddToCart() { this.addToCart.emit(this.product.id); } } ``` คอมโพเนนต์ standalone (Angular 14+) ทำให้การสร้างง่ายขึ้น โดยไม่จำเป็นต้องประกาศคอมโพเนนต์ใน NgModule อีกต่อไป ### 3. อธิบายวงจรชีวิตของคอมโพเนนต์ Angular Angular มี lifecycle hook ที่ช่วยให้สามารถรันโค้ดในช่วงเวลาเฉพาะของวงจรชีวิตของคอมโพเนนต์ได้ ```typescript // lifecycle-demo.component.ts import { Component, OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy, Input, SimpleChanges } from '@angular/core'; @Component({ selector: 'app-lifecycle-demo', standalone: true, template: `

{{ data }}

` }) export class LifecycleDemoComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { @Input() data = ''; // 1. Called when an @Input changes (before ngOnInit) ngOnChanges(changes: SimpleChanges) { console.log('ngOnChanges', changes); } // 2. Called once after the first ngOnChanges // Ideal for initializations ngOnInit() { console.log('ngOnInit - Component initialization'); } // 3. Called on every change detection cycle // Use with caution (performance) ngDoCheck() { console.log('ngDoCheck'); } // 4. After content projection (ng-content) ngAfterContentInit() { console.log('ngAfterContentInit'); } // 5. After each projected content check ngAfterContentChecked() { console.log('ngAfterContentChecked'); } // 6. After component view initialization // @ViewChild references are available here ngAfterViewInit() { console.log('ngAfterViewInit - View initialized'); } // 7. After each view check ngAfterViewChecked() { console.log('ngAfterViewChecked'); } // 8. Just before component destruction // Cleanup: unsubscribe, clearInterval, etc. ngOnDestroy() { console.log('ngOnDestroy - Cleanup'); } } ``` Hook ที่ใช้บ่อยที่สุดคือ `ngOnInit` สำหรับการเริ่มต้น `ngOnChanges` เพื่อตอบสนองต่อการเปลี่ยนแปลง input และ `ngOnDestroy` สำหรับการคืนทรัพยากร ### 4. Data Binding ใน Angular คืออะไร? Data binding เชื่อมข้อมูลของคอมโพเนนต์กับเทมเพลต Angular มี data binding ทั้งหมด 4 รูปแบบ ```typescript // data-binding.component.ts import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-data-binding', standalone: true, imports: [FormsModule], template: `

{{ title }}

{{ getFullName() }}

Hello, {{ username }}

` }) export class DataBindingComponent { // Properties for interpolation title = 'My Application'; firstName = 'John'; lastName = 'Doe'; // Properties for property binding imageUrl = '/assets/logo.png'; imageAlt = 'Application logo'; isLoading = false; // Property for two-way binding username = ''; getFullName(): string { return `${this.firstName} ${this.lastName}`; } handleClick(): void { console.log('Button clicked'); } onEnter(event: KeyboardEvent): void { const target = event.target as HTMLInputElement; console.log('Entered value:', target.value); } } ``` Two-way binding `[(ngModel)]` คือการรวม property binding และ event binding เข้าด้วยกัน ทำให้โมเดลและ view ซิงโครไนซ์กันโดยอัตโนมัติ ### 5. Module และ Standalone Component แตกต่างกันอย่างไร? NgModules จัดกลุ่มคอมโพเนนต์ directive และ service ที่เกี่ยวข้องกัน ส่วนคอมโพเนนต์ standalone (Angular 14+) ช่วยให้สร้างคอมโพเนนต์ที่เป็นอิสระได้โดยไม่ต้องใช้โมดูล ```typescript // Traditional approach with NgModule // products.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ProductListComponent } from './product-list.component'; import { ProductCardComponent } from './product-card.component'; import { ProductService } from './product.service'; @NgModule({ // Components belonging to this module declarations: [ ProductListComponent, ProductCardComponent ], // Modules needed imports: [CommonModule], // Components usable outside exports: [ProductListComponent], // Services with module scope providers: [ProductService] }) export class ProductsModule {} // Modern approach with Standalone Components // product-list.component.ts import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ProductCardComponent } from './product-card.component'; import { ProductService } from './product.service'; @Component({ selector: 'app-product-list', // No NgModule needed standalone: true, // Direct dependency imports imports: [CommonModule, ProductCardComponent], template: ` @for (product of products(); track product.id) { } ` }) export class ProductListComponent { // Modern injection with inject() private productService = inject(ProductService); products = this.productService.getProducts(); } ``` คอมโพเนนต์ standalone ลดความซับซ้อน ช่วยให้ tree-shaking ดีขึ้น และทำให้ lazy loading ง่ายขึ้น ถือเป็นแนวทางที่แนะนำสำหรับโปรเจกต์ใหม่ ## Service และ Dependency Injection ### 6. Dependency injection ใน Angular ทำงานอย่างไร? Dependency injection (DI) เป็นรูปแบบการออกแบบหลักของ Angular เฟรมเวิร์กจะดูแลการสร้างและส่งมอบอินสแตนซ์ของ service ```typescript // user.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, BehaviorSubject } from 'rxjs'; interface User { id: number; name: string; email: string; } // providedIn: 'root' = singleton at application level @Injectable({ providedIn: 'root' }) export class UserService { // Modern injection with inject() private http = inject(HttpClient); // Shared reactive state private currentUser$ = new BehaviorSubject(null); // Public API getUsers(): Observable { return this.http.get('/api/users'); } getUserById(id: number): Observable { return this.http.get(`/api/users/${id}`); } setCurrentUser(user: User): void { this.currentUser$.next(user); } getCurrentUser(): Observable { return this.currentUser$.asObservable(); } } // Usage in a component // user-profile.component.ts @Component({ selector: 'app-user-profile', standalone: true, template: ` @if (user$ | async; as user) {

{{ user.name }}

{{ user.email }}

} ` }) export class UserProfileComponent implements OnInit { // Injection via inject() (recommended) private userService = inject(UserService); user$!: Observable; ngOnInit() { this.user$ = this.userService.getCurrentUser(); } } ``` ระดับการกำหนด provider ที่แตกต่างกัน (`providedIn: 'root'` ระดับโมดูล หรือระดับคอมโพเนนต์) ช่วยควบคุมขอบเขตและวงจรชีวิตของ service ได้ ### 7. providedIn root, any และ platform ต่างกันอย่างไร? ตัวเลือกของ `providedIn` ควบคุมวิธีที่ Angular สร้างและแชร์อินสแตนซ์ของ service ```typescript // 1. providedIn: 'root' - Application-level singleton // Single instance shared across the entire application @Injectable({ providedIn: 'root' }) export class AuthService { private isAuthenticated = false; login() { this.isAuthenticated = true; } logout() { this.isAuthenticated = false; } isLoggedIn() { return this.isAuthenticated; } } // 2. providedIn: 'any' - Instance per lazy-loaded module // Each lazy-loaded module gets its own instance @Injectable({ providedIn: 'any' }) export class FeatureLoggerService { private logs: string[] = []; log(message: string) { this.logs.push(`[${new Date().toISOString()}] ${message}`); } } // 3. providedIn: 'platform' - Shared between applications // Useful for micro-frontends or multi-app setups @Injectable({ providedIn: 'platform' }) export class SharedConfigService { readonly apiUrl = 'https://api.example.com'; } // 4. Component-level provision - Instance per component @Component({ selector: 'app-editor', standalone: true, // Each component instance has its own service instance providers: [EditorStateService], template: `...` }) export class EditorComponent { private editorState = inject(EditorStateService); } ``` ในกรณีส่วนใหญ่ `providedIn: 'root'` เพียงพอและเหมาะสมที่สุดสำหรับ tree-shaking > **Tree-shaking ของ service** > > ด้วย `providedIn: 'root'` Angular สามารถลบ service ที่ไม่ได้ใช้ออกจาก bundle สุดท้ายได้ ซึ่งช่วยให้ประสิทธิภาพการโหลดดีขึ้น ### 8. ใช้งาน Observable กับ RxJS ใน Angular อย่างไร? RxJS คือไลบรารีการเขียนโปรแกรมแบบ reactive ที่ Angular ใช้จัดการกระแสข้อมูลแบบอะซิงโครนัส ```typescript // search.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, Subject, debounceTime, distinctUntilChanged, switchMap, catchError, of, map, tap } from 'rxjs'; interface SearchResult { id: number; title: string; description: string; } @Injectable({ providedIn: 'root' }) export class SearchService { private http = inject(HttpClient); search(term: string): Observable { return this.http.get(`/api/search?q=${term}`); } } // search.component.ts @Component({ selector: 'app-search', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: ` @if (isLoading) {
Loading...
}
    @for (result of results$ | async; track result.id) {
  • {{ result.title }}
  • }
` }) export class SearchComponent implements OnInit, OnDestroy { private searchService = inject(SearchService); private destroy$ = new Subject(); searchControl = new FormControl(''); results$!: Observable; isLoading = false; ngOnInit() { this.results$ = this.searchControl.valueChanges.pipe( // Wait 300ms after the last keystroke debounceTime(300), // Ignore if value hasn't changed distinctUntilChanged(), // Show loading tap(() => this.isLoading = true), // Cancel previous request and launch new one switchMap(term => term ? this.searchService.search(term) : of([]) ), // Hide loading tap(() => this.isLoading = false), // Handle errors catchError(error => { console.error('Search error:', error); this.isLoading = false; return of([]); }) ); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } ``` Operator ของ RxJS เช่น `debounceTime`, `distinctUntilChanged` และ `switchMap` มีความสำคัญมากในการเพิ่มประสิทธิภาพการค้นหาและหลีกเลี่ยงการส่ง request ที่ไม่จำเป็น ## Angular สมัยใหม่ (16+) ### 9. Signals ใน Angular คืออะไร และใช้อย่างไร? Signals (Angular 16+) แนะนำ primitive แบบ reactive ใหม่ที่เรียบง่ายและมีประสิทธิภาพสูงกว่า Observable สำหรับสถานะภายในเครื่อง ```typescript // counter.component.ts import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Counter: {{ count() }}

Double: {{ doubleCount() }}

Message: {{ message() }}

` }) export class CounterComponent { // Writable signal - modifiable value count = signal(0); // Computed signal - automatically derived // Recalculated only when count() changes doubleCount = computed(() => this.count() * 2); // Computed with conditional logic message = computed(() => { const value = this.count(); if (value < 0) return 'Negative value'; if (value === 0) return 'Zero'; if (value < 10) return 'Small number'; return 'Large number'; }); constructor() { // Effect - executed on every change of used signals // Useful for side effects (logs, localStorage, etc.) effect(() => { console.log(`New value: ${this.count()}`); localStorage.setItem('counter', String(this.count())); }); } increment() { // update() to modify based on previous value this.count.update(value => value + 1); } decrement() { this.count.update(value => value - 1); } reset() { // set() to directly set a value this.count.set(0); } } ``` Signals มอบประสิทธิภาพที่ดีกว่าด้วย change detection ที่ละเอียดกว่า และ API ที่ใช้งานง่ายกว่า RxJS สำหรับสถานะภายใน ### 10. Signal Inputs และ Outputs ทำงานอย่างไร? Angular 17+ แนะนำ `input()` และ `output()` เป็นทางเลือกบนพื้นฐาน signal สำหรับ decorator `@Input()` และ `@Output()` ```typescript // task-item.component.ts import { Component, input, output, computed } from '@angular/core'; interface Task { id: number; title: string; completed: boolean; priority: 'low' | 'medium' | 'high'; } @Component({ selector: 'app-task-item', standalone: true, template: `
{{ task().title }} {{ task().priority }} @if (showActions()) { }
` }) export class TaskItemComponent { // Required input - must be provided by parent task = input.required(); // Optional input with default value showActions = input(true); // Signal-based output toggle = output(); delete = output(); // Computed based on input priorityClass = computed(() => `priority-${this.task().priority}`); onToggle() { this.toggle.emit(this.task().id); } onDelete() { this.delete.emit(this.task().id); } } // Usage in parent // task-list.component.ts @Component({ selector: 'app-task-list', standalone: true, imports: [TaskItemComponent], template: ` @for (task of tasks(); track task.id) { } ` }) export class TaskListComponent { tasks = signal([ { id: 1, title: 'Learn Angular', completed: false, priority: 'high' }, { id: 2, title: 'Build a project', completed: false, priority: 'medium' } ]); canEdit = signal(true); onToggleTask(id: number) { this.tasks.update(tasks => tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t) ); } onDeleteTask(id: number) { this.tasks.update(tasks => tasks.filter(t => t.id !== id)); } } ``` Signal input ให้การกำหนดประเภทที่ดีขึ้น API ที่สอดคล้องกับ signal อื่น ๆ มากขึ้น และเตรียมพร้อมสำหรับการเปลี่ยนผ่านสู่ change detection แบบ zoneless ### 11. อธิบาย Control Flow แบบใหม่ใน Angular 17+ Angular 17 นำเสนอไวยากรณ์ control flow ที่ฝังอยู่ในเทมเพลตซึ่งมาแทนที่ structural directive อย่าง `*ngIf`, `*ngFor` และ `*ngSwitch` ```typescript // modern-control-flow.component.ts import { Component, signal } from '@angular/core'; interface User { id: number; name: string; role: 'admin' | 'user' | 'guest'; status: 'active' | 'inactive' | 'pending'; } @Component({ selector: 'app-modern-control-flow', standalone: true, template: ` @if (isLoading()) {
Loading...
} @else if (error()) {
{{ error() }}
} @else {

Users ({{ users().length }})

@for (user of users(); track user.id; let i = $index, first = $first, last = $last) {
{{ i + 1 }}. {{ user.name }} @switch (user.role) { @case ('admin') { Administrator } @case ('user') { User } @default { Guest } }
} @empty {

No users found

}
} ` }) export class ModernControlFlowComponent { isLoading = signal(false); error = signal(null); users = signal([ { id: 1, name: 'Alice', role: 'admin', status: 'active' }, { id: 2, name: 'Bob', role: 'user', status: 'active' }, { id: 3, name: 'Charlie', role: 'guest', status: 'pending' } ]); } ``` ไวยากรณ์ใหม่ให้ประสิทธิภาพที่ดีกว่าด้วยการคอมไพล์ที่ปรับปรุงแล้ว อ่านง่ายขึ้น และมีฟีเจอร์อย่าง `@empty` สำหรับ `@for` ### 12. กำหนดค่า Lazy Loading ด้วยคอมโพเนนต์ standalone อย่างไร? Lazy loading จะโหลดโมดูลหรือคอมโพเนนต์ตามต้องการ ช่วยลดเวลาในการโหลดครั้งแรก ```typescript // app.routes.ts import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', // Component loaded immediately loadComponent: () => import('./home/home.component') .then(m => m.HomeComponent) }, { path: 'products', // Lazy loading a standalone component loadComponent: () => import('./products/product-list.component') .then(m => m.ProductListComponent) }, { path: 'admin', // Lazy loading child routes loadChildren: () => import('./admin/admin.routes') .then(m => m.adminRoutes), // Guard for authentication canActivate: [authGuard] }, { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component') .then(m => m.DashboardComponent), // Inline child routes children: [ { path: 'stats', loadComponent: () => import('./dashboard/stats/stats.component') .then(m => m.StatsComponent) }, { path: 'settings', loadComponent: () => import('./dashboard/settings/settings.component') .then(m => m.SettingsComponent) } ] } ]; // admin/admin.routes.ts import { Routes } from '@angular/router'; export const adminRoutes: Routes = [ { path: '', loadComponent: () => import('./admin-layout.component') .then(m => m.AdminLayoutComponent), children: [ { path: 'users', loadComponent: () => import('./users/users.component') .then(m => m.UsersComponent) }, { path: 'reports', loadComponent: () => import('./reports/reports.component') .then(m => m.ReportsComponent) } ] } ]; // main.ts - Bootstrap with routes import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app/app.component'; import { routes } from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [ provideRouter(routes) ] }); ``` ด้วยคอมโพเนนต์ standalone การทำ lazy loading จะง่ายและละเอียดขึ้นกว่าการใช้ NgModules ## Angular Forms ### 13. Template-driven Forms กับ Reactive Forms ต่างกันอย่างไร? Angular มีแนวทางในการจัดการฟอร์ม 2 แบบ ซึ่งเหมาะกับการใช้งานที่แตกต่างกัน ```typescript // TEMPLATE-DRIVEN FORMS // Simple, declarative, suited for basic forms // template-form.component.ts import { Component } from '@angular/core'; import { FormsModule, NgForm } from '@angular/forms'; @Component({ selector: 'app-template-form', standalone: true, imports: [FormsModule], template: `
@if (emailField.invalid && emailField.touched) { Invalid email } @if (passwordField.errors?.['minlength']) { Minimum 8 characters }
` }) export class TemplateFormComponent { user = { email: '', password: '' }; onSubmit(form: NgForm) { if (form.valid) { console.log('Form data:', this.user); } } } // REACTIVE FORMS // More control, testable, suited for complex forms // reactive-form.component.ts import { Component, inject } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; @Component({ selector: 'app-reactive-form', standalone: true, imports: [ReactiveFormsModule], template: `
@if (loginForm.get('email')?.hasError('required') && loginForm.get('email')?.touched) { Email required } @if (loginForm.get('email')?.hasError('email')) { Invalid email format } @if (loginForm.get('password')?.hasError('minlength')) { Minimum 8 characters }
` }) export class ReactiveFormComponent { private fb = inject(FormBuilder); // Programmatic form definition loginForm = this.fb.group({ email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]] }); onSubmit() { if (this.loginForm.valid) { console.log('Form data:', this.loginForm.value); } } } ``` Reactive Forms แนะนำให้ใช้สำหรับฟอร์มที่ซับซ้อน เพราะมีความยืดหยุ่นสูง ทดสอบได้ง่ายกว่า และนำลอจิกการตรวจสอบกลับมาใช้ใหม่ได้ดียิ่งขึ้น ### 14. สร้าง validator แบบกำหนดเองอย่างไร? Validator แบบกำหนดเองช่วยให้สามารถใช้กฎตรวจสอบความถูกต้องที่เฉพาะเจาะจงตามธุรกิจได้ ```typescript // validators/custom.validators.ts import { AbstractControl, ValidationErrors, ValidatorFn, AsyncValidatorFn } from '@angular/forms'; import { Observable, of, map, delay } from 'rxjs'; // Synchronous validator - checks immediately export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = forbiddenName.test(control.value); return forbidden ? { forbiddenName: { value: control.value } } : null; }; } // Password confirmation validator export function passwordMatchValidator(): ValidatorFn { return (group: AbstractControl): ValidationErrors | null => { const password = group.get('password')?.value; const confirmPassword = group.get('confirmPassword')?.value; return password === confirmPassword ? null : { passwordMismatch: true }; }; } // Asynchronous validator - checks via API export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn { return (control: AbstractControl): Observable => { if (!control.value) { return of(null); } return userService.checkEmailExists(control.value).pipe( map(exists => exists ? { emailTaken: true } : null) ); }; } // Usage in a component // registration.component.ts import { Component, inject } from '@angular/core'; import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'; import { forbiddenNameValidator, passwordMatchValidator, uniqueEmailValidator } from './validators/custom.validators'; import { UserService } from './user.service'; @Component({ selector: 'app-registration', standalone: true, imports: [ReactiveFormsModule], template: `
@if (registrationForm.get('username')?.hasError('forbiddenName')) { This name is not allowed }
@if (registrationForm.get('email')?.pending) { Checking... } @if (registrationForm.get('email')?.hasError('emailTaken')) { This email is already in use }
@if (registrationForm.get('passwords')?.hasError('passwordMismatch')) { Passwords do not match }
` }) export class RegistrationComponent { private fb = inject(FormBuilder); private userService = inject(UserService); registrationForm = this.fb.group({ username: ['', [ Validators.required, forbiddenNameValidator(/admin/i) ]], email: ['', [Validators.required, Validators.email], [uniqueEmailValidator(this.userService)] ], passwords: this.fb.group({ password: ['', [Validators.required, Validators.minLength(8)]], confirmPassword: ['', Validators.required] }, { validators: passwordMatchValidator() }) }); onSubmit() { if (this.registrationForm.valid) { console.log(this.registrationForm.value); } } } ``` Validator แบบอะซิงโครนัสมีประโยชน์มากในการตรวจสอบฝั่งเซิร์ฟเวอร์ เช่น ความเป็นเอกลักษณ์ของอีเมลหรือชื่อผู้ใช้ ## Routing และ Navigation ### 15. ป้องกันเส้นทาง (route) ด้วย Guards อย่างไร? Guards ควบคุมการเข้าถึงเส้นทางตามเงื่อนไข เช่น การยืนยันตัวตนหรือสิทธิ์การเข้าถึง ```typescript // guards/auth.guard.ts import { inject } from '@angular/core'; import { Router, CanActivateFn, CanMatchFn } from '@angular/router'; import { AuthService } from '../services/auth.service'; // Functional guard (recommended since Angular 15+) export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); if (authService.isAuthenticated()) { return true; } // Redirect to login page with return URL return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); }; // Role-based guard export const roleGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); const requiredRoles = route.data['roles'] as string[]; const userRole = authService.getUserRole(); if (requiredRoles.includes(userRole)) { return true; } return router.createUrlTree(['/unauthorized']); }; // Guard for lazy loading (canMatch) export const featureGuard: CanMatchFn = (route, segments) => { const featureService = inject(FeatureService); return featureService.isFeatureEnabled(route.path || ''); }; // Route configuration with guards // app.routes.ts export const routes: Routes = [ { path: 'login', loadComponent: () => import('./login.component') }, { path: 'dashboard', loadComponent: () => import('./dashboard.component'), canActivate: [authGuard] }, { path: 'admin', loadChildren: () => import('./admin/admin.routes'), canActivate: [authGuard, roleGuard], canMatch: [featureGuard], data: { roles: ['admin', 'superadmin'] } }, { path: 'settings', loadComponent: () => import('./settings.component'), canActivate: [authGuard], // Guard for leaving page (unsaved data) canDeactivate: [unsavedChangesGuard] } ]; // Guard for unsaved changes export const unsavedChangesGuard: CanDeactivateFn<{ hasUnsavedChanges: () => boolean }> = (component) => { if (component.hasUnsavedChanges()) { return confirm('Unsaved changes will be lost. Continue?'); } return true; }; ``` Functional guard เรียบง่ายกว่าและทำงานร่วมกับ dependency injection สมัยใหม่ได้ดีกว่า ### 16. ส่งผ่านข้อมูลระหว่างเส้นทางอย่างไร? Angular มีหลายวิธีในการส่งข้อมูลระหว่างการนำทาง ```typescript // 1. Route Parameters (in URL) // app.routes.ts export const routes: Routes = [ { path: 'products/:id', loadComponent: () => import('./product-detail.component') } ]; // product-detail.component.ts import { Component, inject, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-product-detail', standalone: true, template: `

Product {{ productId }}

` }) export class ProductDetailComponent implements OnInit { private route = inject(ActivatedRoute); productId!: string; ngOnInit() { // Access route parameters this.productId = this.route.snapshot.paramMap.get('id')!; // Or reactively this.route.paramMap.subscribe(params => { this.productId = params.get('id')!; }); } } // 2. Query Parameters (?key=value) // navigation.component.ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-navigation', standalone: true, template: ` ` }) export class NavigationComponent { private router = inject(Router); navigateWithQuery() { this.router.navigate(['/products'], { queryParams: { category: 'electronics', sort: 'price' } }); } } // 3. State (data not visible in URL) // order-confirmation.component.ts @Component({ selector: 'app-order-confirmation', standalone: true, template: ` @if (orderData) {

Order #{{ orderData.orderId }} confirmed

} ` }) export class OrderConfirmationComponent implements OnInit { private route = inject(ActivatedRoute); orderData: any; ngOnInit() { // Retrieve state passed via navigation this.orderData = history.state; } } // Navigate with state this.router.navigate(['/order-confirmation'], { state: { orderId: '12345', total: 99.99 } }); // 4. Resolvers (data pre-loading) // product.resolver.ts import { inject } from '@angular/core'; import { ResolveFn } from '@angular/router'; import { ProductService } from './product.service'; export const productResolver: ResolveFn = (route) => { const productService = inject(ProductService); const id = route.paramMap.get('id')!; return productService.getProduct(id); }; // Route with resolver { path: 'products/:id', loadComponent: () => import('./product-detail.component'), resolve: { product: productResolver } } // Access resolved data ngOnInit() { this.product = this.route.snapshot.data['product']; } ``` > **State และการรีเฟรชหน้า** > > ข้อมูลที่ส่งผ่าน `state` จะหายไปเมื่อโหลดหน้าใหม่ สำหรับข้อมูลที่ต้องเก็บถาวร ควรใช้พารามิเตอร์ใน URL หรือเก็บไว้ใน service ## ประสิทธิภาพและการปรับแต่ง ### 17. Change detection ใน Angular ทำงานอย่างไร? Angular ใช้ Zone.js ตรวจจับเหตุการณ์อะซิงโครนัสและกระตุ้นการตรวจสอบคอมโพเนนต์ ```typescript // change-detection.component.ts import { Component, ChangeDetectionStrategy, ChangeDetectorRef, inject, signal } from '@angular/core'; // Default strategy: checks entire component tree @Component({ selector: 'app-default-strategy', template: `

{{ data }}

` }) export class DefaultStrategyComponent { data = 'Hello'; } // OnPush strategy: checks only if inputs change // or if an event is triggered within the component @Component({ selector: 'app-onpush-strategy', changeDetection: ChangeDetectionStrategy.OnPush, template: `

{{ data().name }}

` }) export class OnPushStrategyComponent { private cdr = inject(ChangeDetectorRef); // Signal: automatically triggers detection data = signal({ name: 'Alice' }); update() { // With signal, update is automatic this.data.set({ name: 'Bob' }); } // For cases where manual detection is needed manualUpdate() { // Mark component for checking this.cdr.markForCheck(); // Or force immediate detection this.cdr.detectChanges(); } } // Practical example: optimized list @Component({ selector: 'app-optimized-list', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for (item of items(); track item.id) { } ` }) export class OptimizedListComponent { items = signal([]); addItem(item: Item) { // Create new array to trigger detection this.items.update(current => [...current, item]); } updateItem(id: number, changes: Partial) { this.items.update(current => current.map(item => item.id === id ? { ...item, ...changes } : item ) ); } } ``` กลยุทธ์ OnPush เมื่อใช้ร่วมกับ Signals ให้ประสิทธิภาพที่ดีที่สุด เพราะจำกัดการตรวจสอบให้เกิดเฉพาะกับคอมโพเนนต์ที่มีการเปลี่ยนแปลงจริง ### 18. ปรับแต่งประสิทธิภาพของแอป Angular อย่างไร? มีหลายเทคนิคที่สามารถปรับปรุงประสิทธิภาพของแอป Angular ได้ ```typescript // 1. Lazy Loading routes (see question 12) // 2. TrackBy for lists (mandatory with @for) @Component({ template: ` @for (user of users(); track user.id) { } ` }) export class UserListComponent { users = signal([]); } // 3. Pure pipes for transformations // format-date.pipe.ts import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'formatDate', standalone: true, pure: true // Default, recalculated only if input changes }) export class FormatDatePipe implements PipeTransform { transform(value: Date, format: string = 'short'): string { return new Intl.DateTimeFormat('en-US', { dateStyle: format as any }).format(value); } } // 4. Virtual Scrolling for large lists import { Component } from '@angular/core'; import { ScrollingModule } from '@angular/cdk/scrolling'; @Component({ selector: 'app-virtual-list', standalone: true, imports: [ScrollingModule], template: `
{{ item.name }}
`, styles: [` .viewport { height: 400px; } .item { height: 50px; } `] }) export class VirtualListComponent { items = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })); } // 5. Defer for deferred loading (Angular 17+) @Component({ template: `
Always loaded immediately
@defer (on viewport) { } @placeholder {
Loading...
} @loading (minimum 500ms) { } @defer (on interaction) { } @placeholder { } @defer (on timer(2000ms)) { } ` }) export class OptimizedPageComponent {} ``` การผสานเทคนิคเหล่านี้กับการวิเคราะห์ผ่าน Angular DevTools ช่วยระบุและจัดการคอขวดได้ ## การสื่อสารระหว่างคอมโพเนนต์ ### 19. มีวิธีใดบ้างในการสื่อสารระหว่างคอมโพเนนต์? Angular มีรูปแบบการสื่อสารหลายแบบขึ้นอยู่กับความสัมพันธ์เชิงลำดับชั้นระหว่างคอมโพเนนต์ ```typescript // 1. Parent → Child: @Input / input() // parent.component.ts @Component({ template: `` }) export class ParentComponent { parentMessage = 'Hello from parent'; } // child.component.ts @Component({ template: `

{{ message() }}

` }) export class ChildComponent { message = input.required(); } // 2. Child → Parent: @Output / output() // child.component.ts @Component({ template: `` }) export class ChildComponent { messageEvent = output(); sendMessage() { this.messageEvent.emit('Hello from child'); } } // parent.component.ts @Component({ template: `` }) export class ParentComponent { onMessage(message: string) { console.log(message); } } // 3. Via shared Service (unrelated components) // message.service.ts @Injectable({ providedIn: 'root' }) export class MessageService { private messageSubject = new Subject(); message$ = this.messageSubject.asObservable(); // Or with Signal currentMessage = signal(''); sendMessage(message: string) { this.messageSubject.next(message); this.currentMessage.set(message); } } // component-a.ts @Component({ template: `` }) export class ComponentA { private messageService = inject(MessageService); send() { this.messageService.sendMessage('Hello from A'); } } // component-b.ts @Component({ template: `

{{ message$ | async }}

` }) export class ComponentB implements OnDestroy { private messageService = inject(MessageService); private destroy$ = new Subject(); message$ = this.messageService.message$; ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } // 4. ViewChild to access a child @Component({ template: `` }) export class ParentComponent implements AfterViewInit { @ViewChild('timer') timerComponent!: TimerComponent; ngAfterViewInit() { this.timerComponent.start(); } } // 5. ContentChild for projected content @Component({ selector: 'app-card', template: `
` }) export class CardComponent implements AfterContentInit { @ContentChild('header') header!: ElementRef; ngAfterContentInit() { console.log('Header content:', this.header); } } ``` การเลือกวิธีการสื่อสารขึ้นอยู่กับความสัมพันธ์ระหว่างคอมโพเนนต์และความซับซ้อนของการสื่อสารนั้น ### 20. ใช้งานระบบจัดการสถานะด้วย Signals อย่างไร? Signals ช่วยให้สร้าง store แบบ reactive ที่เรียบง่ายได้โดยไม่ต้องพึ่งไลบรารีภายนอก ```typescript // store/cart.store.ts import { Injectable, signal, computed } from '@angular/core'; interface CartItem { id: number; name: string; price: number; quantity: number; } interface CartState { items: CartItem[]; loading: boolean; error: string | null; } @Injectable({ providedIn: 'root' }) export class CartStore { // Private state private state = signal({ items: [], loading: false, error: null }); // Public selectors (read-only) readonly items = computed(() => this.state().items); readonly loading = computed(() => this.state().loading); readonly error = computed(() => this.state().error); readonly itemCount = computed(() => this.state().items.reduce((sum, item) => sum + item.quantity, 0) ); readonly total = computed(() => this.state().items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) ); readonly isEmpty = computed(() => this.state().items.length === 0); // Actions addItem(product: Omit) { this.state.update(state => { const existingItem = state.items.find(i => i.id === product.id); if (existingItem) { return { ...state, items: state.items.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ) }; } return { ...state, items: [...state.items, { ...product, quantity: 1 }] }; }); } removeItem(id: number) { this.state.update(state => ({ ...state, items: state.items.filter(item => item.id !== id) })); } updateQuantity(id: number, quantity: number) { if (quantity <= 0) { this.removeItem(id); return; } this.state.update(state => ({ ...state, items: state.items.map(item => item.id === id ? { ...item, quantity } : item ) })); } clearCart() { this.state.update(state => ({ ...state, items: [] })); } // Async action async checkout() { this.state.update(s => ({ ...s, loading: true, error: null })); try { // API call await fetch('/api/checkout', { method: 'POST', body: JSON.stringify({ items: this.items() }) }); this.clearCart(); } catch (e) { this.state.update(s => ({ ...s, error: 'Order error' })); } finally { this.state.update(s => ({ ...s, loading: false })); } } } // Usage in a component // cart.component.ts @Component({ selector: 'app-cart', standalone: true, template: ` @if (cartStore.isEmpty()) {

Your cart is empty

} @else { @for (item of cartStore.items(); track item.id) {
{{ item.name }} {{ item.price }} $ × {{ item.quantity }}
}
Total: {{ cartStore.total() }} $ ({{ cartStore.itemCount() }} items)
} ` }) export class CartComponent { cartStore = inject(CartStore); } ``` รูปแบบนี้ให้การจัดการสถานะที่คาดเดาได้และตอบสนองดี โดยไม่ต้องเผชิญกับความซับซ้อนของ NgRx เหมาะสำหรับแอปพลิเคชันขนาดกลาง ## การทดสอบใน Angular ### 21. ทดสอบคอมโพเนนต์ Angular อย่างไร? การทดสอบคอมโพเนนต์ตรวจสอบการเรนเดอร์ การโต้ตอบ และการทำงานร่วมกับ service ```typescript // user-card.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { UserCardComponent } from './user-card.component'; describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent] }).compileComponents(); fixture = TestBed.createComponent(UserCardComponent); component = fixture.componentInstance; }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display user name', () => { // Arrange fixture.componentRef.setInput('user', { name: 'Alice', email: 'alice@test.com' }); // Act fixture.detectChanges(); // Assert const nameElement = fixture.debugElement.query(By.css('.user-name')); expect(nameElement.nativeElement.textContent).toContain('Alice'); }); it('should emit event when delete button clicked', () => { // Arrange fixture.componentRef.setInput('user', { id: 1, name: 'Alice' }); fixture.detectChanges(); const deleteSpy = jest.spyOn(component.delete, 'emit'); // Act const deleteButton = fixture.debugElement.query(By.css('.delete-btn')); deleteButton.triggerEventHandler('click', null); // Assert expect(deleteSpy).toHaveBeenCalledWith(1); }); it('should show loading state', () => { // Arrange fixture.componentRef.setInput('isLoading', true); // Act fixture.detectChanges(); // Assert const spinner = fixture.debugElement.query(By.css('.spinner')); expect(spinner).toBeTruthy(); }); }); // Test with mocked service // user-list.component.spec.ts import { of, throwError } from 'rxjs'; describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture; let mockUserService: jest.Mocked; beforeEach(async () => { mockUserService = { getUsers: jest.fn(), deleteUser: jest.fn() } as any; await TestBed.configureTestingModule({ imports: [UserListComponent], providers: [ { provide: UserService, useValue: mockUserService } ] }).compileComponents(); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; }); it('should load users on init', () => { // Arrange const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; mockUserService.getUsers.mockReturnValue(of(users)); // Act fixture.detectChanges(); // Assert expect(mockUserService.getUsers).toHaveBeenCalled(); expect(component.users()).toEqual(users); }); it('should handle error state', () => { // Arrange mockUserService.getUsers.mockReturnValue( throwError(() => new Error('Network error')) ); // Act fixture.detectChanges(); // Assert expect(component.error()).toBe('Loading error'); }); }); ``` ### 22. ทดสอบ service ของ Angular อย่างไร? การทดสอบ service ตรวจสอบลอจิกทางธุรกิจและการโต้ตอบกับ API ```typescript // auth.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { AuthService } from './auth.service'; describe('AuthService', () => { let service: AuthService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [AuthService] }); service = TestBed.inject(AuthService); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { // Verify no pending requests httpMock.verify(); }); describe('login', () => { it('should return user on successful login', () => { // Arrange const credentials = { email: 'test@test.com', password: 'password' }; const mockResponse = { id: 1, email: 'test@test.com', token: 'abc123' }; // Act service.login(credentials).subscribe(user => { // Assert expect(user).toEqual(mockResponse); expect(service.isAuthenticated()).toBe(true); }); // Simulate HTTP response const req = httpMock.expectOne('/api/auth/login'); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(credentials); req.flush(mockResponse); }); it('should handle login error', () => { // Arrange const credentials = { email: 'test@test.com', password: 'wrong' }; // Act service.login(credentials).subscribe({ error: (error) => { // Assert expect(error.status).toBe(401); expect(service.isAuthenticated()).toBe(false); } }); // Simulate error const req = httpMock.expectOne('/api/auth/login'); req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' }); }); }); describe('logout', () => { it('should clear authentication state', () => { // Arrange - simulate logged in user service['currentUser'].set({ id: 1, email: 'test@test.com' }); // Act service.logout(); // Assert expect(service.isAuthenticated()).toBe(false); expect(service.getCurrentUser()).toBeNull(); }); }); }); ``` ## คำถามขั้นสูง ### 23. Server-Side Rendering (SSR) ทำงานกับ Angular อย่างไร? Angular Universal ช่วยให้สามารถเรนเดอร์ฝั่งเซิร์ฟเวอร์ได้เพื่อปรับปรุง SEO และประสิทธิภาพที่ผู้ใช้รู้สึก ```typescript // SSR Configuration with Angular 17+ // app.config.server.ts import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering() ] }; export const config = mergeApplicationConfig(appConfig, serverConfig); // Handling browser-only APIs // platform.service.ts import { Injectable, PLATFORM_ID, inject } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class PlatformService { private platformId = inject(PLATFORM_ID); get isBrowser(): boolean { return isPlatformBrowser(this.platformId); } get isServer(): boolean { return isPlatformServer(this.platformId); } } // SSR-aware component // analytics.component.ts @Component({ selector: 'app-analytics', standalone: true, template: ` @if (platform.isBrowser) {
} ` }) export class AnalyticsComponent implements OnInit { platform = inject(PlatformService); ngOnInit() { // Code executed only on client side if (this.platform.isBrowser) { this.initializeAnalytics(); } } private initializeAnalytics() { // window and document are available console.log('Analytics initialized on', window.location.href); } } // TransferState to avoid duplicate requests // product.service.ts import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TransferState, makeStateKey } from '@angular/core'; import { of, tap } from 'rxjs'; const PRODUCTS_KEY = makeStateKey('products'); @Injectable({ providedIn: 'root' }) export class ProductService { private http = inject(HttpClient); private transferState = inject(TransferState); private platform = inject(PlatformService); getProducts() { // Client side: check if data already exists if (this.platform.isBrowser) { const cachedProducts = this.transferState.get(PRODUCTS_KEY, null); if (cachedProducts) { this.transferState.remove(PRODUCTS_KEY); return of(cachedProducts); } } return this.http.get('/api/products').pipe( tap(products => { // Server side: store for client if (this.platform.isServer) { this.transferState.set(PRODUCTS_KEY, products); } }) ); } } ``` SSR ปรับปรุง First Contentful Paint และช่วยให้ search engine สามารถเก็บดัชนีเนื้อหาแบบไดนามิกได้ ### 24. จัดการการแปลภาษาหลายภาษา (i18n) ใน Angular อย่างไร? Angular มีหลายแนวทางในการแปลภาษาแอปพลิเคชัน ```typescript // 1. Built-in Angular i18n (separate compilation per language) // app.component.ts @Component({ template: `

Welcome to our application

{itemCount, plural, =0 {No items} =1 {One item} other {{{itemCount}} items} }

` }) export class AppComponent { itemCount = 5; } // 2. ngx-translate (runtime language switching) // app.config.ts import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); } // Configuration provideTranslateService({ defaultLanguage: 'en', loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [HttpClient] } }) // language-switcher.component.ts @Component({ selector: 'app-language-switcher', standalone: true, imports: [TranslateModule], template: `

{{ 'HOME.TITLE' | translate }}

{{ 'HOME.WELCOME' | translate:{ name: userName } }}

` }) export class LanguageSwitcherComponent { private translate = inject(TranslateService); userName = 'Alice'; changeLanguage(event: Event) { const lang = (event.target as HTMLSelectElement).value; this.translate.use(lang); } } // assets/i18n/en.json { "HOME": { "TITLE": "Welcome", "WELCOME": "Hello {{name}}!" } } // assets/i18n/fr.json { "HOME": { "TITLE": "Bienvenue", "WELCOME": "Bonjour {{name}} !" } } ``` การเลือกระหว่าง i18n ในตัวกับ ngx-translate ขึ้นอยู่กับความต้องการ: การคอมไพล์แยกเพื่อประสิทธิภาพสูงสุด หรือการสลับแบบไดนามิกเพื่อความยืดหยุ่นมากขึ้น ### 25. แนวปฏิบัติที่ดีที่สุดในการจัดโครงสร้างโปรเจกต์ Angular คืออะไร? โครงสร้างที่จัดวางอย่างเป็นระเบียบช่วยให้การบำรุงรักษาและการขยายโปรเจกต์ง่ายขึ้น ``` src/ ├── app/ │ ├── core/ # Singleton services, guards, interceptors │ │ ├── guards/ │ │ │ └── auth.guard.ts │ │ ├── interceptors/ │ │ │ └── auth.interceptor.ts │ │ ├── services/ │ │ │ ├── auth.service.ts │ │ │ └── api.service.ts │ │ └── core.provider.ts # Provider configuration │ │ │ ├── shared/ # Reusable components, pipes, directives │ │ ├── components/ │ │ │ ├── button/ │ │ │ └── modal/ │ │ ├── directives/ │ │ ├── pipes/ │ │ └── index.ts # Barrel exports │ │ │ ├── features/ # Feature modules (lazy-loaded) │ │ ├── products/ │ │ │ ├── components/ │ │ │ ├── services/ │ │ │ ├── models/ │ │ │ ├── products.routes.ts │ │ │ └── products.component.ts │ │ ├── cart/ │ │ └── checkout/ │ │ │ ├── layouts/ # Page layouts │ │ ├── main-layout/ │ │ └── auth-layout/ │ │ │ ├── app.component.ts │ ├── app.config.ts │ └── app.routes.ts │ ├── assets/ ├── environments/ └── styles/ ``` ```typescript // Best code practices // 1. Barrel exports to simplify imports // shared/index.ts export * from './components/button/button.component'; export * from './components/modal/modal.component'; export * from './pipes/format-date.pipe'; // 2. Centralized provider configuration // core/core.provider.ts import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { authInterceptor } from './interceptors/auth.interceptor'; export const coreProviders = [ provideHttpClient( withInterceptors([authInterceptor]) ), // Other global providers ]; // 3. Typed models // features/products/models/product.model.ts export interface Product { id: number; name: string; price: number; category: ProductCategory; createdAt: Date; } export type ProductCategory = 'electronics' | 'clothing' | 'books'; export interface CreateProductDto { name: string; price: number; category: ProductCategory; } // 4. Functional interceptor // core/interceptors/auth.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.getToken(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } return next(req); }; ``` ข้อตกลงเหล่านี้ช่วยให้นำทางในโค้ดได้อย่างเป็นธรรมชาติและทำงานเป็นทีมได้ง่ายขึ้น ## บทสรุป คำถาม 25 ข้อนี้ครอบคลุมแนวคิด Angular ที่สำคัญซึ่งมักถูกถามในการสัมภาษณ์ ประเด็นสำคัญที่ควรเชี่ยวชาญ: - ✅ **พื้นฐาน**: คอมโพเนนต์, data binding, วงจรชีวิต, DI - ✅ **Angular สมัยใหม่**: Signals, คอมโพเนนต์ standalone, control flow ใหม่ - ✅ **ความเป็น reactive**: RxJS, Observable, การจัดการสถานะด้วย Signals - ✅ **ฟอร์ม**: Template-driven vs Reactive, การตรวจสอบแบบกำหนดเอง - ✅ **Routing**: Guards, lazy loading, การส่งข้อมูล - ✅ **ประสิทธิภาพ**: OnPush, defer, virtual scrolling - ✅ **การทดสอบ**: unit test, mock, HttpTestingController การเตรียมตัวสำหรับการสัมภาษณ์ Angular ต้องอาศัยการฝึกฝนอย่างสม่ำเสมอ การสร้างโปรเจกต์ส่วนตัวช่วยตอกย้ำความรู้เหล่านี้และอธิบายได้อย่างเป็นธรรมชาติในระหว่างสัมภาษณ์ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/angular/top-25-angular-interview-questions