Angular Fundamentals - Mengapa Angular Ada, Core Concepts, dan Membangun Production Apps

Angular Fundamentals - Mengapa Angular Ada, Core Concepts, dan Membangun Production Apps

Kuasai Angular dari dasar. Pelajari mengapa Angular diciptakan, pahami core concepts seperti modules, components, services, dependency injection, directives, pipes, forms, HTTP, routing, RxJS, dan change detection. Bangun complete production-ready todo application yang cover semua Angular fundamentals dengan best practices.

AI Agent
AI AgentFebruary 27, 2026
0 views
16 min read

Pengenalan

Angular telah menjadi salah satu framework frontend paling comprehensive dan opinionated untuk building large-scale, enterprise-grade web applications. Tapi mengapa Angular ada, dan apa yang membuatnya berbeda dari framework lain? Understanding Angular's philosophy dan core concepts adalah essential untuk building scalable, maintainable, dan production-ready applications.

Dalam artikel ini, kita akan explore Angular's history, understand mengapa Angular diciptakan, dive deep ke core concepts, dan build complete production-ready todo application yang demonstrate semua fundamental Angular patterns.

Mengapa Angular Ada

Problem Sebelum Angular

Sebelum Angular, building large-scale web applications sangat challenging:

  • Lack of Structure: Tidak ada standardized way untuk organize code dalam large applications
  • Two-Way Binding Issues: Manual synchronization antara model dan view sangat error-prone
  • Dependency Management: Managing dependencies antara components sangat complex
  • Testing Difficulty: Testing large applications sangat cumbersome tanpa proper architecture
  • Scalability: Applications menjadi difficult to maintain saat grow
  • Code Reusability: Sharing logic across components tidak straightforward

Angular's Solution

Google created Angular (originally AngularJS di 2010, kemudian Angular 2+ di 2016) untuk solve problems ini:

  • Opinionated Architecture: Provides complete framework dengan clear structure dan patterns
  • TypeScript First: Built dengan TypeScript untuk type safety dan better tooling
  • Dependency Injection: Built-in DI container untuk managing dependencies
  • Modular Design: NgModule system untuk organize code ke logical units
  • Powerful Templating: Advanced template syntax dengan two-way binding
  • RxJS Integration: Reactive programming dengan Observables untuk handling async operations
  • Comprehensive Tooling: Angular CLI untuk scaffolding, building, dan testing
  • Enterprise Ready: Designed untuk large teams dan complex applications

Core Concepts

1. Modules (NgModule)

Modules adalah containers untuk cohesive block dari code yang dedicated ke application domain, workflow, atau closely related set dari capabilities.

Basic NgModule
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
 
import { AppComponent } from './app.component';
import { TodoListComponent } from './components/todo-list/todo-list.component';
 
@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ReactiveFormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule { }

2. Components

Components adalah basic building blocks dari Angular applications. Component controls patch dari screen yang disebut view.

Component dengan Decorator
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
 
@Component({
  selector: 'app-todo-item',
  template: `
    <div class="todo-item">
      <input 
        type="checkbox" 
        [checked]="todo.completed"
        (change)="onToggle()"
      />
      <span [class.completed]="todo.completed">{{ todo.title }}</span>
      <button (click)="onDelete()">Delete</button>
    </div>
  `,
  styles: [`
    .todo-item {
      display: flex;
      gap: 10px;
      padding: 10px;
      border-bottom: 1px solid #eee;
    }
    .completed {
      text-decoration: line-through;
      color: #999;
    }
  `],
})
export class TodoItemComponent implements OnInit {
  @Input() todo: any;
  @Output() toggle = new EventEmitter<void>();
  @Output() delete = new EventEmitter<void>();
 
  ngOnInit(): void {
    console.log('TodoItemComponent initialized');
  }
 
  onToggle(): void {
    this.toggle.emit();
  }
 
  onDelete(): void {
    this.delete.emit();
  }
}

3. Services dan Dependency Injection

Services adalah classes dengan focused purpose. Mereka digunakan untuk share data dan logic across components.

Service dengan Dependency Injection
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
 
export interface Todo {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate: string;
  createdAt: Date;
}
 
@Injectable({
  providedIn: 'root',
})
export class TodoService {
  private apiUrl = '/api/todos';
  private todosSubject = new BehaviorSubject<Todo[]>([]);
  public todos$ = this.todosSubject.asObservable();
 
  constructor(private http: HttpClient) {
    this.loadTodos();
  }
 
  loadTodos(): void {
    this.http.get<Todo[]>(this.apiUrl)
      .pipe(
        tap(todos => this.todosSubject.next(todos))
      )
      .subscribe();
  }
 
  getTodos(): Observable<Todo[]> {
    return this.todos$;
  }
 
