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,*ngForwithtrackfor 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 withsetorupdate.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 withprovidedIn: '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
asyncpipe,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
@forloops. - Template-driven forms for complex PO workflows — validation logic scattered in HTML; use reactive forms.
- Importing entire RxJS — import operators from
rxjs/operatorsindividually for tree-shaking. - Ignoring lazy loading — initial bundle includes every feature; split routes with
loadChildrenorloadComponent. - 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 lazyfeatures/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;
trackitem 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
- TypeScript fundamentals explained — types, generics, and strict compiler options Angular assumes
- React fundamentals explained — the component-hook model Angular is often compared against
- Frontend state management explained — local, server, and global state patterns across frameworks
- Playwright E2E testing explained — browser automation for approval workflows and regressions