Angular

Essential Angular syntax, components, services, and best practices for building web applications.

frameworks
angulartypescriptfrontendspa

Component Basics

import { Component, Input, Output, EventEmitter } from '@angular/core';

// Basic component
@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `<h1>Hello, {{ name }}!</h1>`
})
export class GreetingComponent {
  @Input() name: string = '';
}

// Component with inputs and outputs
@Component({
  selector: 'app-button',
  standalone: true,
  template: `
    <button (click)="handleClick()">{{ text }}</button>
  `
})
export class ButtonComponent {
  @Input() text: string = 'Click me';
  @Output() clicked = new EventEmitter<void>();
  
  handleClick() {
    this.clicked.emit();
  }
}

// Component with external template
@Component({
  selector: 'app-card',
  standalone: true,
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent {
  @Input() title: string = '';
}

Template Syntax

<!-- Interpolation -->
<p>{{ message }}</p>
<p>{{ user.name }}</p>
<p>{{ getValue() }}</p>

<!-- Property binding -->
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="isDisabled">Click</button>
<div [class.active]="isActive"></div>
<div [style.color]="textColor"></div>

<!-- Event binding -->
<button (click)="handleClick()">Click</button>
<input (input)="onInput($event)">
<form (submit)="onSubmit($event)">

<!-- Two-way binding -->
<input [(ngModel)]="name">

<!-- Template reference -->
<input #nameInput>
<button (click)="logValue(nameInput.value)">Log</button>

Control Flow (Angular 17+)

<!-- @if -->
@if (isLoggedIn) {
  <app-dashboard />
} @else if (isLoading) {
  <app-spinner />
} @else {
  <app-login />
}

<!-- @for -->
@for (item of items; track item.id) {
  <div>{{ item.name }}</div>
} @empty {
  <p>No items found</p>
}

<!-- @switch -->
@switch (status) {
  @case ('active') {
    <span class="active">Active</span>
  }
  @case ('inactive') {
    <span class="inactive">Inactive</span>
  }
  @default {
    <span>Unknown</span>
  }
}

<!-- @defer (lazy loading) -->
@defer (on viewport) {
  <app-heavy-component />
} @placeholder {
  <p>Loading...</p>
}

Legacy Directives

<!-- *ngIf -->
<div *ngIf="isVisible">Visible</div>
<div *ngIf="user; else noUser">{{ user.name }}</div>
<ng-template #noUser><p>No user</p></ng-template>

<!-- *ngFor -->
<li *ngFor="let item of items; let i = index; trackBy: trackById">
  {{ i }}: {{ item.name }}
</li>

<!-- *ngSwitch -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'">Active</p>
  <p *ngSwitchCase="'inactive'">Inactive</p>
  <p *ngSwitchDefault>Unknown</p>
</div>

<!-- ngClass and ngStyle -->
<div [ngClass]="{ 'active': isActive, 'disabled': isDisabled }"></div>
<div [ngStyle]="{ 'color': textColor, 'font-size': fontSize + 'px' }"></div>

Services & Dependency Injection

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

// Injectable service
@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = '/api/users';
  
  getUsers() {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUser(id: number) {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: User) {
    return this.http.post<User>(this.apiUrl, user);
  }
}

// Using service in component
@Component({
  selector: 'app-users',
  standalone: true,
  template: `...`
})
export class UsersComponent {
  private userService = inject(UserService);
  users: User[] = [];
  
  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

Signals (Angular 16+)

import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  // Writable signal
  count = signal(0);
  
  // Computed signal
  doubleCount = computed(() => this.count() * 2);
  
  constructor() {
    // Effect runs when signals change
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }
  
  increment() {
    this.count.update(c => c + 1);
    // Or: this.count.set(this.count() + 1);
  }
}

Lifecycle Hooks

import { 
  Component, OnInit, OnDestroy, OnChanges, 
  AfterViewInit, SimpleChanges 
} from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  standalone: true,
  template: `<p>{{ message }}</p>`
})
export class LifecycleComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
  @Input() data: string = '';
  message: string = '';
  
  // Called when input properties change
  ngOnChanges(changes: SimpleChanges) {
    console.log('Changes:', changes);
  }
  
  // Called once after first ngOnChanges
  ngOnInit() {
    console.log('Component initialized');
  }
  
  // Called after view is initialized
  ngAfterViewInit() {
    console.log('View initialized');
  }
  
  // Called before component is destroyed
  ngOnDestroy() {
    console.log('Component destroyed');
    // Clean up subscriptions, etc.
  }
}

Reactive Forms

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="name" placeholder="Name">
      @if (form.get('name')?.invalid && form.get('name')?.touched) {
        <span class="error">Name is required</span>
      }
      
      <input formControlName="email" placeholder="Email">
      
      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>
  `
})
export class FormComponent {
  form = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(3)]),
    email: new FormControl('', [Validators.required, Validators.email])
  });
  
  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}

HTTP Client

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable, catchError, map } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  
  // GET request
  getData(): Observable<Data[]> {
    return this.http.get<Data[]>('/api/data');
  }
  
  // GET with params
  search(term: string): Observable<Data[]> {
    const params = new HttpParams().set('q', term);
    return this.http.get<Data[]>('/api/search', { params });
  }
  
  // POST request
  create(data: Data): Observable<Data> {
    return this.http.post<Data>('/api/data', data);
  }
  
  // With headers
  getWithAuth(): Observable<Data> {
    const headers = new HttpHeaders().set('Authorization', 'Bearer token');
    return this.http.get<Data>('/api/protected', { headers });
  }
  
  // With error handling
  getDataSafe(): Observable<Data[]> {
    return this.http.get<Data[]>('/api/data').pipe(
      map(response => response),
      catchError(error => {
        console.error('Error:', error);
        throw error;
      })
    );
  }
}

Routing

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'users/:id', component: UserDetailComponent },
  { 
    path: 'admin', 
    loadComponent: () => import('./admin/admin.component')
      .then(m => m.AdminComponent),
    canActivate: [AuthGuard]
  },
  { path: '**', component: NotFoundComponent }
];

// Using router in component
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';

@Component({
  selector: 'app-nav',
  standalone: true,
  imports: [RouterLink],
  template: `
    <a routerLink="/">Home</a>
    <a routerLink="/about">About</a>
    <a [routerLink]="['/users', userId]">User</a>
    <button (click)="navigate()">Go to About</button>
  `
})
export class NavComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);
  userId = 1;
  
  navigate() {
    this.router.navigate(['/about']);
  }
  
  ngOnInit() {
    // Get route params
    this.route.params.subscribe(params => {
      console.log(params['id']);
    });
    
    // Get query params
    this.route.queryParams.subscribe(params => {
      console.log(params['search']);
    });
  }
}

Pipes

// Built-in pipes
// In template:
{{ value | uppercase }}
{{ value | lowercase }}
{{ date | date:'short' }}
{{ date | date:'yyyy-MM-dd' }}
{{ price | currency:'USD' }}
{{ value | number:'1.2-2' }}
{{ object | json }}
{{ items | slice:0:5 }}
{{ observable$ | async }}

// Custom pipe
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50): string {
    if (value.length <= limit) return value;
    return value.substring(0, limit) + '...';
  }
}

// Usage: {{ longText | truncate:100 }}

Content Projection

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="header">
        <ng-content select="[card-header]"></ng-content>
      </div>
      <div class="body">
        <ng-content></ng-content>
      </div>
      <div class="footer">
        <ng-content select="[card-footer]"></ng-content>
      </div>
    </div>
  `
})
export class CardComponent {}

// Usage
// <app-card>
//   <h2 card-header>Title</h2>
//   <p>Main content goes here</p>
//   <button card-footer>Action</button>
// </app-card>

ViewChild & ViewChildren

import { Component, ViewChild, ViewChildren, ElementRef, QueryList, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-example',
  standalone: true,
  template: `
    <input #nameInput>
    <app-child #childComponent></app-child>
    <div #item *ngFor="let i of items">{{ i }}</div>
  `
})
export class ExampleComponent implements AfterViewInit {
  @ViewChild('nameInput') inputRef!: ElementRef<HTMLInputElement>;
  @ViewChild('childComponent') child!: ChildComponent;
  @ViewChildren('item') items!: QueryList<ElementRef>;
  
  ngAfterViewInit() {
    this.inputRef.nativeElement.focus();
    this.child.someMethod();
    this.items.forEach(item => console.log(item.nativeElement));
  }
}