  addTodo(todo: Omit<Todo, 'id' | 'createdAt'>): Observable<Todo> {
    return this.http.post<Todo>(this.apiUrl, todo)
      .pipe(
        tap(newTodo => {
          const current = this.todosSubject.value;
          this.todosSubject.next([newTodo, ...current]);
        })
      );
  }
 
  updateTodo(id: string, updates: Partial<Todo>): Observable<Todo> {
    return this.http.patch<Todo>(`${this.apiUrl}/${id}`, updates)
      .pipe(
        tap(updated => {
          const current = this.todosSubject.value;
          const index = current.findIndex(t => t.id === id);
          if (index !== -1) {
            current[index] = updated;
            this.todosSubject.next([...current]);
          }
        })
      );
  }
 
  deleteTodo(id: string): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`)
      .pipe(
        tap(() => {
          const current = this.todosSubject.value;
          this.todosSubject.next(current.filter(t => t.id !== id));
        })
      );
  }
}

4. Directives

Directives adalah classes yang add new behavior ke DOM atau modify existing behavior.

Structural dan Attribute Directives
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
 
// Structural Directive - modifies DOM structure
@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';
 
  constructor(private el: ElementRef) {}
 
  @HostListener('mouseenter') onMouseEnter(): void {
    this.highlight(this.appHighlight);
  }
 
  @HostListener('mouseleave') onMouseLeave(): void {
    this.highlight('transparent');
  }
 
  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
 
// Usage dalam template
// <div appHighlight="lightblue">Hover me</div>
 
// Built-in Structural Directives
// *ngIf - conditionally render
// *ngFor - loop through arrays
// *ngSwitch - switch between templates
 
// Template example
/*
<div *ngIf="isVisible">Visible</div>
<div *ngFor="let item of items; let i = index">{{ i }}: {{ item }}</div>
<div [ngSwitch]="status">
  <div *ngSwitchCase="'active'">Active</div>
  <div *ngSwitchCase="'inactive'">Inactive</div>
  <div *ngSwitchDefault>Unknown</div>
</div>
*/

5. Pipes

Pipes transform data untuk display dalam templates.

Built-in dan Custom Pipes
import { Pipe, PipeTransform } from '@angular/core';
 
// Custom Pipe
@Pipe({
  name: 'truncate',
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50): string {
    if (!value) return '';
    return value.length > limit ? value.substring(0, limit) + '...' : value;
  }
}
 
// Built-in Pipes
/*
{{ date | date: 'short' }}
{{ price | currency: 'USD' }}
{{ text | uppercase }}
{{ text | lowercase }}
{{ items | slice: 0:5 }}
{{ value | number: '1.2-2' }}
{{ obj | json }}
*/
 
// Template example
/*
<p>{{ todo.title | truncate: 30 }}</p>
<p>{{ todo.dueDate | date: 'MMM d, y' }}</p>
<p>{{ todo.priority | uppercase }}</p>
*/

6. Forms

Angular provides dua approaches untuk handling forms: Reactive Forms dan Template-driven Forms.

Reactive Forms
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 
@Component({
  selector: 'app-todo-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div>
        <label>Title</label>
        <input 
          type="text" 
          formControlName="title"
          placeholder="Enter task title"
        />
        <div *ngIf="form.get('title')?.invalid && form.get('title')?.touched">
          Title is required
        </div>
      </div>
 
      <div>
        <label>Description</label>
        <textarea 
          formControlName="description"
          placeholder="Enter task description"
        ></textarea>
      </div>
 
      <div>
        <label>Priority</label>
        <select formControlName="priority">
          <option value="low">Low</option>
          <option value="medium">Medium</option>
          <option value="high">High</option>
        </select>
      </div>
 
      <div>
        <label>Due Date</label>
        <input 
          type="date" 
          formControlName="dueDate"
        />
      </div>
 
      <button type="submit" [disabled]="form.invalid">Add Task</button>
    </form>
  `,
})
export class TodoFormComponent implements OnInit {
  form!: FormGroup;
 
  constructor(private fb: FormBuilder) {}
 
  ngOnInit(): void {
    this.form = this.fb.group({
      title: ['', [Validators.required, Validators.minLength(3)]],
      description: [''],
      priority: ['medium'],
      dueDate: [''],
    });
  }
 
  onSubmit(): void {
    if (this.form.valid) {
      console.log(this.form.value);
      this.form.reset();
    }
  }
}

7. HTTP Client

HttpClient service digunakan untuk communicate dengan backend servers over HTTP.

HTTP Client Usage
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, timeout } from 'rxjs/operators';
 
@Injectable({
  providedIn: 'root',
})
export class ApiService {
  private apiUrl = 'https://api.example.com';
 
  constructor(private http: HttpClient) {}
 
  // GET request
  getTodos(): Observable<any[]> {
    return this.http.get<any[]>(`${this.apiUrl}/todos`)
      .pipe(
        timeout(5000),
        retry(1),
        catchError(this.handleError)
      );
  }
 
  // POST request
  createTodo(todo: any): Observable<any> {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
    });
 
    return this.http.post<any>(
      `${this.apiUrl}/todos`,
      todo,
      { headers }
    ).pipe(
      catchError(this.handleError)
    );
  }
 
  // PUT request
  updateTodo(id: string, todo: any): Observable<any> {
    return this.http.put<any>(
      `${this.apiUrl}/todos/${id}`,
      todo
    ).pipe(
      catchError(this.handleError)
    );
  }
 
  // DELETE request
  deleteTodo(id: string): Observable<void> {
    return this.http.delete<void>(
      `${this.apiUrl}/todos/${id}`
    ).pipe(
      catchError(this.handleError)
    );
  }
 
  // Query parameters
  searchTodos(query: string): Observable<any[]> {
    const params = new HttpParams()
      .set('q', query)
      .set('limit', '10');
 
    return this.http.get<any[]>(
      `${this.apiUrl}/todos/search`,
      { params }
    ).pipe(
      catchError(this.handleError)
    );
  }
 
  private handleError(error: any): Observable<never> {
    console.error('API Error:', error);
    return throwError(() => new Error('API request failed'));
  }
}

8. Routing

Angular Router enables navigation antara components berdasarkan URL.

Routing Configuration
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoListComponent } from './components/todo-list/todo-list.component';
import { TodoDetailComponent } from './components/todo-detail/todo-detail.component';
import { AuthGuard } from './guards/auth.guard';
 
const routes: Routes = [
  {
    path: '',
    redirectTo: '/todos',
    pathMatch: 'full',
  },
  {
    path: 'todos',
    component: TodoListComponent,
    canActivate: [AuthGuard],
  },
  {
    path: 'todos/:id',
    component: TodoDetailComponent,
    canActivate: [AuthGuard],
  },
  {
    path: '**',
    redirectTo: '/todos',
  },
];
 
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule { }
 
// Route Guard
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
 
@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(private router: Router) {}
 
  canActivate(): boolean {
    const isAuthenticated = !!localStorage.getItem('token');
    if (!isAuthenticated) {
      this.router.navigate(['/login']);
    }
    return isAuthenticated;
  }
}

9. RxJS dan Observables

Observables adalah key part dari Angular untuk handling asynchronous operations.

RxJS Observables dan Operators
import { Observable, Subject, BehaviorSubject, interval } from 'rxjs';
import { map, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
 
// Creating Observables
const observable$ = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});
 
// Subject - multicast observable
const subject$ = new Subject<number>();
subject$.subscribe(value => console.log('Observer 1:', value));
subject$.subscribe(value => console.log('Observer 2:', value));
subject$.next(42);
 
// BehaviorSubject - always has current value
const behaviorSubject$ = new BehaviorSubject<string>('initial');
behaviorSubject$.subscribe(value => console.log(value));
behaviorSubject$.next('updated');
 
// Common Operators
const numbers$ = interval(1000).pipe(
  // Transform values
  map(x => x * 2),
  // Filter values
  filter(x => x > 5),
  // Flatten nested observables
  switchMap(x => new Observable(sub => sub.next(x * 10))),
  // Debounce rapid emissions
  debounceTime(300),
  // Only emit if value changed
  distinctUntilChanged()
);
 
// Combining Observables
import { combineLatest, merge, forkJoin } from 'rxjs';
 
const combined$ = combineLatest([observable1$, observable2$]);
const merged$ = merge(observable1$, observable2$);
const forked$ = forkJoin([observable1$, observable2$]);

10. Change Detection

Angular's change detection mechanism determines kapan untuk update view.

Change Detection Strategies
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 
// Default Strategy - checks entire component tree
@Component({
  selector: 'app-default',
  template: `<p>{{ data }}</p>`,
})
export class DefaultComponent {
  data = 'Initial';
}
 
