- Pengenalan Angular
- Setup dan Instalasi Angular CLI
- Memahami Components
- Templates dan Data Binding
- Directives: Structural & Attribute
- Services dan Dependency Injection
- Routing dan Navigasi
- RxJS: Reactive Programming
- HTTP Client dan API Communication
- Form Handling di Angular
- Best Practices dan Tips
- Quiz Pemahaman
1. Pengenalan Angular
Angular adalah framework web yang dikembangkan oleh Google dan pertama kali dirilis pada tahun 2016 sebagai versi rewrite dari AngularJS. Angular menggunakan TypeScript sebagai bahasa utama dan dirancang khusus untuk membangun aplikasi web enterprise yang kompleks, scalable, dan mudah di-maintain.
Berbeda dengan library seperti React yang hanya menangani view layer, Angular bersifat opinionated full framework yang menyediakan semua tools yang dibutuhkan: routing, form handling, HTTP client, testing, dan banyak lagi β semuanya terintegrasi dalam satu paket.
Mengapa Memilih Angular?
| Keunggulan | Penjelasan |
|---|---|
| TypeScript Native | Angular dibangun dengan TypeScript yang menyediakan type safety, autocompletion, dan refactoring yang lebih baik |
| Full Framework | Semua tools sudah tersedia dari awal β routing, HTTP, forms, testing, i18n |
| Dependency Injection | Sistem DI bawaan yang powerful untuk mengelola dependencies antar komponen |
| CLI yang Powerful | Angular CLI memudahkan generate components, services, testing, dan build production |
| Enterprise Ready | Banyak digunakan di perusahaan besar seperti Google, Microsoft, Deutsche Bank |
| RxJS Integration | Dukungan reactive programming bawaan untuk menangani async operations |
Angular vs Framework Lain
| Aspek | Angular | React | Vue.js |
|---|---|---|---|
| Tipe | Full Framework | Library | Framework |
| Bahasa | TypeScript | JSX/TSX | JavaScript/Template |
| Ukuran | ~143 KB (gzip) | ~42 KB (gzip) | ~33 KB (gzip) |
| Learning Curve | π΄ Curam | π‘ Sedang | π’ Mudah |
| State Management | NgRx / Services / Signals | Redux / Zustand | Vuex / Pinia |
| DOM | Incremental DOM | Virtual DOM | Virtual DOM |
| Cocok untuk | Enterprise, Banking, SPA besar | SPA, Mobile App | SPA kecil-sedang |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β ANGULAR APPLICATION β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Angular Modules β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β β β AppModule β β Feature β β Shared Module β β β β β β (Root) β β Modules β β (Common) β β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Component Tree β β β β ββββββββββββββββββββββββββββββββββββββββββββββββ β β β β β AppComponent β β β β β β ββββββββββββββ βββββββββββββββββββββββββ β β β β β β β Navbar β β Router Outlet β β β β β β β β Component β β ββββββββ ββββββββββ β β β β β β β ββββββββββββββ β βHomeCmpβ βAboutCmpβ β β β β β β β β ββββββββ ββββββββββ β β β β β β β ββββββββββββββ βββββββββββββββββββββββββ β β β β β β β Footer β β β β β β β β Component β β β β β β β ββββββββββββββ β β β β β ββββββββββββββββββββββββββββββββββββββββββββββββ β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Services Layer (DI) β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β β βAuthServiceβ βDataServiceβ β NotificationSvc β β β β β ββββββββββββ ββββββββββββ ββββββββββββββββββββ β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Jangan bingung antara Angular (v2+ sampai v17+) dan AngularJS (v1.x). Angular adalah rewrite total yang menggunakan TypeScript, component-based architecture, dan performa jauh lebih baik. AngularJS sudah deprecated sejak Desember 2021.
2. Setup dan Instalasi Angular CLI
Untuk memulai dengan Angular, kita perlu menginstal Angular CLI (Command Line Interface) yang merupakan tool resmi untuk membuat, mengembangkan, dan mem-build aplikasi Angular.
Prasyarat
- Node.js versi 18.13 atau lebih baru
- npm versi 8 atau lebih baru (sudah terinstal bersama Node.js)
- Text Editor β VS Code sangat direkomendasikan dengan extension Angular Language Service
Instalasi Angular CLI
# Instal Angular CLI secara global npm install -g @angular/cli # Verifikasi instalasi ng version # Buat proyek baru ng new my-angular-app # Masuk ke direktori proyek cd my-angular-app # Jalankan development server ng serve --open
Struktur Folder Angular
my-angular-app/ βββ src/ β βββ app/ β β βββ app.component.ts # Root component β β βββ app.component.html # Template root β β βββ app.component.css # Style root β β βββ app.component.spec.ts # Unit test β β βββ app.config.ts # App configuration β β βββ app.routes.ts # Route definitions β βββ assets/ # Static files β βββ environments/ # Env configs β βββ index.html # Main HTML β βββ main.ts # Bootstrap entry β βββ styles.css # Global styles βββ angular.json # Angular config βββ package.json # Dependencies βββ tsconfig.json # TypeScript config βββ tsconfig.app.json # App TS config
Sejak Angular 14+, Angular mendukung standalone components yang tidak memerlukan NgModule. Mulai Angular 15-17, standalone menjadi default. Dalam tutorial ini kita menggunakan pendekatan standalone terbaru.
3. Memahami Components
Components adalah blok bangunan utama dari setiap aplikasi Angular. Setiap component terdiri dari tiga bagian utama: template (HTML), class (TypeScript), dan styles (CSS/SCSS).
Membuat Component
# Generate component menggunakan CLI ng generate component components/user-card # Atau shorthand: ng g c components/user-card # Hasilnya: # src/app/components/user-card/ # βββ user-card.component.ts # βββ user-card.component.html # βββ user-card.component.css # βββ user-card.component.spec.ts
Anatomi Component
// user-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule],
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
// Input properties β data dari parent component
@Input() name: string = '';
@Input() email: string = '';
@Input() avatar: string = '';
@Input() role: string = 'User';
// Output events β emit event ke parent
@Output() onDelete = new EventEmitter<string>();
@Output() onEdit = new EventEmitter<void>();
// Internal state
isExpanded: boolean = false;
// Methods
toggleExpand(): void {
this.isExpanded = !this.isExpanded;
}
handleDelete(): void {
if (confirm(`Hapus user ${this.name}?`)) {
this.onDelete.emit(this.name);
}
}
handleEdit(): void {
this.onEdit.emit();
}
// Getter
get badgeColor(): string {
const colors: Record<string, string> = {
'Admin': '#e74c3c',
'Editor': '#f39c12',
'User': '#2ecc71'
};
return colors[this.role] || '#95a5a6';
}
}
<!-- user-card.component.html -->
<div class="user-card" [class.expanded]="isExpanded">
<div class="user-header">
<img [src]="avatar" [alt]="name" class="user-avatar">
<div class="user-info">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
<span class="badge" [style.backgroundColor]="badgeColor">
{{ role }}
</span>
</div>
</div>
<div class="user-actions">
<button (click)="toggleExpand()">
{{ isExpanded ? 'Tutup' : 'Detail' }}
</button>
<button (click)="handleEdit()">βοΈ Edit</button>
<button (click)="handleDelete()">ποΈ Hapus</button>
</div>
<div class="user-details" *ngIf="isExpanded">
<ng-content></ng-content>
</div>
</div>
Component Lifecycle Hooks
| Hook | Saat Dipanggil | Penggunaan Umum |
|---|---|---|
ngOnChanges | Saat input properties berubah | React terhadap perubahan data dari parent |
ngOnInit | Setelah component pertama kali diinisialisasi | Inisialisasi data, fetch dari API |
ngDoCheck | Saat change detection berjalan | Custom change detection |
ngAfterContentInit | Setelah content (ng-content) di-proyeksikan | Akses projected content |
ngAfterViewInit | Setelah view dan child views diinisialisasi | Akses DOM, inisialisasi chart |
ngOnDestroy | Saat component di-destroy | Cleanup subscriptions, timers |
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<div *ngFor="let user of users">
<app-user-card
[name]="user.name"
[email]="user.email"
[avatar]="user.avatar"
[role]="user.role"
(onDelete)="removeUser($event)"
/>
</div>
`
})
export class UserListComponent implements OnInit, OnDestroy {
users: any[] = [];
private subscription!: Subscription;
constructor(private userService: UserService) {}
ngOnInit(): void {
console.log('Component initialized');
this.subscription = this.userService.getUsers().subscribe(
data => this.users = data
);
}
ngOnDestroy(): void {
console.log('Component destroyed');
this.subscription.unsubscribe();
}
removeUser(name: string): void {
this.users = this.users.filter(u => u.name !== name);
}
}
4. Templates dan Data Binding
Angular menggunakan sistem data binding yang powerful untuk menghubungkan data di component class dengan tampilan di template. Ada beberapa jenis data binding yang perlu dipahami.
Jenis Data Binding
| Jenis | Sintaks | Arah | Contoh |
|---|---|---|---|
| Interpolation | {{ value }} | Component β Template | {{ userName }} |
| Property Binding | [property]="value" | Component β Template | [src]="imageUrl" |
| Event Binding | (event)="handler" | Template β Component | (click)="onClick()" |
| Two-Way Binding | [(ngModel)]="value" | Bidirectional | [(ngModel)]="name" |
| Attribute Binding | [attr.attr]="value" | Component β Template | [attr.aria-label]="text" |
| Class Binding | [class.name]="cond" | Component β Template | [class.active]="isActive" |
| Style Binding | [style.prop]="value" | Component β Template | [style.color]="color" |
<!-- Contoh Semua Jenis Data Binding -->
<!-- 1. Interpolation -->
<h1>Selamat datang, {{ username }}!</h1>
<p>Total: {{ items.length }} item</p>
<!-- 2. Property Binding -->
<img [src]="user.avatar" [alt]="user.name">
<button [disabled]="isLoading">Simpan</button>
<!-- 3. Event Binding -->
<button (click)="saveData()">Simpan</button>
<input (keyup.enter)="search($event)">
<form (submit)="onSubmit()">...</form>
<!-- 4. Two-Way Binding -->
<input [(ngModel)]="searchTerm">
<p>Anda mencari: {{ searchTerm }}</p>
<!-- 5. Class dan Style Binding -->
<div [class.selected]="isSelected"
[class.error]="hasError"
[style.background-color]="bgColor"
[style.width.px]="progressWidth">
Status: {{ isSelected ? 'Dipilih' : 'Tidak dipilih' }}
</div>
<!-- 6. Template Reference Variables -->
<input #searchInput>
<button (click)="doSearch(searchInput.value)">Cari</button>
Angular menyediakan pipes untuk mentransformasi data di template. Contoh: {{ harga | currency:'IDR' }}, {{ tanggal | date:'dd MMM yyyy' }}, {{ nama | uppercase }}. Kamu juga bisa membuat custom pipe untuk kebutuhan spesifik.
5. Directives: Structural & Attribute
Directives adalah instruksi khusus yang mengubah tampilan atau perilaku elemen DOM. Angular memiliki dua jenis directive utama: Structural Directives yang mengubah struktur DOM, dan Attribute Directives yang mengubah penampilan atau perilaku elemen.
Structural Directives
<!-- *ngIf: Conditional rendering -->
<div *ngIf="isLoggedIn; else loginTemplate">
<h2>Selamat datang, {{ user.name }}!</h2>
</div>
<ng-template #loginTemplate>
<h2>Silakan login terlebih dahulu</h2>
<button (click)="login()">Login</button>
</ng-template>
<!-- *ngFor: Looping daftar -->
<ul>
<li *ngFor="let product of products; let i = index;
trackBy: trackByProductId;
let isOdd = odd; let isEven = even"
[class.odd-row]="isOdd"
[class.even-row]="isEven">
{{ i + 1 }}. {{ product.name }} - {{ product.price | currency:'IDR' }}
</li>
</ul>
<!-- *ngSwitch: Switch case -->
<div [ngSwitch]="userRole">
<p *ngSwitchCase="'admin'">Panel Administrator</p>
<p *ngSwitchCase="'editor'">Panel Editor</p>
<p *ngSwitchCase="'user'">Panel Pengguna</p>
<p *ngSwitchDefault>Panel Tamu</p>
</div>
<!-- @if, @for, @switch (Angular 17+ Control Flow) -->
@if (isLoggedIn) {
<h2>Selamat datang, {{ user.name }}!</h2>
} @else {
<h2>Silakan login</h2>
}
@for (product of products; track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>Tidak ada produk ditemukan.</p>
}
Attribute Directives
// Membuat Custom Attribute Directive
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight: string = 'yellow';
@Input() defaultColor: string = 'transparent';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// Penggunaan di template:
// <p appHighlight="lightblue">Hover saya!</p>
// <p [appHighlight]="'yellow'" [defaultColor]="'white'">Hover!</p>
6. Services dan Dependency Injection
Services adalah kelas yang menyediakan logika bisnis, data sharing, dan fungsionalitas yang bisa digunakan oleh berbagai components. Angular menggunakan sistem Dependency Injection (DI) untuk menyediakan instance services ke komponen yang membutuhkan.
βββββββββββββββββββββββββββββββββββββββββββββββββββ β Angular DI Container β β β β βββββββββββββββββββββββββββββββββββββββββββββ β β β Injector (Root) β β β β β β β β βββββββββββββββ βββββββββββββββββββββ β β β β β UserService β β AuthService β β β β β β (singleton) β β (singleton) β β β β β ββββββββ¬ββββββββ ββββββββββ¬βββββββββββ β β β β β β β β β β ββββββ΄βββββ βββββ΄βββββ β β β β β β β β β β β β ββββΌβββ ββββΌβββ ββββΌβββ ββββΌβββ β β β β βUser β βUser β βLoginβ βDash β β β β β βList β βCard β βPage β βBoardβ β β β β β Cmp β β Cmp β β Cmp β β Cmp β β β β β ββββββββ βββββββ βββββββ βββββββ β β β βββββββββββββββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββ
Membuat dan Menggunakan Service
# Generate service menggunakan CLI ng generate service services/product # Atau shorthand: ng g s services/product
// product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, catchError, throwError } from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
category: string;
stock: number;
}
@Injectable({
providedIn: 'root' // Singleton tersedia di seluruh app
})
export class ProductService {
private apiUrl = 'https://api.example.com/products';
// BehaviorSubject untuk menyimpan state
private productsSubject = new BehaviorSubject<Product[]>([]);
public products$ = this.productsSubject.asObservable();
constructor(private http: HttpClient) {}
// GET semua produk
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl).pipe(
catchError(this.handleError)
);
}
// GET produk berdasarkan ID
getProductById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
// POST produk baru
createProduct(product: Omit<Product, 'id'>): Observable<Product> {
return this.http.post<Product>(this.apiUrl, product).pipe(
catchError(this.handleError)
);
}
// PUT update produk
updateProduct(id: number, product: Partial<Product>): Observable<Product> {
return this.http.put<Product>(`${this.apiUrl}/${id}`, product).pipe(
catchError(this.handleError)
);
}
// DELETE produk
deleteProduct(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
// Error handler
private handleError(error: any): Observable<never> {
console.error('API Error:', error);
return throwError(() => new Error(
error.message || 'Terjadi kesalahan pada server'
));
}
}
Providing Services β Level yang Berbeda
// 1. Root level (singleton di seluruh app)
@Injectable({ providedIn: 'root' })
export class GlobalService { }
// 2. Module level (singleton di dalam module)
@NgModule({
providers: [ModuleService]
})
export class FeatureModule { }
// 3. Component level (instance baru per component)
@Component({
selector: 'app-demo',
providers: [LocalService]
})
export class DemoComponent { }
// 4. Standalone component level
@Component({
selector: 'app-demo',
standalone: true,
providers: [LocalService] // Instance baru setiap kali
})
export class DemoComponent { }
7. Routing dan Navigasi
Angular Router memungkinkan kita membangun Single Page Application (SPA) dengan navigasi antar halaman tanpa reload browser. Router mendukung lazy loading, route guards, nested routes, dan banyak fitur lainnya.
Konfigurasi Routes
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () =>
import('./pages/home/home.component')
.then(m => m.HomeComponent),
title: 'Beranda - MyApp'
},
{
path: 'products',
loadComponent: () =>
import('./pages/products/products.component')
.then(m => m.ProductsComponent),
title: 'Produk - MyApp'
},
{
path: 'products/:id',
loadComponent: () =>
import('./pages/product-detail/product-detail.component')
.then(m => m.ProductDetailComponent),
title: 'Detail Produk - MyApp'
},
{
path: 'admin',
canActivate: [authGuard],
loadComponent: () =>
import('./pages/admin/admin.component')
.then(m => m.AdminComponent),
title: 'Admin Panel',
children: [
{
path: 'dashboard',
loadComponent: () =>
import('./pages/admin/dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'users',
loadComponent: () =>
import('./pages/admin/users/users.component')
.then(m => m.UsersComponent)
},
{
path: 'settings',
loadComponent: () =>
import('./pages/admin/settings/settings.component')
.then(m => m.SettingsComponent)
}
]
},
{
path: '**',
loadComponent: () =>
import('./pages/not-found/not-found.component')
.then(m => m.NotFoundComponent),
title: 'Halaman Tidak Ditemukan'
}
];
Router Outlet dan Navigasi
<!-- app.component.html --> <app-navbar></app-navbar> <main> <router-outlet></router-outlet> </main> <app-footer></app-footer> <!-- navbar.component.html --> <nav> <a routerLink="/home" routerLinkActive="active">Beranda</a> <a routerLink="/products" routerLinkActive="active">Produk</a> <a [routerLink]="['/products', 42]">Produk #42</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> </nav>
Route Guard
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect ke login dengan return URL
router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
};
// Guard untuk role-based access
export const roleGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const requiredRole = route.data['role'] as string;
return authService.hasRole(requiredRole);
};
Navigasi Programmatic
import { Router, ActivatedRoute } from '@angular/router';
@Component({ /* ... */ })
export class ProductComponent {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
// Navigasi sederhana
goToHome(): void {
this.router.navigate(['/home']);
}
// Navigasi dengan parameter
goToProduct(id: number): void {
this.router.navigate(['/products', id]);
}
// Navigasi dengan query params
searchProducts(query: string): void {
this.router.navigate(['/products'], {
queryParams: { q: query, page: 1 }
});
}
// Mengambil route params
ngOnInit(): void {
this.route.params.subscribe(params => {
const id = +params['id'];
this.loadProduct(id);
});
this.route.queryParams.subscribe(params => {
const query = params['q'] || '';
this.searchTerm = query;
});
}
}
8. RxJS: Reactive Programming
RxJS (Reactive Extensions for JavaScript) adalah library untuk reactive programming menggunakan Observables. Angular terintegrasi erat dengan RxJS untuk menangani operasi asynchronous seperti HTTP requests, event handling, dan state management.
Konsep Dasar RxJS
| Konsep | Penjelasan | Analogi |
|---|---|---|
| Observable | Stream data yang bisa dipublish seiring waktu | Kanal YouTube yang mengupload video berkala |
| Observer | Yang menerima data dari Observable | Subscriber YouTube yang menonton video |
| Subscription | Langganan aktif ke Observable | Tombol Subscribe yang ditekan |
| Operator | Fungsi untuk memanipulasi stream | Filter, edit, atau gabungkan video |
| Subject | Observable dan Observer sekaligus (multicasting) | Forum diskusi (bisa bicara dan mendengar) |
Observable dan Operators
import { Observable, of, from, interval, Subject, BehaviorSubject } from 'rxjs';
import {
map, filter, switchMap, mergeMap, debounceTime,
distinctUntilChanged, takeUntil, catchError,
tap, retry, delay, combineLatest
} from 'rxjs/operators';
// === 1. Membuat Observable ===
const numbers$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
// Subscribe ke Observable
numbers$.subscribe({
next: value => console.log('Nilai:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Selesai')
});
// === 2. Observable dari array / promise ===
const fruits$ = of('Apel', 'Mangga', 'Jeruk');
const numberArray$ = from([10, 20, 30, 40, 50]);
const timer$ = interval(1000); // Setiap 1 detik
// === 3. Operators: map, filter ===
const processed$ = numberArray$.pipe(
map(x => x * 2), // Kalikan 2
filter(x => x > 40), // Ambil yang > 40
tap(x => console.log('Processed:', x)) // Side effect
);
// Output: 60, 80, 100
// === 4. debounceTime untuk search ===
const searchInput$ = new Subject<string>();
searchInput$.pipe(
debounceTime(300), // Tunggu 300ms setelah ketikan
distinctUntilChanged(), // Hanya jika nilai berubah
filter(query => query.length >= 3), // Min 3 karakter
switchMap(query => this.searchService.search(query)) // Cancel prev
).subscribe(results => {
this.searchResults = results;
});
// Trigger search dari template:
// onSearch(event: Event) {
// this.searchInput$.next((event.target as HTMLInputElement).value);
// }
// === 5. combineLatest untuk data gabungan ===
const user$ = this.userService.getCurrentUser();
const settings$ = this.settingsService.getSettings();
combineLatest([user$, settings$]).pipe(
map(([user, settings]) => ({
...user,
theme: settings.theme,
language: settings.language
}))
).subscribe(combinedData => {
this.appState = combinedData;
});
// === 6. Subject dan BehaviorSubject ===
// Subject β tidak punya nilai awal
const clickSubject$ = new Subject<string>();
clickSubject$.subscribe(val => console.log('Click:', val));
clickSubject$.next('button-1');
// BehaviorSubject β punya nilai awal
const authState$ = new BehaviorSubject<boolean>(false);
authState$.subscribe(state => console.log('Auth:', state));
// Output: Auth: false (nilai awal)
authState$.next(true);
// Output: Auth: true
Penggunaan RxJS di Component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, takeUntil, switchMap, startWith, catchError, of } from 'rxjs';
import { ProductService, Product } from '../services/product.service';
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<input
placeholder="Cari produk..."
(input)="onSearch($event)"
>
<div *ngIf="loading" class="spinner">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngFor="let product of products" class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency:'IDR' }}</p>
</div>
`
})
export class ProductListComponent implements OnInit, OnDestroy {
products: Product[] = [];
loading = false;
error = '';
private destroy$ = new Subject<void>();
private search$ = new Subject<string>();
constructor(private productService: ProductService) {}
ngOnInit(): void {
// Reactive search dengan RxJS
this.search$.pipe(
startWith(''), // Load semua saat pertama
debounceTime(300),
distinctUntilChanged(),
tap(() => {
this.loading = true;
this.error = '';
}),
switchMap(query =>
this.productService.getProducts().pipe(
map(products =>
query
? products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase()))
: products
),
catchError(err => {
this.error = 'Gagal memuat data produk';
return of([]);
})
)
),
takeUntil(this.destroy$)
).subscribe(products => {
this.products = products;
this.loading = false;
});
}
onSearch(event: Event): void {
const query = (event.target as HTMLInputElement).value;
this.search$.next(query);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Selalu pastikan untuk unsubscribe dari Observable saat component di-destroy untuk menghindari memory leaks. Gunakan takeUntil pattern, async pipe di template, atau DestroyRef (Angular 16+).
9. HTTP Client dan API Communication
Angular menyediakan HttpClientModule yang terintegrasi dengan RxJS untuk melakukan HTTP requests ke backend API. Fitur ini mendukung interceptors, error handling, dan berbagai metode HTTP.
Mengkonfigurasi HttpClient
// app.config.ts β konfigurasi standalone
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor])
)
]
};
HTTP Interceptors
// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: 'Bearer ' + token
}
});
return next(cloned);
}
return next(req);
};
// interceptors/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 401:
router.navigate(['/login']);
break;
case 403:
alert('Anda tidak memiliki akses');
break;
case 404:
console.error('Resource tidak ditemukan');
break;
case 500:
alert('Terjadi kesalahan server');
break;
}
return throwError(() => error);
})
);
};
10. Form Handling di Angular
Angular menyediakan dua pendekatan untuk menangani forms: Template-Driven Forms dan Reactive Forms. Reactive Forms lebih disarankan untuk form kompleks karena memberikan kontrol lebih besar dan testability yang lebih baik.
Reactive Forms
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
ReactiveFormsModule,
FormBuilder,
FormGroup,
FormArray,
Validators
} from '@angular/forms';
@Component({
selector: 'app-registration',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="regForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Nama Lengkap</label>
<input formControlName="fullName" placeholder="Nama Anda">
<div class="error"
*ngIf="regForm.get('fullName')?.invalid &&
regForm.get('fullName')?.touched">
Nama wajib diisi (min 3 karakter)
</div>
</div>
<div class="form-group">
<label>Email</label>
<input formControlName="email" type="email">
<div class="error"
*ngIf="regForm.get('email')?.errors?.['required'] &&
regForm.get('email')?.touched">
Email wajib diisi
</div>
<div class="error"
*ngIf="regForm.get('email')?.errors?.['email'] &&
regForm.get('email')?.touched">
Format email tidak valid
</div>
</div>
<div formGroupName="password">
<div class="form-group">
<label>Password</label>
<input formControlName="pass" type="password">
</div>
<div class="form-group">
<label>Konfirmasi Password</label>
<input formControlName="confirmPass" type="password">
</div>
</div>
<div formArrayName="skills">
<label>Keahlian</label>
<div *ngFor="let skill of skills.controls; let i = index">
<input [formControlName]="i">
<button type="button" (click)="removeSkill(i)">Hapus</button>
</div>
<button type="button" (click)="addSkill()">+ Tambah Skill</button>
</div>
<button type="submit" [disabled]="regForm.invalid">
Daftar
</button>
</form>
`
})
export class RegistrationComponent implements OnInit {
regForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.regForm = this.fb.group({
fullName: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: this.fb.group({
pass: ['', [Validators.required, Validators.minLength(8)]],
confirmPass: ['', Validators.required]
}),
skills: this.fb.array([
this.fb.control('')
])
});
}
get skills(): FormArray {
return this.regForm.get('skills') as FormArray;
}
addSkill(): void {
this.skills.push(this.fb.control(''));
}
removeSkill(index: number): void {
this.skills.removeAt(index);
}
onSubmit(): void {
if (this.regForm.valid) {
console.log('Form Data:', this.regForm.value);
} else {
this.regForm.markAllAsTouched();
}
}
}
11. Best Practices dan Tips
Berikut adalah beberapa best practices yang harus diikuti saat mengembangkan aplikasi Angular untuk menghasilkan kode yang bersih, scalable, dan mudah di-maintain.
Daftar Best Practices
| Praktik | Penjelasan |
|---|---|
| Gunakan Standalone Components | Angular 15+ mendukung standalone β lebih ringan tanpa NgModule boilerplate |
| Lazy Loading Routes | Gunakan loadComponent() untuk memuat halaman hanya saat dibutuhkan |
| OnPush Change Detection | Gunakan ChangeDetectionStrategy.OnPush untuk performa lebih baik |
| Avoid Logic di Template | Simpan logika di component class, bukan di template HTML |
| Unsubscribe Properly | Gunakan takeUntil, async pipe, atau DestroyRef |
| Folder Structure Rapi | Group berdasarkan fitur, bukan berdasarkan tipe file |
| Environment Variables | Gunakan environment.ts untuk konfigurasi berbeda (dev, staging, prod) |
| Unit Testing | Test setiap component dan service menggunakan Jasmine/Karma atau Jest |
// === OnPush Change Detection ===
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-user-display',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="user-display">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserDisplayComponent {
@Input({ required: true }) user!: { name: string; email: string };
}
// === Signal-based Components (Angular 16+) ===
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<h2>Count: {{ count() }}</h2>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
`
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
constructor() {
// Effect β runs when signals change
effect(() => {
console.log('Count changed to:', this.count());
});
}
increment(): void {
this.count.update(n => n + 1);
}
decrement(): void {
this.count.update(n => n - 1);
}
}
Angular 16+ memperkenalkan Signals sebagai alternatif dari RxJS untuk state management sederhana. Signals menyediakan reactive state dengan API yang lebih intuitif: signal(), computed(), dan effect(). Ini membantu mengurangi boilerplate dan mempercepat development.
12. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Angular: