Web Development

Angular: Framework Web Enterprise

Tutorial lengkap belajar Angular dari nol β€” components, services, routing, RxJS, dependency injection, dan best practices untuk membangun aplikasi enterprise yang scalable

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 NativeAngular dibangun dengan TypeScript yang menyediakan type safety, autocompletion, dan refactoring yang lebih baik
Full FrameworkSemua tools sudah tersedia dari awal β€” routing, HTTP, forms, testing, i18n
Dependency InjectionSistem DI bawaan yang powerful untuk mengelola dependencies antar komponen
CLI yang PowerfulAngular CLI memudahkan generate components, services, testing, dan build production
Enterprise ReadyBanyak digunakan di perusahaan besar seperti Google, Microsoft, Deutsche Bank
RxJS IntegrationDukungan reactive programming bawaan untuk menangani async operations

Angular vs Framework Lain

Aspek Angular React Vue.js
TipeFull FrameworkLibraryFramework
BahasaTypeScriptJSX/TSXJavaScript/Template
Ukuran~143 KB (gzip)~42 KB (gzip)~33 KB (gzip)
Learning CurveπŸ”΄ Curam🟑 Sedang🟒 Mudah
State ManagementNgRx / Services / SignalsRedux / ZustandVuex / Pinia
DOMIncremental DOMVirtual DOMVirtual DOM
Cocok untukEnterprise, Banking, SPA besarSPA, Mobile AppSPA kecil-sedang
Diagram: Arsitektur Angular Application
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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  β”‚ β”‚ β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’‘ Angular vs AngularJS

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

Instalasi Angular CLI

bash
# 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

text
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
⚠️ Standalone Components (Angular 17+)

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

bash
# 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

typescript
// 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';
  }
}
html
<!-- 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
ngOnChangesSaat input properties berubahReact terhadap perubahan data dari parent
ngOnInitSetelah component pertama kali diinisialisasiInisialisasi data, fetch dari API
ngDoCheckSaat change detection berjalanCustom change detection
ngAfterContentInitSetelah content (ng-content) di-proyeksikanAkses projected content
ngAfterViewInitSetelah view dan child views diinisialisasiAkses DOM, inisialisasi chart
ngOnDestroySaat component di-destroyCleanup subscriptions, timers
typescript
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"
html
<!-- 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>
πŸ’‘ Pipe untuk Transformasi Data

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

html
<!-- *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

typescript
// 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.

Diagram: Dependency Injection di Angular
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              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

bash
# Generate service menggunakan CLI
ng generate service services/product
# Atau shorthand:
ng g s services/product
typescript
// 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

typescript
// 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

typescript
// 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

html
<!-- 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

typescript
// 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

typescript
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
ObservableStream data yang bisa dipublish seiring waktuKanal YouTube yang mengupload video berkala
ObserverYang menerima data dari ObservableSubscriber YouTube yang menonton video
SubscriptionLangganan aktif ke ObservableTombol Subscribe yang ditekan
OperatorFungsi untuk memanipulasi streamFilter, edit, atau gabungkan video
SubjectObservable dan Observer sekaligus (multicasting)Forum diskusi (bisa bicara dan mendengar)

Observable dan Operators

typescript
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

typescript
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 Unsubscribe!

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

typescript
// 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

typescript
// 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

typescript
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 ComponentsAngular 15+ mendukung standalone β€” lebih ringan tanpa NgModule boilerplate
Lazy Loading RoutesGunakan loadComponent() untuk memuat halaman hanya saat dibutuhkan
OnPush Change DetectionGunakan ChangeDetectionStrategy.OnPush untuk performa lebih baik
Avoid Logic di TemplateSimpan logika di component class, bukan di template HTML
Unsubscribe ProperlyGunakan takeUntil, async pipe, atau DestroyRef
Folder Structure RapiGroup berdasarkan fitur, bukan berdasarkan tipe file
Environment VariablesGunakan environment.ts untuk konfigurasi berbeda (dev, staging, prod)
Unit TestingTest setiap component dan service menggunakan Jasmine/Karma atau Jest
typescript
// === 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 Signals (v16+)

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:

Pertanyaan 1: Apa bahasa pemrograman utama yang digunakan oleh Angular?

a) JavaScript
b) TypeScript
c) Dart
d) CoffeeScript

Pertanyaan 2: Apa fungsi dari Dependency Injection di Angular?

a) Untuk meng-upload file ke server
b) Untuk mengelola CSS styling
c) Untuk menyediakan instance services ke komponen yang membutuhkan
d) Untuk membuat animasi

Pertanyaan 3: Operator RxJS apa yang digunakan untuk membatalkan HTTP request sebelumnya saat ada request baru?

a) mergeMap
b) concatMap
c) switchMap
d) exhaustMap

Pertanyaan 4: Apa perbedaan utama antara Template-Driven Forms dan Reactive Forms?

a) Tidak ada perbedaan
b) Reactive Forms didefinisikan di component class dengan kontrol lebih besar, Template-Driven didefinisikan di template
c) Template-Driven lebih cepat performanya
d) Reactive Forms tidak bisa digunakan dengan validasi

Pertanyaan 5: Apa fungsi dari router-outlet di Angular?

a) Menampilkan loading indicator
b) Menjadi tempat di-render-nya component berdasarkan route aktif
c) Mengatur tema aplikasi
d) Meng-handle error routing
πŸ” Zoom
100%
🎨 Tema