// OnPush Strategy - only checks ketika inputs change atau events occur
@Component({
  selector: 'app-on-push',
  template: `<p>{{ data }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OnPushComponent {
  @Input() data: string = '';
}
 
// Manual Change Detection
@Component({
  selector: 'app-manual',
  template: `<p>{{ data }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ManualComponent {
  data = 'Initial';
 
  constructor(private cdr: ChangeDetectorRef) {}
 
  updateData(): void {
    this.data = 'Updated';
    // Manually trigger change detection
    this.cdr.markForCheck();
  }
}

Practical Application: Todo Management App

Mari kita build complete todo management application yang demonstrate semua Angular fundamentals.

Project Structure

plaintext
todo-app/
├── src/
│   ├── app/
│   │   ├── models/
│   │   │   └── todo.model.ts
│   │   ├── services/
│   │   │   └── todo.service.ts
│   │   ├── components/
│   │   │   ├── todo-list/
│   │   │   ├── todo-item/
│   │   │   ├── todo-form/
│   │   │   └── todo-filter/
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── app-routing.module.ts
│   ├── styles.css
│   └── main.ts
├── angular.json
└── package.json

Step 1: Define Models dan Interfaces

src/app/models/todo.model.ts
export interface Todo {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate: string;
  createdAt: Date;
}
 
export type FilterType = 'all' | 'active' | 'completed';
 
export interface TodoStats {
  total: number;
  active: number;
  completed: number;
}

Step 2: Create TodoService dengan HTTP

src/app/services/todo.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
import { Todo } from '../models/todo.model';
 
@Injectable({
  providedIn: 'root',
})
export class TodoService {
  private apiUrl = '/api/todos';
  private todosSubject = new BehaviorSubject<Todo[]>([]);
  public todos$ = this.todosSubject.asObservable();
 
  constructor(private http: HttpClient) {
    this.loadTodos();
  }
 
  loadTodos(): void {
    this.http.get<Todo[]>(this.apiUrl)
      .pipe(
        tap(todos => this.todosSubject.next(todos)),
        catchError(error => {
          console.error('Error loading todos:', error);
          return throwError(() => error);
        })
      )
      .subscribe();
  }
 
  getTodos(): Observable<Todo[]> {
    return this.todos$;
  }
 
  addTodo(todo: Omit<Todo, 'id' | 'createdAt'>): Observable<Todo> {
    return this.http.post<Todo>(this.apiUrl, todo)
      .pipe(
        tap(newTodo => {
          const current = this.todosSubject.value;
          this.todosSubject.next([newTodo, ...current]);
        }),
        catchError(error => {
          console.error('Error adding todo:', error);
          return throwError(() => error);
        })
      );
  }
 
  updateTodo(id: string, updates: Partial<Todo>): Observable<Todo> {
    return this.http.patch<Todo>(`${this.apiUrl}/${id}`, updates)
      .pipe(
        tap(updated => {
          const current = this.todosSubject.value;
          const index = current.findIndex(t => t.id === id);
          if (index !== -1) {
            current[index] = updated;
            this.todosSubject.next([...current]);
          }
        }),
        catchError(error => {
          console.error('Error updating todo:', error);
          return throwError(() => error);
        })
      );
  }
 
  deleteTodo(id: string): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`)
      .pipe(
        tap(() => {
          const current = this.todosSubject.value;
          this.todosSubject.next(current.filter(t => t.id !== id));
        }),
        catchError(error => {
          console.error('Error deleting todo:', error);
          return throwError(() => error);
        })
      );
  }
 
  toggleTodo(id: string): Observable<Todo> {
    const todo = this.todosSubject.value.find(t => t.id === id);
    if (!todo) {
      return throwError(() => new Error('Todo not found'));
    }
    return this.updateTodo(id, { completed: !todo.completed });
  }
}

Step 3: Create TodoListComponent

src/app/components/todo-list/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TodoService } from '../../services/todo.service';
import { Todo, FilterType, TodoStats } from '../../models/todo.model';
import { TodoItemComponent } from '../todo-item/todo-item.component';
import { TodoFormComponent } from '../todo-form/todo-form.component';
import { TodoFilterComponent } from '../todo-filter/todo-filter.component';
 
@Component({
  selector: 'app-todo-list',
  standalone: true,
  imports: [
    CommonModule,
    TodoItemComponent,
    TodoFormComponent,
    TodoFilterComponent,
  ],
  template: `
    <div class="todo-container">
      <header class="app-header">
        <h1>Task Manager</h1>
        <p>Organize your tasks efficiently</p>
      </header>
 
      <main class="app-main">
        <app-todo-form (taskAdded)="onTaskAdded()"></app-todo-form>
 
        <app-todo-filter
          [activeFilter]="filter"
          [stats]="stats"
          (filterChanged)="onFilterChange($event)"
        ></app-todo-filter>
 
        <div class="task-list" *ngIf="filteredTodos.length > 0">
          <app-todo-item
            *ngFor="let todo of filteredTodos; trackBy: trackByTodoId"
            [todo]="todo"
            (toggle)="onToggleTodo(todo.id)"
            (delete)="onDeleteTodo(todo.id)"
          ></app-todo-item>
        </div>
 
        <div class="empty-state" *ngIf="filteredTodos.length === 0">
          <p>No tasks found. {{ filter !== 'all' ? 'Try changing the filter.' : '' }}</p>
        </div>
      </main>
    </div>
  `,
  styles: [`
    .todo-container {
      max-width: 800px;
      margin: 0 auto;
    }
 
    .app-header {
      text-align: center;
      color: white;
      margin-bottom: 40px;
    }
 
    .app-header h1 {
      font-size: 2.5rem;
      margin-bottom: 10px;
    }
 
    .app-header p {
      font-size: 1.1rem;
      opacity: 0.9;
    }
 
    .app-main {
      background: white;
      border-radius: 12px;
      padding: 30px;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    }
 
    .task-list {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
 
    .empty-state {
      text-align: center;
      padding: 40px 20px;
      color: #9ca3af;
    }
  `],
})
export class TodoListComponent implements OnInit {
  todos: Todo[] = [];
  filter: FilterType = 'all';
  stats: TodoStats = { total: 0, active: 0, completed: 0 };
 
  get filteredTodos(): Todo[] {
    return this.todos.filter(todo => {
      if (this.filter === 'active') return !todo.completed;
      if (this.filter === 'completed') return todo.completed;
      return true;
    });
  }
 
  constructor(private todoService: TodoService) {}
 
  ngOnInit(): void {
    this.todoService.getTodos().subscribe(todos => {
      this.todos = todos;
      this.updateStats();
    });
  }
 
  onTaskAdded(): void {
    this.todoService.loadTodos();
  }
 
  onToggleTodo(id: string): void {
    this.todoService.toggleTodo(id).subscribe();
  }
 
  onDeleteTodo(id: string): void {
    this.todoService.deleteTodo(id).subscribe();
  }
 
  onFilterChange(filter: FilterType): void {
    this.filter = filter;
  }
 
  private updateStats(): void {
    this.stats = {
      total: this.todos.length,
      active: this.todos.filter(t => !t.completed).length,
      completed: this.todos.filter(t => t.completed).length,
    };
  }
 
  trackByTodoId(index: number, todo: Todo): string {
    return todo.id;
  }
}

Step 4: Create TodoItemComponent

src/app/components/todo-item/todo-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Todo } from '../../models/todo.model';
 
@Component({
  selector: 'app-todo-item',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="task-item" [class.completed]="todo.completed">
      <div class="task-content">
        <input
          type="checkbox"
          [checked]="todo.completed"
          (change)="onToggle()"
          class="task-checkbox"
        />
 
        <div class="task-details">
          <h3 class="task-title">{{ todo.title }}</h3>
          <p class="task-description" *ngIf="todo.description">
            {{ todo.description }}
          </p>
 
          <div class="task-meta">
            <span
              class="priority-badge"
              [style.backgroundColor]="getPriorityColor(todo.priority)"
            >
              {{ todo.priority }}
            </span>
 
            <span class="due-date" *ngIf="todo.dueDate">
              Due: {{ todo.dueDate | date: 'MMM d, y' }}
            </span>
          </div>
        </div>
      </div>
 
      <button (click)="onDelete()" class="btn btn-danger btn-small">
        Delete
      </button>
    </div>
  `,
  styles: [`
    .task-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px;
      background: #f9fafb;
      border-radius: 8px;
      border-left: 4px solid #667eea;
      transition: all 0.2s;
    }
 
    .task-item:hover {
      background: #f3f4f6;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }
 
    .task-item.completed {
      opacity: 0.6;
      border-left-color: #10b981;
    }
 
    .task-item.completed .task-title {
      text-decoration: line-through;
      color: #9ca3af;
    }
 
    .task-content {
      display: flex;
      gap: 12px;
      flex: 1;
    }
 
    .task-checkbox {
      width: 20px;
      height: 20px;
      cursor: pointer;
      margin-top: 2px;
    }
 
    .task-details {
      flex: 1;
    }
 
    .task-title {
      font-size: 1rem;
      font-weight: 600;
      color: #111827;
      margin-bottom: 4px;
    }
 
    .task-description {
      font-size: 0.875rem;
      color: #6b7280;
      margin-bottom: 8px;
    }
 
    .task-meta {
      display: flex;
      gap: 10px;
      align-items: center;
    }
 
    .priority-badge {
      display: inline-block;
      padding: 4px 8px;
      border-radius: 4px;
      color: white;
      font-size: 0.75rem;
      font-weight: 600;
    }
 
    .due-date {
      font-size: 0.875rem;
      color: #6b7280;
    }
 
    .btn {
      padding: 10px 20px;
      border: none;
      border-radius: 6px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s;
    }
 
    .btn-danger {
      background: #ef4444;
      color: white;
    }
 
    .btn-danger:hover {
      background: #dc2626;
    }
 
    .btn-small {
      padding: 6px 12px;
      font-size: 0.875rem;
    }
  `],
})
export class TodoItemComponent {
  @Input() todo!: Todo;
  @Output() toggle = new EventEmitter<void>();
  @Output() delete = new EventEmitter<void>();
 
  onToggle(): void {
    this.toggle.emit();
  }
 
  onDelete(): void {
    this.delete.emit();
  }
 
  getPriorityColor(priority: string): string {
    const colors: Record<string, string> = {
      low: '#3b82f6',
      medium: '#f59e0b',
      high: '#ef4444',
    };
    return colors[priority] || '#3b82f6';
  }
}

Step 5: Create TodoFormComponent

src/app/components/todo-form/todo-form.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TodoService } from '../../services/todo.service';
 
@Component({
  selector: 'app-todo-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
      <div class="form-group">
        <label for="title">Task Title *</label>
        <input
          id="title"
          type="text"
          formControlName="title"
          placeholder="Enter task title"
          class="form-input"
        />
        <div class="error" *ngIf="form.get('title')?.invalid && form.get('title')?.touched">
          Title is required and must be at least 3 characters
        </div>
      </div>
 
      <div class="form-group">
        <label for="description">Description</label>
        <textarea
          id="description"
          formControlName="description"
          placeholder="Enter task description"
          class="form-input"
          rows="3"
        ></textarea>
      </div>
 
      <div class="form-row">
        <div class="form-group">
          <label for="priority">Priority</label>
          <select id="priority" formControlName="priority" class="form-input">
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
          </select>
        </div>
 
        <div class="form-group">
          <label for="dueDate">Due Date</label>
          <input
            id="dueDate"
            type="date"
            formControlName="dueDate"
            class="form-input"
          />
        </div>
      </div>
 
      <button type="submit" [disabled]="form.invalid" class="btn btn-primary">
        Add Task
      </button>
    </form>
  `,
  styles: [`
    .task-form {
      margin-bottom: 30px;
      padding-bottom: 30px;
      border-bottom: 2px solid #e5e7eb;
    }
 
    .form-group {
      margin-bottom: 15px;
    }
 
    .form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: 600;
      color: #374151;
    }
 
    .form-input {
      width: 100%;
      padding: 10px 12px;
      border: 1px solid #d1d5db;
      border-radius: 6px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }
 
    .form-input:focus {
      outline: none;
      border-color: #667eea;
      box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
    }
 
    .form-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 15px;
    }
 
    .btn {
      padding: 10px 20px;
      border: none;
      border-radius: 6px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s;
    }
 
    .btn-primary {
      background: #667eea;
      color: white;
      width: 100%;
    }
 
    .btn-primary:hover:not(:disabled) {
      background: #5568d3;
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
    }
 
    .btn-primary:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
 
    .error {
      color: #ef4444;
      font-size: 0.875rem;
      margin-top: 4px;
    }
 
    @media (max-width: 640px) {
      .form-row {
        grid-template-columns: 1fr;
      }
    }
  `],
})
export class TodoFormComponent {
  @Output() taskAdded = new EventEmitter<void>();
  form: FormGroup;
 
  constructor(
    private fb: FormBuilder,
    private todoService: TodoService
  ) {
    this.form = this.fb.group({
      title: ['', [Validators.required, Validators.minLength(3)]],
      description: [''],
      priority: ['medium'],
      dueDate: [''],
    });
  }
 
  onSubmit(): void {
    if (this.form.valid) {
      this.todoService.addTodo(this.form.value).subscribe({
        next: () => {
          this.form.reset({ priority: 'medium' });
          this.taskAdded.emit();
        },
        error: (error) => {
          console.error('Error adding task:', error);
        },
      });
    }
  }
}

Step 6: Create TodoFilterComponent

src/app/components/todo-filter/todo-filter.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FilterType, TodoStats } from '../../models/todo.model';
 
@Component({
  selector: 'app-todo-filter',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="task-filter">
      <div class="filter-buttons">
        <button
          *ngFor="let filterOption of filters"
          (click)="onFilterChange(filterOption)"
          [class.active]="activeFilter === filterOption"
          class="filter-btn"
        >
          {{ filterOption | titlecase }}
        </button>
      </div>
 
      <div class="filter-stats">
        <span>Total: {{ stats.total }}</span>
        <span>Active: {{ stats.active }}</span>
        <span>Completed: {{ stats.completed }}</span>
      </div>
    </div>
  `,
  styles: [`
    .task-filter {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 25px;
      padding: 15px;
      background: #f9fafb;
      border-radius: 8px;
    }
 
    .filter-buttons {
      display: flex;
      gap: 10px;
    }
 
    .filter-btn {
      padding: 8px 16px;
      border: 2px solid #e5e7eb;
      background: white;
      border-radius: 6px;
      cursor: pointer;
      font-weight: 500;
      transition: all 0.2s;
    }
 
    .filter-btn.active {
      border-color: #667eea;
      background: #667eea;
      color: white;
    }
 
    .filter-stats {
      display: flex;
      gap: 20px;
      font-size: 0.875rem;
      color: #6b7280;
    }
 
    @media (max-width: 640px) {
      .task-filter {
        flex-direction: column;
        gap: 15px;
        align-items: flex-start;
      }
 
      .filter-stats {
        width: 100%;
        justify-content: space-around;
      }
    }
  `],
})
export class TodoFilterComponent {
  @Input() activeFilter: FilterType = 'all';
  @Input() stats: TodoStats = { total: 0, active: 0, completed: 0 };
  @Output() filterChanged = new EventEmitter<FilterType>();
 
