- 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 |
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
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
Anatomi Component
Membuat dan Menggunakan Service
// 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: