Guide

Angular fundamentals explained

A procurement team managing thousands of purchase orders cannot afford ad-hoc jQuery patches and inconsistent validation rules across dozens of screens. Angular is Google’s TypeScript-first, batteries-included framework for building large, maintainable web applications: components with declarative templates, a first-class dependency injection container, built-in routing and forms, and RxJS for async data streams. Modern Angular (v17+) defaults to standalone components (no NgModule boilerplate) and signals for fine-grained reactivity alongside zone.js change detection. Angular powers internal dashboards at Google, Microsoft, and many Fortune 500 procurement and healthcare portals where strict typing, testability, and long-term support matter more than the smallest possible bundle. This guide covers components and templates, DI and services, signals vs observables, reactive forms, routing and lazy loading, HttpClient patterns, a Harbor Procurement purchase-order portal worked example, a framework decision table, common pitfalls, and a production checklist alongside our TypeScript fundamentals guide, React fundamentals guide, and frontend state management guide.

What Angular is (and how it differs from React and Vue)

Angular is a platform, not just a view library. Where React focuses on UI and leaves routing, forms, and HTTP to ecosystem choices, Angular ships opinionated solutions for each layer — all integrated and versioned together.

  • TypeScript required — templates, DI tokens, and route configs are typed end-to-end; the CLI enforces strict settings in new projects.
  • Dependency injection — services are singletons (or scoped) injected via constructor or inject(); no prop-drilling for shared APIs.
  • Templates, not JSX — HTML extended with directives (*ngIf, *ngFor, [property], (event)) compiled for performance.
  • RxJS everywhere — HttpClient returns Observables; async pipes unsubscribe automatically in templates.
  • Signals (modern) — synchronous reactive primitives (signal, computed, effect) reduce reliance on zone.js patching and enable OnPush-by-default patterns.

Compared to Vue, Angular trades template simplicity for enterprise structure: more ceremony upfront, stronger conventions for teams of fifty engineers shipping the same app for a decade.

Components, templates, and data binding

A standalone component declares its own imports, selector, template, and styles:

@Component({
  selector: 'app-po-line',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <tr>
      <td>{{ line().sku }}</td>
      <td>
        <input [formControl]="qtyControl" type="number" />
      </td>
      <td>{{ lineTotal() | currency }}</td>
      <td>
        <button (click)="remove.emit(line().id)">Remove</button>
      </td>
    </tr>
  `
})
export class PoLineComponent {
  line = input.required<LineItem>();
  remove = output<string>();
  qtyControl = new FormControl(1, { nonNullable: true });

  lineTotal = computed(() =>
    this.line().unitPrice * (this.qtyControl.value ?? 0)
  );
}

Binding types

  • Interpolation{{ expression }} displays values in the template.
  • Property binding[disabled]="isSaving" sets DOM properties from component state.
  • Event binding(click)="submit()" wires DOM events to methods.
  • Two-way binding[(ngModel)] for simple forms; prefer reactive forms for validation-heavy UIs.
  • Structural directives@if, @for (new control flow) or legacy *ngIf, *ngFor with track for list identity.

Use ChangeDetectionStrategy.OnPush on presentational components so Angular checks them only when inputs, signals, or events change — critical for tables with hundreds of rows.

Dependency injection and services

Services encapsulate business logic, HTTP calls, and cross-cutting concerns. Register them with providedIn: 'root' for app-wide singletons, or providers: [PoService] on a route for scoped instances (e.g. one service per lazy-loaded feature module).

@Injectable({ providedIn: 'root' })
export class PoService {
  private http = inject(HttpClient);
  private base = '/api/purchase-orders';

  list(status: PoStatus): Observable<PurchaseOrder[]> {
    return this.http.get<PurchaseOrder[]>(this.base, {
      params: { status }
    });
  }

  submit(id: string): Observable<PurchaseOrder> {
    return this.http.post<PurchaseOrder>(
      `${this.base}/${id}/submit`, {}
    );
  }
}

Inject services in components via private po = inject(PoService) (functional style) or constructor injection. Interceptors attach auth headers and handle 401 redirects globally; guards (canActivate, canMatch) protect routes before components load.

Signals, RxJS, and async data

Angular’s modern stack combines signals for synchronous UI state with RxJS Observables for HTTP and WebSocket streams:

  • signal(initial) — writable reactive value; update with set or update.
  • computed(() => ...) — derived read-only signal; recalculates when dependencies change.
  • toSignal(observable$) — bridges an Observable into a signal for template use.
  • toObservable(signal) — exposes a signal as an Observable for RxJS operators.

In templates, call signals as functions: {{ count() }}. The async pipe still works for Observables and auto-unsubscribes. Avoid nested subscriptions in components — use switchMap, combineLatest, or signals instead of manual .subscribe() in ngOnInit without teardown.

Zone.js patches async browser APIs to trigger change detection after Promises and timers. New projects can enable zoneless experimental mode where signals and explicit markForCheck drive updates — smaller runtime, clearer mental model, but requires discipline.

Reactive forms, routing, and HttpClient

Reactive forms

Build forms with FormGroup, FormControl, and validators in TypeScript — not in the template. Cross-field rules (end date after start date) live in custom validator functions. valueChanges streams debounce SKU lookups; statusChanges drives submit button disabled state.

Angular Router

Routes map URLs to components with optional lazy loading: loadComponent: () => import('./po-editor.component') splits chunks per feature. Nested routes use router-outlet in parent layouts; route data and resolve functions preload PO details before the editor renders. Use routerLink and routerLinkActive for navigation; prefer withComponentInputBinding to pass route params as component inputs.

HttpClient

Typed GET/POST/PATCH with interceptors for JWT refresh, retry on 503, and error toast mapping. For list pagination, return { items, total } and bind page signals to query params so URLs are shareable.

Angular CLI and project layout

ng new harbor-procurement --standalone --style=scss --ssr=false scaffolds a Vite/esbuild-powered workspace (Angular 17+). Key commands:

  • ng generate component po-list — creates standalone component files.
  • ng generate service po — service with providedIn: 'root'.
  • ng build --configuration production — AOT compilation, tree-shaking, hashed assets.
  • ng test — Karma or Vitest (experimental) unit tests with TestBed.

Typical structure: src/app/core/ (singleton services, interceptors), features/po/ (lazy routes), shared/ (dumb components, pipes, directives). Environment files swap API base URLs per deploy target.

Worked example: Harbor Procurement purchase-order portal

Harbor Procurement supplies parts to regional manufacturers. Buyers create purchase orders (POs), route them through approval chains, and track receipt against invoices — a workflow-heavy SPA where validation, audit trails, and role-based access dominate.

Architecture

Angular 19 standalone app with PoService, AuthService, and an ApprovalGuard that checks JWT claims before loading /po/:id/approve. State splits three ways: URL query params for list filters (?status=draft&page=2), signals for ephemeral UI (modal open, selected rows), and server truth via HttpClient Observables converted with toSignal for the PO detail header.

PO editor feature

PoEditorComponent loads a FormArray of line items. Adding a line pushes a new FormGroup; removing splices the array and recalculates computed(() => totalAmount()) from control values. SKU autocomplete debounces valueChanges with switchMap to /api/catalog/search?q=. Submit calls poService.submit(id); on success a MatSnackBar (or custom toast) confirms and the router navigates to the pending-approval queue.

Approval workflow

Managers see a read-only summary with signal-driven @if (po().amount > 50000) requiring CFO secondary approval. Comments append via PATCH; optimistic UI rolls back on 409 conflict when another approver acted first.

Testing and deploy

TestBed tests mock PoService with provideHttpClientTesting. Playwright E2E covers draft → submit → approve on staging. Production build outputs to dist/; nginx serves static files with try_files $uri /index.html for deep links. API lives on a separate subdomain with CORS restricted to the portal origin.

Framework decision table

Need Prefer Why
Large enterprise team, long maintenance horizon Angular Opinionated structure, DI, typed templates, LTS releases
Maximum hiring pool and npm package breadth React + Next.js Largest ecosystem; see our Next.js guide
Gradual adoption on legacy server-rendered site Vue 3 Drop-in components; gentler learning curve
Smallest client bundle, content-heavy marketing site Svelte / SvelteKit Compile-time reactivity; see SvelteKit guide
Complex forms with cross-field validation and audit Angular reactive forms Form model in TypeScript; validators composable and testable
Full-stack Vue with file routing Nuxt Server routes and SSR; see Nuxt guide

Common pitfalls

  • Subscribing without unsubscribing — memory leaks in long-lived components; use async pipe, takeUntilDestroyed, or signals.
  • Mutating @Input() objects — breaks OnPush and parent state; emit events or use immutable updates.
  • Default change detection on huge tables — switch to OnPush and trackBy on @for loops.
  • Template-driven forms for complex PO workflows — validation logic scattered in HTML; use reactive forms.
  • Importing entire RxJS — import operators from rxjs/operators individually for tree-shaking.
  • Ignoring lazy loading — initial bundle includes every feature; split routes with loadChildren or loadComponent.
  • Fighting signals and zone.js — pick a pattern per feature; mixing manual ChangeDetectorRef.detectChanges() calls signals confusion.

Production checklist

  • Scaffold with ng new; enable strict TypeScript, ESLint, and standalone defaults.
  • Organize core/, shared/, and lazy features/ folders; one service per domain aggregate.
  • Use reactive forms for any screen with more than three validated fields.
  • HttpClient interceptors handle auth, errors, and loading indicators globally.
  • Route guards enforce roles; resolvers preload data before paint on detail routes.
  • OnPush + signals on list and table components; track item IDs in @for.
  • Unit test services and validators; Playwright smoke on create-submit-approve path.
  • ng build --configuration production; verify budget warnings in angular.json.
  • Configure CSP and trusted types if hosting user-generated HTML in previews.
  • Monitor API latency and error rates; map 422 validation bodies to form field errors.

Key takeaways

  • Angular is a full TypeScript platform with DI, routing, forms, and HTTP built in.
  • Standalone components and signals are the modern default; learn them before NgModule patterns.
  • Reactive forms and RxJS suit workflow-heavy enterprise UIs like procurement and admin portals.
  • OnPush change detection and lazy routes keep large apps performant.
  • Choose Angular when team scale, typing rigor, and integrated tooling outweigh bundle-size minimalism.

Related reading