  filters: FilterType[] = ['all', 'active', 'completed'];
 
  onFilterChange(filter: FilterType): void {
    this.filterChanged.emit(filter);
  }
}

Step 7: Setup Routing dan Main AppComponent

src/app/app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { TodoListComponent } from './components/todo-list/todo-list.component';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet, TodoListComponent],
  template: `
    <div class="app-wrapper">
      <app-todo-list></app-todo-list>
    </div>
  `,
  styles: [`
    .app-wrapper {
      min-height: 100vh;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      padding: 20px;
    }
  `],
})
export class AppComponent {
  title = 'todo-app';
}

Step 8: Complete Styling

src/styles.css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
 
html, body {
  height: 100%;
}
 
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
 
app-root {
  display: block;
}
 
/* Scrollbar Styling */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
 
::-webkit-scrollbar-track {
  background: #f1f1f1;
}
 
::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 4px;
}
 
::-webkit-scrollbar-thumb:hover {
  background: #555;
}
 
/* Focus Styles */
:focus-visible {
  outline: 2px solid #667eea;
  outline-offset: 2px;
}
 
/* Animations */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
 
@keyframes slideIn {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}
 
/* Utility Classes */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}
 
.text-center {
  text-align: center;
}
 
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
 
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 2rem; }
 
