Angular
A Complete 14-Chapter Course
Table of Contents
- What Angular Is, TypeScript Primer, CLI Setup
- Components and Templates
- Data Binding
- Built-in Directives
- Component Communication
- Services and Dependency Injection
- Pipes
- Template-Driven Forms
- Reactive Forms
- Routing
- HTTP Client and RxJS Basics
- Lifecycle Hooks
- Standalone Components and Signals
- Testing โ Jasmine, Karma, and TestBed
What Angular Is, TypeScript Primer, CLI Setup
React is deliberately a library: it renders UI, and everything else โ routing, forms, HTTP, state management โ is a separate choice (React Router, Zustand, fetch/React Query, all covered as add-ons across the React course). Angular is a full framework: routing, forms, dependency injection, and an HTTP client are all built in from day one, with one official, opinionated way to do each. It's also written in and built around TypeScript โ JavaScript with optional type annotations โ rather than plain JavaScript.
Just Enough TypeScript to Get Started
: type annotations are the core addition โ declaring what a variable, parameter, or return value is allowed to be, checked while writing code rather than only discovered when it crashes at runtime. An interface describes the shape an object must have; greet's parameter being typed as Person means passing an object missing name or age is flagged immediately by the editor, before the code ever runs. Classes already exist in plain JavaScript (JS Intermediate Chapter 3) โ TypeScript just lets a class's properties and methods carry the same type annotations.
Installing the Angular CLI and Creating a Project
ng new my-app asks a few setup questions (routing, stylesheet format) and scaffolds a complete starter project โ Angular's CLI does considerably more for you upfront than Vite's React template did. ng serve starts a local dev server (usually http://localhost:4200) with live reload, the same role npm run dev played throughout the React course.
A Generated Project's Anatomy
src/main.tsโ the entry point; bootstraps the root component into the page.src/app/app.component.tsโ the root component's logic (a TypeScript class).src/app/app.component.htmlโ that same component's template (its markup) โ Angular keeps logic and markup in separate files by default, unlike JSX combining both.src/app/app.component.cssโ styles scoped to just this component.src/app/app.component.spec.tsโ a test file, generated automatically alongside every component (covered properly in Chapter 14).
A First Component
@Component({...}) is a decorator โ metadata attached to the class right above it, telling Angular how to treat GreetingComponent: selector is the HTML tag name (<app-greeting>) used to place it elsewhere, and templateUrl points to its separate HTML file. {{ name }} in the template is interpolation โ Angular's equivalent of JSX's {name}, inserting the class property's current value directly into the rendered output.
NgModule โ a separate declaration file grouping components together. Recent Angular versions (and a fresh ng new project today) default to standalone components instead, each one self-contained and importable directly without belonging to a module โ the approach used throughout this entire course. If older documentation or Stack Overflow answers reference NgModule, that's the previous convention, not a mistake in what's taught here.
| React | Angular | |
|---|---|---|
| Category | Library (you choose the rest) | Full framework (routing/forms/HTTP built in) |
| Language | JavaScript (+ optional TS) | TypeScript by default |
| Markup | JSX โ embedded in the same file as logic | Separate template file (or inline) per component |
| Embedding a value | {value} | {{ value }} |
Coding Challenges
Install the Angular CLI, create a new project, and modify the default AppComponent so its template shows "Welcome, [your name]!" using interpolation of a class property.
๐ View solutionWrite a Person interface (name: string, age: number, email optional) and a function describePerson(person: Person): string returning a sentence describing them, called with at least one object missing the optional property.
๐ View solutionUse the CLI (ng generate component) to create a new standalone component with its own template showing some static content, then use its selector tag inside AppComponent's template to display it.
๐ View solutionChapter 1 Quick Reference
- Angular โ a full framework (routing/forms/HTTP built in), TypeScript-first
- : type annotations, interface โ TypeScript's core additions over plain JS
- ng new / ng serve โ CLI project creation and dev server, Angular's equivalent of Vite's commands
- @Component({...}) โ a decorator configuring a class as an Angular component
- {{ value }} โ interpolation, Angular's equivalent of JSX's
{value} - standalone: true โ the modern default, used throughout this course instead of older NgModule-based setup
- Next chapter: components and templates in depth
Components and Templates
Chapter 1's GreetingComponent used a separate template file. This chapter covers the rest of a component's anatomy โ inline templates and styles as an alternative, how Angular keeps one component's CSS from leaking into another's, and nesting components together to build an actual page, the direct equivalent of composing JSX components in React (Fundamentals Chapter 9).
Inline Template and Styles
template (a backtick string, allowing multiple lines) and styles (an array of strings) are direct alternatives to templateUrl/styleUrl โ fine for a small enough component that separate files would be overkill. Larger components generally still use separate .html/.css files, which most editors syntax-highlight more usefully than a string embedded in TypeScript.
Style Encapsulation โ Styles Don't Leak Between Components
Both components use a class called .box, with completely different styling โ and both render correctly, with no collision. By default, Angular scopes each component's styles so they only ever apply to that component's own template, achieved by attaching unique generated attributes behind the scenes. This is functionally similar to what CSS Modules or styled-components give a React project deliberately โ Angular does it automatically, with no extra setup, for every component.
Composing Components Together
Every standalone component used inside another's template must be listed in that component's own imports array โ the explicit step Chapter 1's third challenge already required. <app-header></app-header> and <app-footer></app-footer> in the template then place those components exactly where written, the same nesting idea as composing <Header /> and <Footer /> in a React JSX tree.
imports array doesn't throw a JavaScript import error โ Angular's compiler instead reports that it doesn't recognize the unknown HTML tag (app-header isn't a known element), since as far as the template compiler is concerned, an unimported selector is indistinguishable from a typo or a real, unrecognized HTML element.
Coding Challenges
Create a component using inline template and styles (no separate .html/.css files) rendering a styled "Beta" tag, and use it inside AppComponent.
๐ View solutionBuild three components (Header, Sidebar, Footer), each with simple static content, and compose all three inside AppComponent's template to form a basic page layout.
๐ View solutionBuild two sibling components, each using the same CSS class name internally but styled completely differently, and use both inside AppComponent โ confirming both render with their own correct styling and neither affects the other.
๐ View solutionChapter 2 Quick Reference
- template / styles โ inline alternatives to
templateUrl/styleUrl, fine for small components - Angular scopes each component's styles automatically โ identical class names in different components never collide
- A component used in another's template must be listed in that component's imports array
- A missing import shows as an "unrecognized element" error, not a JS import error
- Composing components by nesting their selector tags is the same idea as nesting JSX components in React
- Next chapter: data binding โ property binding, event binding, and two-way binding with
ngModel
Data Binding
Interpolation ({{ value }}, Chapter 2) only inserts text content. Angular has three more binding forms covering everything else โ setting a DOM property directly, responding to events, and the special case of keeping a form field and a class property in sync automatically.
Property Binding โ [property]="expression"
Square brackets around an attribute name โ [disabled], [src] โ bind that DOM property directly to a class property or expression, evaluated as real TypeScript/JavaScript rather than treated as a plain string. [disabled]="isSaving" sets the button's actual disabled property to whatever isSaving currently is (a real boolean) โ the same underlying need as React's curly-brace rule for non-string prop values (Fundamentals Chapter 2), just expressed with brackets instead.
Event Binding โ (event)="handler()"
Parentheses around an event name โ (click) โ call the given expression whenever that event fires, the direct equivalent of React's onClick={handler}. increment() is a regular method on the class; this.count++ updates the property directly โ there's no separate setter function the way useState requires, since Angular's change detection automatically re-renders the template after any event handler runs.
Accessing the Event Object โ $event
$event is a special template variable holding the actual DOM event object โ passing it explicitly to the handler is the same idea as React's automatic event-object argument (Fundamentals Chapter 4), just opted into rather than implicit. The as HTMLInputElement cast is TypeScript-specific: event.target is typed generically as EventTarget, so accessing its .value property requires telling the compiler exactly what kind of element it actually is.
Two-Way Binding โ [(ngModel)]
[(ngModel)]="name" โ sometimes called "banana in a box," combining property binding's square brackets with event binding's parentheses โ keeps the input's value and the name property synchronized automatically in both directions: typing updates name, and changing name elsewhere in code updates the input. This is the same end result as React's controlled input pattern (value + onChange, Fundamentals Chapter 7), collapsed into one directive instead of two explicit bindings.
[(ngModel)] only works once FormsModule is added to the component's own imports array โ forgetting it produces a template compile error about ngModel not being a known property, the same category of "unrecognized" error from Chapter 2's missing-component-import warning, just for a built-in directive instead of a custom component this time.
Class and Style Bindings
[class.active] toggles a single CSS class on or off based on a boolean expression, and [style.color] binds one specific inline style property directly โ the same conditional-styling need React handles with a template literal or object passed to className/style (Fundamentals Chapter 5).
| Angular | React equivalent |
|---|---|
| {{ value }} | {value} |
| [property]="expr" | property={expr} |
| (event)="handler()" | onEvent={handler} |
| [(ngModel)]="prop" | value={prop} onChange={...} |
Coding Challenges
Build a component with a boolean isLocked property and a button whose disabled state is property-bound to it, plus a second button (event-bound) that toggles isLocked.
๐ View solutionBuild a click counter component using event binding, with separate +1, -1, and Reset buttons, matching the equivalent React counter from Fundamentals Chapter 3.
๐ View solutionBuild a component with a text input two-way bound via ngModel to a name property, displaying "Hello, [name]!" live below it as the user types. Remember FormsModule.
๐ View solutionChapter 3 Quick Reference
- [property]="expr" โ binds a real DOM property directly, not a plain string
- (event)="handler()" โ calls an expression when that event fires
- $event โ the actual event object, passed explicitly to a handler when needed
- [(ngModel)]="prop" โ two-way binding; requires
FormsModulein the component'simports - [class.x] / [style.y] โ bind a single class or style property conditionally
- No setter function needed โ Angular's change detection re-renders after any event handler runs automatically
- Next chapter: built-in directives โ *ngIf, *ngFor, ngClass, ngStyle
Built-in Directives
React handles conditionals and lists with plain JavaScript embedded in JSX โ ternaries, &&, and .map() (Fundamentals Chapters 5 and 6). Angular instead provides directives: special attributes recognized by the template compiler that add, remove, or repeat elements. This chapter covers the modern built-in control-flow syntax (@if, @for) plus the attribute directives for classes and styles.
@if โ Conditional Rendering
The @if block (introduced in Angular 17 as the modern built-in control flow) renders its contents only when the condition is true, with an optional @else block โ the direct equivalent of React's ternary or &&, but reading more like the if/else statement it actually is. Unlike React's conditional rendering, which keeps the element in the JSX expression, @if genuinely adds or removes the element from the DOM entirely based on the condition.
*ngIf="condition" and *ngFor="let x of items" as attributes on an element. They still work, but the newer @if/@for block syntax is now the recommended default โ cleaner to read, and built into the template compiler rather than requiring CommonModule to be imported. This course uses the modern block syntax throughout; recognize the older *ng forms when you see them in existing code.
@for โ Rendering a List
@for repeats its block once per array item โ the equivalent of React's .map(). The track item.id clause is required, and serves exactly the same purpose as React's key prop (Fundamentals Chapter 6): it tells Angular how to identify each item across re-renders so it can update the DOM efficiently when the list changes, rather than rebuilding everything. Where React's key is optional-but-warned, Angular's track is mandatory.
@empty and the Loop Variables
An optional @empty block renders when the array has no items โ neatly handling the "empty list" case that React's Todo project (Project 1) had to write as a separate conditional. Inside a @for, contextual variables like $index, $first, $last, and $even are also available for free โ {{ $index }} gives the current item's position, for instance.
@switch โ Multiple Cases
@switch handles three-or-more named cases cleanly โ the same job React's lookup-object pattern did (Fundamentals Chapter 5), expressed as template-level branching. This is a natural fit for the loading/error/success status pattern that ran throughout the React projects.
ngClass and ngStyle โ Multiple Classes or Styles at Once
Chapter 3's [class.active] toggles one class; [ngClass] takes an object toggling several at once, each key a class name and each value a boolean deciding whether it applies. [ngStyle] does the same for multiple inline styles. Both require NgClass/NgStyle (or CommonModule) in the component's imports โ unlike the new @ control-flow blocks, these attribute directives still need importing.
@if that's false removes its element from the DOM completely, which also destroys any component inside it (and its state). If an element only needs to be visually hidden while keeping its state alive, a plain [style.display] or [hidden] binding is the right tool instead โ @if is for genuinely adding/removing, not merely showing/hiding.
| Angular | React equivalent |
|---|---|
| @if (x) { } @else { } | Ternary / && |
| @for (x of items; track x.id) { } | items.map(...) + key |
| @empty { } | A separate "empty list" conditional |
| @switch / @case | Lookup object for many states |
| [ngClass]="{ ... }" | Conditional className |
Coding Challenges
Build a component with a boolean isLoggedIn property toggled by a button, using @if/@else to show either "Welcome back!" or "Please log in." accordingly.
๐ View solutionGiven an array of objects ({ id, name }), render them as a list with @for (using track on the id), including an @empty block showing "No items" when the array is cleared.
๐ View solutionBuild a component with a status property ("loading"/"error"/"success") cycled by a button, using @switch to render different content per status, and apply [ngClass] to color the message based on the same status.
๐ View solutionChapter 4 Quick Reference
- @if / @else โ conditional rendering; genuinely adds/removes the element from the DOM
- @for (x of items; track x.id) โ list rendering;
trackis mandatory (React'skeyequivalent) - @empty โ renders when the array is empty; $index/$first/$last/$even available inside
@for - @switch / @case / @default โ clean branching for three-or-more named cases
- [ngClass] / [ngStyle] โ toggle several classes/styles at once via an object (needs importing)
- The modern
@blocks replaced the older*ngIf/*ngForstructural directives - Next chapter: component communication โ @Input, @Output, and EventEmitter
Component Communication
React passes data down through props and back up by passing functions down as props (Fundamentals Chapters 2 and 4). Angular splits these into two explicit, separately-named mechanisms: @Input for data flowing into a child, and @Output (with an EventEmitter) for events flowing out of a child back to its parent.
@Input โ Passing Data Into a Child
The @Input() decorator marks a property as one the parent is allowed to set โ the equivalent of declaring a prop in React. The parent passes it using property binding from Chapter 3: [name]="'Philip'". Note the binding evaluates as an expression, so a literal string needs inner quotes ("'Philip'"), whereas [age]="35" passes a real number โ the same string-vs-expression distinction as React's quotes-vs-curly-braces rule for props.
@Output and EventEmitter โ Emitting an Event Up
An @Output() property is an EventEmitter โ the child calls .emit(value) on it to send data upward, and the parent listens with event binding (parentheses) exactly like a DOM event: (deleted)="onDelete($event)", where $event holds whatever value was emitted. This is the structural equivalent of React's "pass a function down, child calls it" pattern (Project 1's todo delete), but Angular models it as a custom event the child raises rather than a callback function it receives. The <number> on EventEmitter<number> is a TypeScript generic, declaring the type of value this event carries.
@Output with the same (eventName) syntax used for native events like (click), custom outputs are conventionally named as plain past-tense or noun events โ deleted, saved, valueChanged โ not onDelete. From the parent's side, (deleted)="..." then reads naturally alongside (click)="...", as if the child were a built-in element raising its own events.
A Two-Way Binding Output Convention
If a component has an @Input() called value and an @Output() called valueChange (the input name plus Change), Angular lets a parent use the same [(banana-in-a-box)] two-way syntax from Chapter 3 on it โ [(value)]="something". This is exactly how the built-in [(ngModel)] works under the hood, and it's the standard way to build a custom component that supports two-way binding, rather than anything special baked into the framework.
@Input() property's value locally, unlike React's strictly read-only props โ but doing so is strongly discouraged, since the parent has no idea the value changed and the two will silently disagree. Treat inputs as read-from-parent only: to communicate a change back, emit an @Output event and let the parent update the source of truth, keeping the same one-way-data-flow discipline React enforces structurally.
| Angular | React equivalent |
|---|---|
| @Input() name | A prop |
| [name]="value" | Passing that prop |
| @Output() saved = new EventEmitter() | A callback prop (e.g. onSave) |
| this.saved.emit(data) | Calling that callback: onSave(data) |
| (saved)="handle($event)" | Passing the callback: onSave={handle} |
Coding Challenges
Build a MovieCard component with @Input properties for title and year, rendering them as "Title (Year)". Use it three times in a parent with three different movies.
๐ View solutionBuild a TodoItem component with @Input id/text and an @Output deleted EventEmitter. The parent holds an array of todos, renders one TodoItem each, and removes the matching todo when a delete event is emitted.
๐ View solutionBuild a Counter child component with an @Input count and an @Output countChange EventEmitter, then have the parent use two-way binding [(count)] on it so changing the count inside the child updates the parent's value.
๐ View solutionChapter 5 Quick Reference
- @Input() prop โ declares a property a parent can set (Angular's "prop")
- Parent passes it with property binding:
[prop]="value" - @Output() ev = new EventEmitter<T>() โ declares a custom event the child can emit
- Child raises it with
this.ev.emit(data); parent listens with(ev)="handler($event)" - Name outputs as events (
deleted,saved), notonXโ they read like native DOM events - An
@Input value+@Output valueChangepair enables[(value)]two-way binding - Next chapter: services and dependency injection โ sharing logic and state beyond parent/child
Services and Dependency Injection
The React course needed Context (Intermediate Chapter 2) and eventually Zustand/Redux (Advanced Chapter 1) to share state across distant components without prop drilling. Angular has a single, built-in answer baked into the framework from the start: a service โ an ordinary class holding shared logic or state โ combined with dependency injection (DI), which hands that service to any component that asks for it. This is arguably Angular's defining feature.
A Service Is Just a Class
The @Injectable({ providedIn: 'root' }) decorator marks this class as something Angular's DI system can provide, and 'root' means a single shared instance exists for the entire app โ every component that asks for CounterService gets that same one instance, making it a natural place to hold shared state. (A service can also be provided at a narrower scope, but app-wide 'root' is the common default.)
Injecting a Service Into a Component
inject(CounterService) asks Angular's DI system for the service โ the component never creates it with new CounterService() itself. That distinction is the whole point of dependency injection: the component declares what it needs, and the framework supplies it, automatically handing over the same shared 'root' instance. Any other component injecting CounterService shares the exact same count, with no props, no Context provider, and nothing passed between them.
constructor(private counter: CounterService) {}. Both achieve identical results โ the newer inject() function (used throughout this course) reads more cleanly and works in more places, but recognize the constructor form when you encounter it in existing codebases; it remains fully supported.
Why This Replaces Prop Drilling and Context
In React, sharing one piece of state between a header badge and a distant cart page required lifting state up and then either prop drilling or wrapping the tree in a Context provider (the exact journey the Shopping Cart project took). In Angular, both components simply inject() the same service โ there's no provider to wrap anything in, no value object to memoize (Intermediate Chapter 7's concern), and no tree structure that the shared state has to flow through. The service exists independently of the component tree entirely.
Services for Logic, Not Just State
A service doesn't have to hold state โ it's equally the home for shared behavior: logging, formatting, calculations, and (most importantly) talking to a backend API, which the HTTP client chapter builds on directly. Keeping that logic in an injectable service rather than inside components is a core Angular convention: components handle the view, services handle everything else.
counter.getCount() directly in a template works for state changed synchronously by user events (Angular re-checks the template after each event). But for state that changes asynchronously โ a timer, an API response โ exposing the value as an Observable and using the async pipe (Chapters 7 and 11), or Angular's newer signals (Chapter 13), is the more robust pattern. The plain-method approach here is the simplest starting point, deliberately, before those tools are introduced.
| Sharing need | React approach | Angular approach |
|---|---|---|
| Nearby components | Lift state up + props | A shared service (or @Input/@Output) |
| Distant components | Context, or Zustand/Redux | A service injected into both |
| Shared behavior/API calls | A custom hook / util module | An injectable service |
Coding Challenges
Build a CounterService (providedIn: 'root') holding a count with increment/decrement/reset methods, and inject it into a single component that displays and changes the count via buttons.
๐ View solutionInject the same CounterService into two completely separate, unrelated components โ one that increments the count and one that only displays it โ confirming both reflect the same shared value with nothing passed between them.
๐ View solutionBuild a stateless LoggerService with a log(message) method that prefixes a timestamp, and inject it into a component that calls it from a button click.
๐ View solutionChapter 6 Quick Reference
- A service โ an ordinary class for shared state or behavior, marked
@Injectable({ providedIn: 'root' }) - providedIn: 'root' โ one shared instance for the whole app
- inject(ServiceClass) โ asks Angular's DI to supply the service; never
newit yourself - Two distant components injecting the same service share its state โ no provider, no prop drilling, no Context
- Services are also the home for shared behavior: logging, formatting, and (next) API calls
- Older code injects via the constructor (
constructor(private x: X)) โ equivalent toinject() - Next chapter: pipes โ transforming displayed values in the template
Pipes
In React, formatting a value for display means calling a function in the JSX โ {formatPrice(amount)}, {date.toLocaleDateString()}. Angular has a dedicated template feature for this: a pipe, applied with the | symbol, transforming a value purely for display while leaving the original data untouched. Several common transformations ship built in, and writing custom ones is straightforward.
Built-in Pipes
A pipe takes the value on its left and transforms it for display. Arguments come after a colon โ currency:'EUR' sets the currency code, date:'fullDate' picks a format. The original name, price, and today properties are never modified; only what appears on screen is transformed. The json pipe is especially handy while learning โ it's the quickest way to dump an object's contents into the template to see its shape.
Chaining Pipes
Multiple pipes can be chained with successive | symbols, each receiving the output of the one before it โ applied left to right, the same direction as reading. Here uppercase runs first, then slice takes the first three characters of the result.
CurrencyPipe, DatePipe, UpperCasePipe, and JsonPipe live in @angular/common and must be added to the component's imports array (individually, or via CommonModule) before use โ the same "import what the template uses" rule as ngClass from Chapter 4. Forgetting produces the familiar "unknown pipe" template error.
Writing a Custom Pipe
A custom pipe is a class implementing PipeTransform โ a single transform(value, ...args) method returning the transformed result. The @Pipe decorator's name is what's used in the template (| truncate), and any extra parameters after the value become the pipe's arguments (truncate:50 passes 50 as limit). It's a standalone pipe, imported into a component's imports array exactly like a standalone component.
| truncate:50 reads cleanly inline), and being a class registered with the framework, the same pipe is trivially reusable across every component that imports it, without re-importing a utility function in each file.
The async Pipe โ A Preview
One built-in pipe deserves a special mention now: async. It subscribes to an Observable (or Promise) and renders its latest emitted value, automatically โ directly addressing the async-state limitation flagged in Chapter 6's warning. It comes into its own with the HTTP client and RxJS in Chapter 11, but it's worth recognizing here as a pipe, since that's exactly what it is.
| Angular pipe | React equivalent |
|---|---|
| {{ x | uppercase }} | {x.toUpperCase()} |
| {{ x | currency:'EUR' }} | {formatCurrency(x)} |
| {{ x | date:'fullDate' }} | {x.toLocaleDateString(...)} |
| {{ x | myCustomPipe }} | {myFormatFunction(x)} |
Coding Challenges
Build a component displaying a name (uppercase pipe), a price (currency pipe), and today's date (date pipe with a readable format), remembering to import the needed pipes.
๐ View solutionWrite a custom TruncatePipe (with a configurable limit argument defaulting to 20, appending an ellipsis when it truncates) and use it on a long string with an explicit limit.
๐ View solutionWrite a custom FileSizePipe converting a number of bytes into a human-readable string (e.g. 1536 -> "1.5 KB", 2097152 -> "2 MB"), and use it on a few different byte values.
๐ View solutionChapter 7 Quick Reference
- A pipe โ
{{ value | pipeName }}โ transforms a value for display, leaving the data unchanged - Arguments follow a colon:
currency:'EUR',date:'fullDate',slice:0:3 - Chain pipes with successive
|symbols, applied left to right - Built-in pipes (currency/date/uppercase/jsonโฆ) live in
@angular/commonand must be imported - Custom pipe โ a class with
@Pipe({ name })implementingtransform(value, ...args) - async pipe โ subscribes to an Observable/Promise and renders its latest value (Chapter 11)
- Next chapter: template-driven forms
Template-Driven Forms
Angular has two distinct, official approaches to forms โ template-driven (this chapter) and reactive (next chapter). Template-driven forms keep most of the form's setup in the HTML template itself, built on the ngModel two-way binding from Chapter 3. They're the quicker option for simpler forms, and the closer fit to how React's controlled inputs felt.
The Setup โ ngForm and ngModel Together
Three pieces work together here. #signupForm="ngForm" is a template reference variable โ it captures the form's auto-created NgForm object, giving access to its overall validity (signupForm.invalid). (ngSubmit) fires the handler on submit, already preventing the native page reload that React needed preventDefault() for (Fundamentals Chapter 4). Each [(ngModel)] input needs a name attribute so the form can track it as a named control.
Validation Through Directives
Validation is declared as plain attributes on the inputs โ required, email, minlength, maxlength, pattern. Angular wires these standard-looking HTML attributes into its own validation system, tracking each control's validity and the overall form's. [disabled]="signupForm.invalid" then disables the submit button until every validator passes โ declarative validation with no JavaScript validation code written at all.
Showing Validation Messages
A per-input reference variable โ #email="ngModel" โ captures that single control's state, exposing flags like invalid, valid, touched (the user has focused then left it), and dirty (the value has changed). Combining invalid && touched with the @if from Chapter 4 shows an error message only after the user has actually interacted with the field โ not immediately on an untouched, empty form.
touched waits until they've left a field; dirty waits until they've changed it. React projects had to track this kind of "has the user interacted yet" state manually โ Angular's forms provide it built in, on every control.
[(ngModel)] control without a name attribute (or with a duplicate one) throws a runtime error โ the form uses name as the key to register and track each control. This is easy to forget when copying the simpler standalone ngModel usage from Chapter 3, which worked without a name because it wasn't inside an ngForm.
| Angular template-driven | React controlled-form equivalent |
|---|---|
| [(ngModel)]="model.x" | value + onChange |
| (ngSubmit)="onSubmit()" | onSubmit + preventDefault() |
| required / email / minlength | Hand-written validation logic |
| control.touched / .dirty | Manually tracked "interacted" state |
Coding Challenges
Build a template-driven form with name and email fields (both required, email also using the email validator), a model object, and a submit handler that logs the model โ with the submit button disabled while the form is invalid.
๐ View solutionAdd per-field validation messages to the form, each shown only when that field is both invalid and touched, using a per-input #ref="ngModel" reference variable and @if.
๐ View solutionBuild a template-driven "create account" form with a username (required, minlength 3) and a password (required, minlength 8), showing the specific reason a field is invalid (e.g. "too short" vs "required") by checking the control's errors.
๐ View solutionChapter 8 Quick Reference
- Template-driven forms build on
[(ngModel)]and require FormsModule - #form="ngForm" โ a template ref capturing the whole form's state (e.g.
form.invalid) - (ngSubmit) โ submit handler; already prevents the native page reload
- Validation is declared as attributes:
required,email,minlength,pattern - #field="ngModel" โ a per-control ref exposing
invalid/touched/dirty/errors - Every
ngModelcontrol inside a form needs a uniquenameattribute - Next chapter: reactive forms โ the same goals, but defined in the component class instead
Reactive Forms
Template-driven forms (Chapter 8) put the form's structure in the HTML, with Angular inferring the model behind the scenes. Reactive forms invert that: the form is defined explicitly in the component class as an object you build and control directly, with the template just connecting to it. More setup, but the form becomes a real, inspectable, programmatically-controllable object โ the better fit for complex forms, dynamic fields, and custom validation.
Building a FormGroup
A FormGroup is the whole form; each field is a FormControl with its initial value and an array of validators. Crucially, this entire structure is a plain object in the class โ readable and writable from code. The validators come from Validators (required, email, minLength(8), pattern(...)) rather than being template attributes, and uses ReactiveFormsModule instead of FormsModule.
Connecting the Template
[formGroup]="loginForm" binds the template's form to the object built in the class, and each input declares which control it represents with formControlName="email". Notice there's no [(ngModel)] anywhere โ the form object is the single source of truth, and the inputs simply attach to its existing controls by name. loginForm.invalid works exactly as before for the submit button.
FormBuilder โ Less Boilerplate
FormBuilder โ injected as a service (Chapter 6) โ is a shorthand for building the same structure with less new FormGroup/new FormControl repetition: each field becomes a terse [initialValue, validators] array. It produces an identical FormGroup; it's purely the more concise and conventional way to write one, and what most real reactive-forms code uses.
Reacting to Value Changes
Every control (and the form itself) exposes a valueChanges Observable that emits each time its value changes โ the core reason these are called reactive forms. This is what makes things like live-filtering, dependent fields, or debounced validation natural, in a way template-driven forms can't match cleanly. Observables are covered properly in Chapter 11; this is a first glimpse of why reactive forms unlock so much.
Programmatic Control
Because the form is a real object, it can be manipulated from code โ setValue/patchValue to fill it (e.g. loading an existing record into an edit form), reset() to clear it. This programmatic control is the practical payoff of reactive forms, and the main reason they're preferred for anything beyond a simple contact form.
[(ngModel)] + FormsModule) or entirely reactive (formControlName + ReactiveFormsModule) โ mixing [(ngModel)] and formControlName on controls within the same form causes confusing conflicts. Pick one approach per form: template-driven for simple cases, reactive when the extra control is genuinely needed.
| Template-driven (Ch 8) | Reactive (Ch 9) | |
|---|---|---|
| Form defined in | The template (HTML) | The component class |
| Module | FormsModule | ReactiveFormsModule |
| Binding | [(ngModel)] | formControlName |
| Validators | Template attributes | Validators in the class |
| Best for | Simple forms | Complex/dynamic forms, programmatic control |
Coding Challenges
Rebuild Chapter 8's signup form (name + email, both required, email validated) as a reactive form using FormGroup/FormControl, logging the form's value on submit and disabling the button while invalid.
๐ View solutionRewrite the same form using FormBuilder (fb.group) instead of new FormGroup/new FormControl, and add per-field validation messages reading each control's errors and touched state.
๐ View solutionBuild a reactive form with a "Fill demo data" button (using patchValue) and a "Reset" button (using reset()), plus subscribe to one field's valueChanges to log its value live as the user types.
๐ View solutionChapter 9 Quick Reference
- Reactive forms define the form in the class; require ReactiveFormsModule
- FormGroup = the whole form; FormControl = one field (initial value + validators)
- Template connects with [formGroup] and formControlName โ no
ngModel - Validators (required/email/minLength/pattern) come from the class, not template attributes
- FormBuilder (
fb.group({...})) โ the concise, conventional way to build the same structure - valueChanges Observable, plus setValue/patchValue/reset โ programmatic power over the form
- Never mix
ngModelandformControlNameon the same form - Next chapter: routing โ RouterModule, route params, and guards
Routing
Routing was a separate library install in React (React Router, Intermediate Chapter 5). In Angular it's part of the framework โ the same single-page navigation concepts apply directly, just with Angular's own API. This chapter covers defining routes, linking and navigating, reading URL parameters, and protecting routes with guards.
Defining Routes
Routes are an array of objects mapping a path to a component โ the direct equivalent of React Router's <Route> elements. :id marks a dynamic segment (matching /product/anything), and '**' is the catch-all for any unmatched URL โ both familiar from React Router, just expressed as data rather than JSX. This array is wired into the app's configuration once, in main.ts, via provideRouter(routes).
RouterOutlet and RouterLink
<router-outlet> is the placeholder where the matched route's component renders โ the equivalent of React Router's <Outlet /> (or the <Routes> block itself). routerLink="/about" navigates without a full page reload, exactly like React's <Link to> โ and the same warning applies: a plain <a href> would trigger a real reload and defeat the point.
Reading Route Parameters
ActivatedRoute โ injected like any service (Chapter 6) โ exposes the current route's parameters. snapshot.paramMap.get('id') reads the :id segment from the URL, the equivalent of React Router's useParams(). The snapshot form is the simplest; for a route that the same component stays on while only the parameter changes, subscribing to route.paramMap (an Observable, Chapter 11) reacts to those changes โ but snapshot suffices for most cases.
Programmatic Navigation
Injecting the Router service gives navigate([...]) for navigating from code โ after a form submits, a login succeeds, an action completes โ the equivalent of React Router's useNavigate(). The path is passed as an array of segments, which Angular joins into the URL.
Route Guards โ Protecting a Route
A guard is a function that runs before a route activates, returning true to allow it or redirecting otherwise โ there's no built-in equivalent in React Router itself (it's typically hand-rolled with a wrapper component). Added to a route via canActivate: [authGuard], this is the standard way to keep an unauthenticated user out of a protected page, redirecting them to login instead. Guards inject services freely, since they run inside Angular's DI context.
lazy/Suspense) directly with routing: { path: 'admin', loadComponent: () => import('./admin.component').then(m => m.AdminComponent) } loads that component's code only when its route is first visited. No separate Suspense boundary is needed โ the router handles the loading transition itself.
'**' route must be the last entry in the array โ placed earlier, it would match everything and prevent any route below it from ever being reached. The same ordering discipline applied to React Router's path="*".
| Angular | React Router equivalent |
|---|---|
| routes array + provideRouter | <Routes> / <Route> |
| <router-outlet> | <Outlet /> |
| routerLink="/x" | <Link to="/x"> |
| ActivatedRoute paramMap | useParams() |
| Router.navigate([...]) | useNavigate() |
| canActivate guard | Hand-rolled protected-route wrapper |
Coding Challenges
Set up a 3-page app (Home, About, Contact) with a routes array, a nav using routerLink, a router-outlet, and a wildcard route showing a NotFound component for unmatched URLs.
๐ View solutionAdd a product/:id route. From a product list, use routerLink (or Router.navigate) to go to a detail page that reads the id via ActivatedRoute and displays the matching product.
๐ View solutionBuild an authGuard (CanActivateFn) backed by a simple AuthService with an isLoggedIn flag, protecting a /dashboard route โ redirecting to /login when not logged in โ and a button toggling the logged-in state to test both outcomes.
๐ View solutionChapter 10 Quick Reference
- routes array (path โ component), wired via
provideRouter(routes)โ built into Angular - <router-outlet> renders the matched route; routerLink navigates without reload
- :id dynamic segments read via
ActivatedRoute.snapshot.paramMap.get('id') - Router.navigate([...]) โ programmatic navigation from code
- canActivate: [guard] โ a function gating a route, allowing or redirecting
- loadComponent in a route โ lazy-loads that component's code on first visit
- The
'**'wildcard route must be last; matching is top-to-bottom - Next chapter: HTTP client and RxJS โ talking to a backend with Observables
HTTP Client and RxJS Basics
React used fetch returning a Promise, awaited in a useEffect (Intermediate Chapter 6). Angular's HttpClient instead returns an Observable โ a stream of values from RxJS, the reactive library Angular is built on. The same Observable already appeared as valueChanges (Chapter 9) and route.paramMap (Chapter 10); this chapter covers it properly, alongside the HTTP client that produces them most often.
Observable vs Promise โ The Core Difference
A Promise resolves once with a single value. An Observable is a stream that can emit many values over time (or just one, like an HTTP response) โ and crucially, it does nothing at all until something subscribes to it. Where await kicks off a Promise immediately, an Observable is lazy: no subscription, no work. That laziness is what makes operators like cancellation, retrying, and debouncing possible in ways Promises can't match.
Setting Up HttpClient
provideHttpClient() registers the HTTP client with Angular's DI system once, the same pattern as provideRouter (Chapter 10). After that, any service can inject HttpClient and make requests.
An API Call Inside a Service
Per the Chapter 6 convention, API calls live in a service, not a component. http.get<User[]>(url) returns an Observable<User[]> โ the <User[]> generic tells TypeScript what shape the response data will be, giving full type safety on the result. Note the service just returns the Observable without subscribing; the component decides when to do that.
Consuming It โ Subscribe, or the async Pipe
The async pipe (Chapter 7) subscribes to the Observable, hands its emitted value to the template, and โ critically โ unsubscribes automatically when the component is destroyed. This is the preferred approach: it avoids manual subscription bookkeeping entirely, and the ; as users syntax captures the emitted array into a template variable to loop over. The naming convention users$ (trailing $) marks a property as an Observable at a glance.
RxJS Operators with pipe()
An Observable's .pipe(...) applies operators โ functions that transform the stream โ much like chaining array methods, but for values arriving over time. map transforms each emitted value (here filtering the user list), and catchError handles failures, returning a fallback Observable (of([]) emits a single empty array). This is RxJS's equivalent of the try/catch error handling from React's Intermediate Chapter 6, expressed as a pipeline.
valueChanges Observable plus these operators is exactly what enables debounced, live-reacting forms: searchControl.valueChanges.pipe(debounceTime(300), switchMap(term => this.api.search(term))) is the entire debounced-search-with-cancellation pattern that took React a custom useDebounce hook plus careful race-condition handling (Projects 4 and Intermediate Ch 6) โ here it's a few composed operators.
.subscribe() (option A) that isn't unsubscribed can leak memory and keep running after a component is gone โ the same category of issue as a React useEffect without a cleanup function (Fundamentals Chapter 8). The async pipe sidesteps this entirely by cleaning up for you, which is the main reason to prefer it. When a manual subscription is genuinely needed, unsubscribe in ngOnDestroy (next chapter) โ HTTP requests are a partial exception, as they complete after one emission, but it's safest to treat all subscriptions as needing cleanup.
| Angular | React equivalent |
|---|---|
| http.get<T>(url) | fetch(url).then(r => r.json()) |
| Returns an Observable | Returns a Promise |
| async pipe in template | Manual loading/data state in useState |
| .pipe(map, catchError) | Transforms + try/catch |
| debounceTime + switchMap | Custom useDebounce + race handling |
Coding Challenges
Set up provideHttpClient, build a service that fetches a list from a free public API returning an Observable, and a component that subscribes in ngOnInit and stores the result for display.
๐ View solutionRewrite Challenge 1's component to use the async pipe instead of a manual subscription โ exposing the Observable directly (users$) and consuming it in the template with @if (users$ | async; as users).
๐ View solutionAdd .pipe() to the service method with a map operator transforming the data (e.g. extracting just the names) and a catchError operator returning an empty array on failure, then display the transformed result.
๐ View solutionChapter 11 Quick Reference
- HttpClient returns an Observable, not a Promise โ provide it with
provideHttpClient() - An Observable is lazy: it does nothing until subscribed; it can emit many values over time
- API calls belong in a service (Chapter 6); the service returns the Observable unsubscribed
- async pipe โ subscribes, renders the value, and unsubscribes automatically (the preferred way)
- .pipe(map, catchError, ...) โ RxJS operators transforming the stream
- Convention: an Observable property ends in
$(e.g.users$) - Manual subscriptions need cleanup (
ngOnDestroy) โ the async pipe avoids that entirely - Next chapter: lifecycle hooks โ ngOnInit, ngOnChanges, ngOnDestroy
Lifecycle Hooks
React's useEffect (Fundamentals Chapter 8) collapsed "run on mount," "run on update," and "clean up on unmount" into one hook differentiated by its dependency array. Angular splits these into separate, explicitly-named lifecycle hook methods โ each a method with a fixed name that Angular calls at a specific moment. A component opts into one by implementing the matching interface and writing the method.
ngOnInit โ Setup After Creation
ngOnInit runs once, right after Angular has created the component and set its initial @Input values โ the standard place for setup work like an initial data fetch (used already in Chapter 11). It's the direct equivalent of React's useEffect(() => {...}, []) with an empty dependency array. Implementing OnInit isn't strictly required for the method to run, but doing so lets TypeScript catch a misspelled ngOnInit โ strongly recommended.
Why Not the Constructor?
A natural question: why not just do setup in the class constructor? The constructor runs when the object is first created, before Angular has finished wiring it up โ specifically before @Input properties have their values. Putting data fetching or any logic depending on inputs in ngOnInit guarantees those inputs are ready. The convention: the constructor is for dependency injection only (or just use inject()); ngOnInit is for actual initialization logic.
ngOnChanges โ Responding to Input Changes
ngOnChanges runs whenever an @Input value changes (and once initially, before ngOnInit) โ the rough equivalent of useEffect with a specific prop in its dependency array. The SimpleChanges argument is an object keyed by which inputs changed, each entry holding previousValue and currentValue โ useful when a component needs to react to which input changed and by how much, like recomputing a chart only when its data prop actually changes.
ngOnDestroy โ Cleanup Before Removal
ngOnDestroy runs just before Angular removes the component โ the place for cleanup, exactly mirroring the cleanup function returned from a React useEffect. This is where the manual subscriptions flagged in Chapter 11's warning get unsubscribed, timers cleared, and listeners removed. The same rule from React applies: anything that "starts" something ongoing needs a matching "stop" here, or it leaks.
Subscription and unsubscribing in ngOnDestroy is verbose. Two modern alternatives largely remove the need: the async pipe (Chapter 11) cleans up its own subscription automatically, and the takeUntilDestroyed() operator ties an Observable's lifetime to the component's automatically. Prefer those where possible; reach for an explicit ngOnDestroy for non-RxJS cleanup (a manual timer, a third-party library handle).
ngOnChanges reacts to changes in @Input properties specifically, not to internal state changes. And like React's dependency comparison, it detects a changed input by reference for objects/arrays โ mutating an object passed as an input in place (rather than passing a new one) won't be seen as a change, the same immutability concern from React's Fundamentals Chapter 3. Pass a new object/array to trigger ngOnChanges reliably.
| Angular hook | React useEffect equivalent |
|---|---|
| ngOnInit | useEffect(() => {...}, []) (mount) |
| ngOnChanges | useEffect(() => {...}, [someProp]) |
| ngOnDestroy | The cleanup function returned from useEffect |
Coding Challenges
Build a component implementing OnInit that logs a message and fetches/sets some initial data in ngOnInit, confirming (via console) that it runs once after the component is created.
๐ View solutionBuild a child component with an @Input value, implementing OnChanges to log the previousValue and currentValue each time the input changes. Drive it from a parent with a button that changes the input.
๐ View solutionBuild a component that starts an RxJS interval subscription in ngOnInit (logging a tick each second) and properly unsubscribes in ngOnDestroy. Toggle the component's presence with @if in a parent to confirm the ticking stops when it's removed.
๐ View solutionChapter 12 Quick Reference
- ngOnInit โ runs once after creation; the place for initial setup/data fetching (mount equivalent)
- Use
ngOnInit, not the constructor, for init logic โ inputs aren't ready in the constructor - ngOnChanges(changes) โ runs when an
@Inputchanges;SimpleChangesgives previous/current values - ngOnDestroy โ runs before removal; the place for cleanup (unsubscribe, clear timers)
- Implement the matching interface (
OnInit/OnChanges/OnDestroy) for type safety - The
asyncpipe andtakeUntilDestroyed()reduce the need for manualngOnDestroycleanup ngOnChangesdetects object/array input changes by reference โ pass new ones, don't mutate- Next chapter: standalone components and signals โ Angular's modern direction
Standalone Components and Signals
This whole course has quietly used the modern direction already โ every component has been standalone: true, and Chapters 10โ11 used provideRouter/provideHttpClient rather than the older module setup. This chapter names those choices explicitly, then introduces signals โ Angular's newer reactivity primitive, the closest thing in Angular to React's useState.
Standalone Components, Recapped
Historically, Angular grouped components into NgModules โ separate files declaring which components, directives, and pipes belonged together and what they could use. Standalone components (now the default) drop that layer: each component declares its own imports directly, as seen in every example so far. The result is less boilerplate, no app.module.ts to maintain, and an import list that lives right next to the component using it.
@NgModule with a declarations array and a root AppModule. It's fully supported and not going away soon, but new projects should use standalone (the ng new default). Recognize the module-based structure when reading older code; this course deliberately teaches only the standalone approach.
Signals โ A Reactive Value
signal(0) creates a reactive value holding 0. Reading it is a function call โ count(), both in the template and in code. Writing is done with .set(value) (a new value) or .update(fn) (based on the current one) โ directly parallel to React's setCount(5) and setCount(n => n + 1). When a signal changes, Angular knows precisely which parts of the template depend on it and updates only those, more surgically than the older change-detection mechanism.
| Signal | React useState equivalent |
|---|---|
| count = signal(0) | const [count, setCount] = useState(0) |
| count() | count (read) |
| count.set(5) | setCount(5) |
| count.update(n => n + 1) | setCount(n => n + 1) |
computed โ Derived Signals
computed derives a new signal from others โ its value automatically recalculates whenever any signal it reads changes, and (importantly) is cached until then, so it only recomputes when genuinely needed. This is the signals version of React's useMemo (Intermediate Chapter 7), but the dependency tracking is automatic: there's no dependency array to maintain โ Angular knows total depends on price and quantity simply because it read them.
effect โ Reacting to Signal Changes
effect runs a side effect whenever any signal it reads changes โ automatically tracked, the same way computed works. It's the rough equivalent of React's useEffect with the relevant value in its dependency array, again with no manual dependency list. Effects are for genuine side effects (logging, syncing to localStorage); deriving a value should use computed instead.
Why Signals Matter
Signals are Angular's strategic direction for reactivity โ gradually offering a simpler, more explicit, and more performant alternative to the change-detection-plus-RxJS model that came before. They don't replace RxJS (Observables are still the right tool for streams of events and HTTP, Chapter 11), but for component-local state, signals are increasingly the recommended default. Inputs can even be signals now (input() instead of @Input()), and the async pipe has a signal counterpart in toSignal() โ the ecosystem is steadily building around them.
signal/set/update is useState, computed is useMemo, effect is useEffect โ all with automatic dependency tracking instead of manual arrays. If the rest of Angular felt like a different paradigm, this is the chapter where it converges back toward familiar ground.
| Angular signals | React hooks |
|---|---|
| signal() | useState |
| computed() | useMemo (auto-tracked) |
| effect() | useEffect (auto-tracked) |
Coding Challenges
Build a counter using a signal, with +1/-1/reset buttons calling set/update, reading the value with count() in the template.
๐ View solutionBuild a component with price and quantity signals and a computed total signal, plus inputs/buttons to change price and quantity, confirming total updates automatically with no manual recalculation.
๐ View solutionBuild a component with a signal whose value is persisted to localStorage via an effect (saving on every change) and read back as the initial value, so it survives a page refresh โ the signals version of the React useLocalStorage hook.
๐ View solutionChapter 13 Quick Reference
- Standalone components (the default) replace NgModules; each declares its own
imports - signal(value) โ a reactive value; read with
x(), write with.set()/.update()(=useState) - computed(() => ...) โ a derived signal, auto-recalculated and cached (=
useMemo, no dep array) - effect(() => ...) โ runs a side effect when read signals change (=
useEffect, auto-tracked) - Signals are Angular's strategic direction for reactivity โ preferred for component-local state
- Signals complement, don't replace, RxJS โ Observables still own event streams and HTTP
- Next chapter: testing โ Jasmine, Karma, and TestBed (the final chapter)
Testing โ Jasmine, Karma, and TestBed
Every ng generate component has quietly created a .spec.ts file alongside each component (Chapter 1) โ those are tests, and this chapter finally puts them to use. Angular's testing stack uses Jasmine (the test framework: describe/it/expect) run by Karma (the test runner, executing tests in a real browser), with TestBed as Angular's own utility for creating components in a test environment.
it() for React's test(), jasmine.createSpy() for vi.fn() โ but the goals are identical: render a component, simulate interaction, assert on what the user would see. If that chapter made sense, this one will feel familiar despite the different tool names.
A Test for a Service โ The Simplest Case
describe groups related tests; it defines one; beforeEach runs before each test for fresh setup; expect(...).toBe(...) asserts. TestBed.inject(CounterService) retrieves the service through the same DI system the real app uses (Chapter 6), rather than constructing it manually โ so the test exercises it exactly as it'd be used in practice.
Testing a Component with TestBed
TestBed.createComponent returns a fixture โ a handle to the live component plus its rendered DOM (fixture.nativeElement). The key Angular-specific detail is fixture.detectChanges(): unlike React Testing Library, which re-renders automatically, an Angular test must explicitly trigger change detection to render the initial view and to update it after a state change. Forgetting it is the most common reason an Angular test sees stale, un-updated DOM.
Mocking a Dependency
Dependency injection makes testing easier: a component asks for UserService, and the test simply provides a fake one instead via { provide: UserService, useValue: fakeUserService }. The component is none the wiser โ it gets whatever DI supplies. This means a component can be tested in isolation, with a fake service returning fixed data, without ever hitting a real API โ the same goal as React's mock functions (Advanced Chapter 4), achieved through DI rather than passing mocks as props.
fixture.detectChanges() first โ after creating the component (to render the initial view) and after any action that changes state (to re-render). React Testing Library handles this automatically, so it's an easy habit to miss when coming from that background. If a test sees an empty or unchanged DOM, a missing detectChanges() is the first thing to check.
| Angular (Jasmine/TestBed) | React (Vitest/RTL) |
|---|---|
| describe / it / expect | describe / test / expect |
| TestBed.createComponent | render(...) |
| fixture.nativeElement.querySelector | screen.getByRole / getByText |
| fixture.detectChanges() | (automatic) |
| jasmine.createSpy() | vi.fn() |
| { provide, useValue } mock | Mock passed as a prop |
Running the Tests
ng test runs the whole suite, opening a browser Karma controls to execute the tests in, and re-running automatically as files change โ the equivalent of a test runner's watch mode. The default starter project already passes its one generated AppComponent test, so ng test works from the very first ng new.
Coding Challenges
Write a spec for a CounterService (with increment/decrement/reset) using TestBed.inject, asserting the count behaves correctly after each operation.
๐ View solutionWrite a component spec using TestBed.createComponent that clicks the +1 button and asserts the rendered text updates โ remembering detectChanges() after creation and after the click.
๐ View solutionWrite a spec for a component that depends on a UserService, providing a fake service (via { provide, useValue }) returning fixed data, and assert the component renders that fake data without any real HTTP call.
๐ View solutionChapter 14 Quick Reference
- Jasmine โ the test framework (
describe/it/expect/beforeEach); Karma โ the runner - TestBed.inject(Service) โ gets a service through DI for testing
- TestBed.createComponent โ a fixture (live component +
nativeElementDOM) - fixture.detectChanges() โ manually trigger render; required after creation and state changes
- Mock a dependency with { provide: Service, useValue: fake } โ DI makes isolation easy
- ng test โ runs the suite in watch mode via Karma
- Same goals as React Testing Library; different tool names (
it=test,createSpy=vi.fn)