From angular-plugin
Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).
How this skill is triggered — by the user, by Claude, or both
Slash command
/angular-plugin:angular-routingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Angular Router is built-in (`@angular/router`) — no third-party. This skill covers configuration, navigation, guards, lazy loading.
Angular Router is built-in (@angular/router) — no third-party. This skill covers configuration, navigation, guards, lazy loading.
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './core/auth/auth.guard';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component').then((m) => m.DashboardComponent),
canActivate: [authGuard],
},
{
path: 'users',
loadChildren: () => import('./features/users/users.routes').then((m) => m.usersRoutes),
},
{ path: '**', component: NotFoundComponent },
];
// main.ts (or app.config.ts)
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
provideHttpClient(),
],
});
provideRouter features:
withComponentInputBinding() — auto-binds route params to component @Input() / input().withViewTransitions() — uses View Transitions API for navigation animations.withInMemoryScrolling({ scrollPositionRestoration: 'enabled' }) — restore scroll on back nav.withDebugTracing() — log all navigation events.// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [ /* same shape */ ];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
// AppModule imports AppRoutingModule
<router-outlet> in app.component.html renders the matched component.
{
path: 'users',
component: UsersComponent,
data: { title: 'Users' }, // arbitrary route metadata
resolve: { users: usersResolver }, // pre-load data
canActivate: [authGuard],
canDeactivate: [unsavedChangesGuard],
children: [
{ path: '', component: UsersListComponent },
{ path: ':id', component: UserDetailComponent, resolve: { user: userResolver } },
{ path: ':id/edit', component: UserEditComponent, canDeactivate: [unsavedChangesGuard] },
],
}
{ path: '', component: HomeComponent } // empty path (root)
{ path: 'users', component: UsersComponent } // /users
{ path: 'users/:id', component: UserDetailComponent } // /users/123
{ path: 'users/:id/posts/:postId' } // multiple params
{ path: 'docs/**', component: DocsComponent } // wildcard catch-all
{ path: '**', component: NotFoundComponent } // global 404 (always last)
{ path: 'old', redirectTo: 'new', pathMatch: 'full' } // redirect
pathMatch: 'full' for redirects — otherwise path: '' matches every URL.
Standalone component:
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component').then((m) => m.DashboardComponent),
}
Feature routes file:
// features/users/users.routes.ts
import { Routes } from '@angular/router';
import { UsersListComponent } from './users-list.component';
export const usersRoutes: Routes = [
{ path: '', component: UsersListComponent },
{ path: ':id', loadComponent: () => import('./user-detail.component').then((m) => m.UserDetailComponent) },
];
// app.routes.ts
{
path: 'users',
loadChildren: () => import('./features/users/users.routes').then((m) => m.usersRoutes),
}
NgModule (legacy):
{
path: 'users',
loadChildren: () => import('./features/users/users.module').then((m) => m.UsersModule),
}
Lazy loading creates a separate JS chunk — first navigation downloads it, subsequent uses cached.
// core/auth/auth.guard.ts
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
// Redirect to login with return URL
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
};
Apply:
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }
{ path: 'admin', component: AdminComponent, canActivate: [authGuard, roleGuard('admin')] }
Composed guards run in order; if any returns false / UrlTree, route blocks.
export const roleGuard = (requiredRole: string): CanActivateFn => (route, state) => {
const auth = inject(AuthService);
return auth.hasRole(requiredRole);
};
// Usage
{ canActivate: [roleGuard('admin')] }
| Guard | When |
|---|---|
canActivate | Before activating route |
canActivateChild | Before activating child routes |
canDeactivate | Before leaving route (unsaved changes prompt) |
canLoad | Before lazy-loading the route module/component (deprecated in favor of canMatch) |
canMatch | Conditionally match the route (Angular 14.2+) |
resolve | Pre-fetch data before route activates |
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
// ...
}
}
Still works but functional API is preferred. Don't migrate existing class guards as part of feature work unless BA spec asks.
Pre-fetch data before route activates. Component receives data via route.data instead of loading inside ngOnInit().
// users.resolver.ts
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UsersService } from './users.service';
import type { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const usersService = inject(UsersService);
const id = route.paramMap.get('id')!;
return usersService.getUser(id);
};
// In route config
{ path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }
// In component
@Component({...})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data['user'] as User; // sync access
// OR reactive
user$ = this.route.data.pipe(map((data) => data['user'] as User));
}
Resolvers return Observable, Promise, or value. Navigation pauses until resolution completes.
import { Router } from '@angular/router';
import { inject } from '@angular/core';
@Component({...})
export class MyComponent {
private router = inject(Router);
goToProfile(id: string) {
this.router.navigate(['/users', id]);
}
goRelative() {
this.router.navigate(['..'], { relativeTo: this.route });
}
navigateWithQuery() {
this.router.navigate(['/search'], { queryParams: { q: 'hello', page: 1 } });
}
replaceUrl() {
this.router.navigate(['/login'], { replaceUrl: true });
}
}
router.navigate(commands, extras?) — commands array; first element is path, rest are segments.
router.navigateByUrl('/users/123?tab=overview') — string URL alternative.
private route = inject(ActivatedRoute);
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
const tab = this.route.snapshot.queryParamMap.get('tab') ?? 'overview';
}
Snapshot reflects route at component creation. Won't update if user navigates within same component.
this.route.paramMap
.pipe(takeUntilDestroyed())
.subscribe((params) => {
this.id = params.get('id')!;
});
this.route.queryParamMap
.pipe(takeUntilDestroyed())
.subscribe((qp) => {
this.tab = qp.get('tab') ?? 'overview';
});
Updates on every route change within same component instance.
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
id = toSignal(this.route.paramMap.pipe(map((p) => p.get('id') ?? '')), { initialValue: '' });
tab = toSignal(this.route.queryParamMap.pipe(map((q) => q.get('tab') ?? 'overview')), { initialValue: 'overview' });
Use signals when consuming in computed / template:
title = computed(() => `User ${this.id()}`);
withComponentInputBinding() (Angular 16+)provideRouter(routes, withComponentInputBinding()),
Route params auto-bind to component inputs:
@Component({ ... })
export class UserDetailComponent {
@Input() id!: string; // bound from route param :id
@Input() tab = 'overview'; // bound from query ?tab=
}
In Angular 17.1+ with signal inputs:
id = input.required<string>(); // signal version
tab = input<string>('overview');
Cleanest pattern. No need for ActivatedRoute injection in many cases.
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">{{ user.name }}</a>
<a [routerLink]="['/search']" [queryParams]="{ q: 'hello' }">Search</a>
<a routerLink="/dashboard" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
For external URLs, use plain <a href="..."> — routerLink is for internal nav only.
data{ path: 'admin', component: AdminComponent, data: { title: 'Admin', requiresAuth: true } }
Read in component:
this.route.data.subscribe((data) => {
this.title = data['title'];
});
For app-wide title management:
// In root component
this.router.events
.pipe(
filter((e) => e instanceof NavigationEnd),
takeUntilDestroyed()
)
.subscribe(() => {
let route = this.route;
while (route.firstChild) route = route.firstChild;
const title = route.snapshot.data['title'] ?? 'My App';
this.titleService.setTitle(title);
});
Or use provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'always' })) + Title service.
Or Angular 14.1+: title route property, set automatically:
{ path: 'admin', component: AdminComponent, title: 'Admin | My App' }
{ path: 'users/:id', component: UserDetailComponent, title: userTitleResolver }
<router-outlet> in layout — routes don't render.<a href="/internal"> for internal nav — full reload, defeats SPA.app.routes.ts or shared file.route.params without takeUntilDestroyed() — memory leak.Observable<never> from a guard without UrlTree — guard hangs.pathMatch: 'full' missing on redirect — redirect catches every URL.<router-outlet> without named outlets — only the first activates.npx claudepluginhub aratkruglik/claude-sdlc --plugin angular-pluginConfigures routing in Angular v20+ with lazy loading, functional guards, resolvers, and signal-based route parameters. Use for navigation setup, protected routes, and nested routing.
Helps build module-free Angular apps using standalone components, bootstrapApplication, and lazy-loaded routes. Covers migration, code generation, and provider scoping.
Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).