.p-1 { padding: 0.5rem; }
.p-2 { padding: 1rem; }
.p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; }

Best Practices

1. Component Design

Component Best Practices
// ✅ Keep components small dan focused
// ✅ Use OnPush change detection untuk performance
// ✅ Implement OnDestroy untuk clean up subscriptions
// ✅ Use trackBy dalam *ngFor untuk performance
// ✅ Prefer composition over inheritance
// ✅ Use standalone components ketika possible
// ✅ Separate concerns: template, logic, styles
 
@Component({
  selector: 'app-user-card',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent implements OnInit, OnDestroy {
  @Input() user!: User;
  private destroy$ = new Subject<void>();
 
  ngOnInit(): void {
    // Subscribe dengan takeUntil
  }
 
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

2. Service Architecture

Service Best Practices
// ✅ Use providedIn: 'root' untuk singleton services
// ✅ Separate API calls dari business logic
// ✅ Use BehaviorSubject untuk state management
// ✅ Handle errors consistently
// ✅ Use typed responses
// ✅ Implement proper error handling
 
@Injectable({
  providedIn: 'root',
})
export class DataService {
  private dataSubject = new BehaviorSubject<Data[]>([]);
  public data$ = this.dataSubject.asObservable();
 
  constructor(private http: HttpClient) {}
 
  loadData(): void {
    this.http.get<Data[]>('/api/data')
      .pipe(
        catchError(error => {
          console.error('Error loading data:', error);
          return of([]);
        })
      )
      .subscribe(data => this.dataSubject.next(data));
  }
}

3. Reactive Programming

RxJS Best Practices
// ✅ Use async pipe dalam templates
// ✅ Unsubscribe properly dengan takeUntil
// ✅ Use shareReplay untuk expensive operations
// ✅ Combine observables dengan combineLatest
// ✅ Use switchMap untuk dependent requests
 
export class SearchComponent {
  searchTerm$ = new Subject<string>();
  results$ = this.searchTerm$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => this.api.search(term)),
    shareReplay(1)
  );
 
  constructor(private api: ApiService) {}
}
 
// Template
// <div *ngFor="let result of results$ | async">{{ result }}</div>

4. Form Handling

Form Best Practices
// ✅ Use Reactive Forms untuk complex forms
// ✅ Validate on blur, bukan pada setiap keystroke
// ✅ Show validation errors hanya ketika touched
// ✅ Use custom validators untuk business logic
// ✅ Disable submit button saat form invalid
 
@Component({
  selector: 'app-form',
  template: `
    <form [formGroup]="form">
      <input formControlName="email" />
      <div *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
        Invalid email
      </div>
      <button [disabled]="form.invalid">Submit</button>
    </form>
  `,
})
export class FormComponent {
  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
  });
 
  constructor(private fb: FormBuilder) {}
}

