Routing and forms
Route configuration with lazy loading and functional guards, then typed reactive forms with validation that the user can act on.
Routes, lazy loading and guards
// app.routes.ts
import { Routes, Router, CanActivateFn } from '@angular/router'
import { inject } from '@angular/core'
import { AuthStore } from './auth.store'
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthStore)
return auth.signedIn() ? true : inject(Router).createUrlTree(['/login'])
}
export const routes: Routes = [
{ path: '', loadComponent: () => import('./home.component').then(m => m.HomeComponent) },
{
path: 'guides/:id',
loadComponent: () => import('./guide.component').then(m => m.GuideComponent)
},
{
path: 'admin',
canActivate: [authGuard],
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
},
{ path: '**', loadComponent: () => import('./not-found.component').then(m => m.NotFoundComponent) }
]// main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter, withComponentInputBinding, withInMemoryScrolling } from '@angular/router'
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding(), withInMemoryScrolling({ scrollPositionRestoration: 'top' }))
]
})loadComponentandloadChildrensplit routes into separate bundles, so the first screen does not download the admin section.- With
withComponentInputBinding(), a route parameter lands in aninput()of the same name instead of being read from a snapshot. - Functional guards use
inject()and return a boolean or aUrlTree; returning a tree is cleaner than navigating imperatively. withInMemoryScrollingrestores scroll position on navigation, which users notice when it is missing.'**'must be the last route — it is the wildcard that catches everything.
Typed reactive forms
import { Component, inject } from '@angular/core'
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'
import { JsonPipe } from '@angular/common'
@Component({
selector: 'app-signup',
imports: [ReactiveFormsModule, JsonPipe],
templateUrl: './signup.component.html'
})
export class SignupComponent {
private fb = inject(FormBuilder)
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(12)]],
plan: ['free' as 'free' | 'pro', Validators.required]
})
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched() // show errors on every control
return
}
const value = this.form.getRawValue() // fully typed payload
this.save(value)
}
}<form [formGroup]="form" (ngSubmit)="submit()">
<label for="email">Email</label>
<input id="email" type="email" formControlName="email"
[attr.aria-invalid]="form.controls.email.invalid && form.controls.email.touched" />
@if (form.controls.email.touched && form.controls.email.hasError('email')) {
<p class="error">Enter a valid email address.</p>
}
<button type="submit" [disabled]="form.invalid">Create account</button>
</form>| Need | Approach |
|---|---|
| A few simple fields | FormsModule with ngModel |
| Typed model, async validators, dynamic rows | Reactive forms with FormBuilder |
| Cross-field rule | A validator on the group, not on one control |
| Sync with a server check | Async validator returning an error map or null |
| Repeatable block of fields | FormArray |
⚠️
Client-side validation is a courtesy, not a security control. Validate every payload again on the server, and design messages so the user knows which field to change rather than that something failed.
Navigating and reading data in a component
import { Component, inject, input, computed } from '@angular/core'
import { RouterLink, RouterOutlet } from '@angular/router'
@Component({
selector: 'app-guide',
imports: [RouterLink, RouterOutlet],
template: `
<h1>Guide {{ id() }}</h1>
<a routerLink="/guides" [queryParams]="{ page: 2 }">Back to list</a>
<router-outlet />
`
})
export class GuideComponent {
readonly id = input.required<string>() // bound from the :id route parameter
readonly router = inject(Router)
}routerLinkrenders a real anchor, so middle-click and open-in-new-tab keep working; useRouter.navigateonly for programmatic redirects.- Prefer route inputs over reading a route snapshot: the component reacts when the parameter changes instead of being recreated.
- A resolver or a route-level
resourcecan load data before activation, but a loading state inside the component usually feels faster.
FAQ
Template-driven or reactive forms?
Reactive forms for anything with validation, typing or dynamic structure — the model is in the class, which makes it testable. Template-driven is fine for a one-field search box.
Why is my route parameter not updating?
You read
route.snapshot once, so the value is captured at creation. Read the observable stream, or bind the parameter to a component input.Related
Services and dependency injection File-based routing and layouts
Last refreshed 2026-09-18.