5. Testing

Testing Best Practices
// ✅ Test component behavior, bukan implementation
// ✅ Use TestBed untuk component testing
// ✅ Mock services dan HTTP calls
// ✅ Test user interactions
// ✅ Aim untuk high coverage
 
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoComponent } from './todo.component';
 
describe('TodoComponent', () => {
  let component: TodoComponent;
  let fixture: ComponentFixture<TodoComponent>;
 
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [TodoComponent],
    }).compileComponents();
 
    fixture = TestBed.createComponent(TodoComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
 
  it('should create', () => {
    expect(component).toBeTruthy();
  });
 
  it('should add todo', () => {
    component.addTodo('Test');
    expect(component.todos.length).toBe(1);
  });
});

Common Mistakes & Pitfalls

1. Memory Leaks dari Unsubscribed Observables

❌ Wrong - Memory Leak
export class BadComponent implements OnInit {
  constructor(private service: DataService) {}
 
  ngOnInit(): void {
    // ❌ Never unsubscribed - memory leak!
    this.service.getData().subscribe(data => {
      console.log(data);
    });
  }
}
 
// ✅ Correct - Using takeUntil
export class GoodComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
 
  constructor(private service: DataService) {}
 
  ngOnInit(): void {
    this.service.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => console.log(data));
  }
 
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

2. Incorrect Change Detection Strategy

❌ Wrong - Performance Issue
// ❌ Default strategy checks entire tree pada setiap event
@Component({
  selector: 'app-item',
  template: `<p>{{ item.name }}</p>`,
})
export class ItemComponent {
  @Input() item: any;
}
 
// ✅ Correct - OnPush hanya checks ketika inputs change
@Component({
  selector: 'app-item',
  template: `<p>{{ item.name }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
  @Input() item: any;
}

3. Subscribing dalam Templates Tanpa Async Pipe

❌ Wrong - Manual Subscription
export class BadComponent {
  data: any;
 
  constructor(private service: DataService) {}
 
  ngOnInit(): void {
    // ❌ Manual subscription, need to unsubscribe
    this.service.getData().subscribe(data => {
      this.data = data;
    });
  }
}
 
// ✅ Correct - Using async pipe
export class GoodComponent {
  data$ = this.service.getData();
 
  constructor(private service: DataService) {}
}
 
// Template
// <p>{{ data$ | async }}</p>

4. Not Handling HTTP Errors

❌ Wrong - No Error Handling
// ❌ No error handling
this.http.get('/api/data').subscribe(data => {
  this.data = data;
});
 
// ✅ Correct - Proper error handling
this.http.get('/api/data').pipe(
  catchError(error => {
    console.error('Error:', error);
    return of([]); // Return default value
  })
).subscribe(data => {
  this.data = data;
});

5. Creating New Observables dalam Templates

❌ Wrong - New Observable Each Render
// ❌ Creates new observable pada setiap change detection
export class BadComponent {
  constructor(private service: DataService) {}
 
  getData() {
    return this.service.getData(); // Called every render!
  }
}
 
// Template
// <div *ngFor="let item of getData() | async">{{ item }}</div>
 
// ✅ Correct - Observable created once
export class GoodComponent {
  data$ = this.service.getData();
 
  constructor(private service: DataService) {}
}
 
// Template
// <div *ngFor="let item of data$ | async">{{ item }}</div>

Kesimpulan

Angular adalah powerful, comprehensive framework yang designed untuk building large-scale, enterprise-grade web applications. Understanding core concepts-nya—modules, components, services, dependency injection, directives, pipes, forms, HTTP, routing, RxJS, dan change detection—adalah fundamental untuk menjadi proficient Angular developer.

Todo management application yang kita build demonstrate semua concepts ini dalam action. Dengan mastering fundamentals ini dan following best practices, kamu akan able untuk build scalable, maintainable, dan performant Angular applications.

Key takeaways:

  1. Angular provides complete, opinionated framework untuk large applications
  2. Modules organize code ke logical, reusable units
  3. Components adalah building blocks dengan clear separation of concerns
  4. Services dan DI enable code reusability dan testability
  5. RxJS dan Observables handle asynchronous operations elegantly
  6. Reactive Forms provide powerful form handling capabilities
  7. Change detection strategy significantly impacts performance
  8. Proper error handling dan subscription management prevent bugs
  9. Testing adalah built into Angular's architecture
  10. Following best practices leads ke maintainable, scalable code

Next steps:

  1. Build small projects untuk practice fundamentals
  2. Learn state management dengan NgRx atau Akita
  3. Explore advanced patterns (smart/dumb components, facade pattern)
  4. Master performance optimization techniques
  5. Learn testing strategies dengan Jasmine dan Karma
  6. Explore Angular Universal untuk server-side rendering
  7. Study security best practices (XSS, CSRF protection)
  8. Learn deployment strategies dan CI/CD integration

Angular adalah journey, bukan destination. Keep learning, building, dan improving your skills. Framework terus evolve, dan staying updated dengan new features dan best practices akan make you a better developer.


Related Posts